165 lines
7.0 KiB
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);
|
|
});
|