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); });