CluPilotCloud/tests/Feature/Auth/UnverifiedAccountTest.php

165 lines
7.0 KiB
PHP

<?php
use App\Console\Commands\PruneUnverifiedAccounts;
use App\Livewire\Auth\VerifyEmail;
use App\Models\Customer;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Support\Facades\File;
use Livewire\Livewire;
/**
* A registration nobody confirmed does not sit in the database for ever.
*
* The page used to say "sign out and register again", which cannot work: signing
* out frees nothing, the address is still taken by the unique index, and the
* abandoned row stays. So the button discards the account, and anything nobody
* comes back to goes by itself after five days.
*/
it('discards the registration and frees the address', function () {
$user = User::factory()->create(['email' => 'neu@example.test', 'email_verified_at' => null]);
Livewire::actingAs($user)
->test(VerifyEmail::class)
->call('deleteAccount');
expect(User::query()->where('email', 'neu@example.test')->exists())->toBeFalse();
});
it('refuses to discard an account that already has a contract behind it', function () {
// The webhook creates a customer's login from a paid checkout, and such a
// user can sit on this page unconfirmed while already having a contract.
// Deleting that is data loss dressed up as a convenience.
$user = User::factory()->create(['email' => 'zahlt@example.test', 'email_verified_at' => null]);
$customer = Customer::factory()->create(['email' => 'zahlt@example.test', 'user_id' => $user->id]);
Subscription::factory()->create(['customer_id' => $customer->id, 'status' => 'active']);
Livewire::actingAs($user)
->test(VerifyEmail::class)
->call('deleteAccount')
->assertDispatched('notify');
expect($user->fresh())->not->toBeNull();
});
it('sends a confirmed account to its dashboard instead of deleting it', function () {
// Closing a confirmed account is a different action with an invoice history
// behind it — ConfirmCloseAccount, not this.
//
// Confirmed AFTER mounting, because mount() already redirects a verified
// user away and there would be no component left to call anything on. The
// case this guards is the real one: a tab left open on this page while the
// link was opened in another window.
$user = User::factory()->create(['email_verified_at' => null]);
$page = Livewire::actingAs($user)->test(VerifyEmail::class);
$user->forceFill(['email_verified_at' => now()])->save();
$page->call('deleteAccount');
expect($user->fresh())->not->toBeNull();
});
it('confirms in a modal rather than on one click', function () {
// R23. And the modal mutates nothing itself: it dispatches the event the
// page listens for, so the checks stay in the one place they already are.
$page = File::get(resource_path('views/livewire/auth/verify-email.blade.php'));
expect($page)->toContain("component: 'confirm-delete-own-account'");
$modal = File::get(app_path('Livewire/ConfirmDeleteOwnAccount.php'));
expect($modal)->toContain("dispatch('own-account-delete-confirmed')")
->and($modal)->not->toContain('$user');
});
it('no longer tells anybody to sign out and register again', function () {
// Advice that could not work: signing out frees nothing.
expect(__('verify_email.notice_delete'))->not->toBe('verify_email.notice_delete');
foreach (['de', 'en'] as $locale) {
expect(File::get(base_path("lang/{$locale}/verify_email.php")))
->not->toContain("'notice_signout'");
}
});
// ---- The five-day sweep ----
it('removes a registration nobody confirmed after five days', function () {
$stale = User::factory()->create(['email_verified_at' => null]);
$stale->forceFill(['created_at' => now()->subDays(PruneUnverifiedAccounts::AFTER_DAYS + 1)])->save();
$this->artisan('clupilot:prune-unverified')->assertSuccessful();
expect(User::query()->whereKey($stale->id)->exists())->toBeFalse();
});
it('leaves a fresh registration alone', function () {
// Long enough for somebody who registered on a Friday and found the mail in
// a spam folder on Monday.
$fresh = User::factory()->create(['email_verified_at' => null]);
$fresh->forceFill(['created_at' => now()->subDays(PruneUnverifiedAccounts::AFTER_DAYS - 1)])->save();
$this->artisan('clupilot:prune-unverified');
expect(User::query()->whereKey($fresh->id)->exists())->toBeTrue();
});
it('never touches a confirmed account, however old', function () {
$old = User::factory()->create(['email_verified_at' => now()->subYear()]);
$old->forceFill(['created_at' => now()->subYears(2)])->save();
$this->artisan('clupilot:prune-unverified');
expect(User::query()->whereKey($old->id)->exists())->toBeTrue();
});
it('never touches an unconfirmed account with a customer behind it', function () {
// Both links count: a customers row can point at the user, or merely carry
// the same address — see Customer::emailTaken for why one can exist without
// the other.
$byId = User::factory()->create(['email_verified_at' => null]);
$byId->forceFill(['created_at' => now()->subDays(30)])->save();
Customer::factory()->create(['user_id' => $byId->id, 'email' => 'andere@example.test']);
$byEmail = User::factory()->create(['email' => 'gleich@example.test', 'email_verified_at' => null]);
$byEmail->forceFill(['created_at' => now()->subDays(30)])->save();
Customer::factory()->create(['email' => 'gleich@example.test', 'user_id' => null]);
$this->artisan('clupilot:prune-unverified');
expect(User::query()->whereKey($byId->id)->exists())->toBeTrue()
->and(User::query()->whereKey($byEmail->id)->exists())->toBeTrue();
});
it('says what it would remove without removing it', function () {
$stale = User::factory()->create(['email' => 'weg@example.test', 'email_verified_at' => null]);
$stale->forceFill(['created_at' => now()->subDays(30)])->save();
$this->artisan('clupilot:prune-unverified --dry-run')
->expectsOutputToContain('weg@example.test')
->assertSuccessful();
expect(User::query()->whereKey($stale->id)->exists())->toBeTrue();
});
it('runs on the schedule, or it never runs at all', function () {
$console = File::get(base_path('routes/console.php'));
expect($console)->toContain("Schedule::command('clupilot:prune-unverified')")
->toContain('withoutOverlapping');
});
it('tells the person about the deadline, on the page and in the mail', function () {
// Somebody who never got the mail is looking at the PAGE, and the deadline
// is the reason they do not have to do anything about an abandoned attempt.
// Matched on the number, not on a whole phrase: "fünf Tage lang" and "nach
// fünf Tagen" are both correct German and an assertion on one of them tells
// the next person to write the sentence a particular way.
expect(__('verify_email.notice_wrong_address'))->toContain('fünf')
->and(__('verify_email.not_you'))->toContain('fünf')
// And the sentence and the command agree on the number.
->and(PruneUnverifiedAccounts::AFTER_DAYS)->toBe(5);
});