205 lines
7.7 KiB
PHP
205 lines
7.7 KiB
PHP
<?php
|
|
|
|
use App\Console\Commands\PruneDormantAccounts;
|
|
use App\Console\Commands\PruneUnverifiedAccounts;
|
|
use App\Mail\DormantAccountWarningMail;
|
|
use App\Models\Customer;
|
|
use App\Models\User;
|
|
use App\Models\UserDevice;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Mail;
|
|
|
|
/**
|
|
* A confirmed account that never had a package goes after a year.
|
|
*
|
|
* The five-day sweep is about registrations nobody confirmed — see
|
|
* UnverifiedAccountTest. This is the other half of the same question the owner
|
|
* asked: "wird nun der account gelöscht nach 5 tagen?" No. Only an unconfirmed
|
|
* one. A confirmed account stays, unless a year passes with no package on it at
|
|
* all — and then it is told a fortnight in advance.
|
|
*/
|
|
function dormantUser(int $daysOld, array $attributes = []): User
|
|
{
|
|
$user = User::factory()->create(array_merge(['email_verified_at' => now()->subDays($daysOld)], $attributes));
|
|
|
|
$user->forceFill(['created_at' => now()->subDays($daysOld)])->save();
|
|
|
|
return $user;
|
|
}
|
|
|
|
it('warns a fortnight before, rather than deleting out of the blue', function () {
|
|
Mail::fake();
|
|
|
|
$user = dormantUser(PruneDormantAccounts::AFTER_DAYS - PruneDormantAccounts::WARN_DAYS_BEFORE);
|
|
|
|
$this->artisan('clupilot:prune-dormant')->assertSuccessful();
|
|
|
|
Mail::assertQueued(DormantAccountWarningMail::class, fn ($mail) => $mail->hasTo($user->email));
|
|
|
|
// Still here. The warning is a warning, not the deletion.
|
|
expect($user->fresh())->not->toBeNull()
|
|
->and($user->fresh()->dormant_warned_at)->not->toBeNull();
|
|
});
|
|
|
|
it('warns once, not every night for a fortnight', function () {
|
|
Mail::fake();
|
|
|
|
dormantUser(PruneDormantAccounts::AFTER_DAYS - 5);
|
|
|
|
$this->artisan('clupilot:prune-dormant');
|
|
$this->artisan('clupilot:prune-dormant');
|
|
$this->artisan('clupilot:prune-dormant');
|
|
|
|
Mail::assertQueuedCount(1);
|
|
});
|
|
|
|
it('deletes the account a year on, once the warning has had its fortnight', function () {
|
|
Mail::fake();
|
|
|
|
$user = dormantUser(PruneDormantAccounts::AFTER_DAYS + 1);
|
|
$user->forceFill(['dormant_warned_at' => now()->subDays(PruneDormantAccounts::WARN_DAYS_BEFORE + 1)])->save();
|
|
|
|
$this->artisan('clupilot:prune-dormant')->assertSuccessful();
|
|
|
|
expect(User::query()->whereKey($user->id)->exists())->toBeFalse();
|
|
});
|
|
|
|
it('never deletes an account that was never warned', function () {
|
|
// The stamp is what permits the deletion. An installation whose mail server
|
|
// was down for a month delays the deletion; it does not skip the notice.
|
|
Mail::fake();
|
|
|
|
$user = dormantUser(PruneDormantAccounts::AFTER_DAYS * 2);
|
|
|
|
$this->artisan('clupilot:prune-dormant');
|
|
|
|
expect(User::query()->whereKey($user->id)->exists())->toBeTrue();
|
|
});
|
|
|
|
it('gives the warning its full fortnight before deleting', function () {
|
|
Mail::fake();
|
|
|
|
$user = dormantUser(PruneDormantAccounts::AFTER_DAYS + 30);
|
|
$user->forceFill(['dormant_warned_at' => now()->subDays(PruneDormantAccounts::WARN_DAYS_BEFORE - 1)])->save();
|
|
|
|
$this->artisan('clupilot:prune-dormant');
|
|
|
|
expect(User::query()->whereKey($user->id)->exists())->toBeTrue();
|
|
});
|
|
|
|
it('leaves an account alone that signed in inside the year', function () {
|
|
// What the terms promise: signing in once starts the period again. There is
|
|
// no last_login_at on users, and the device rows carry exactly this.
|
|
Mail::fake();
|
|
|
|
$user = dormantUser(PruneDormantAccounts::AFTER_DAYS * 3);
|
|
UserDevice::query()->create([
|
|
'guard' => 'web',
|
|
'authenticatable_id' => $user->id,
|
|
'token_hash' => hash('sha256', 'egal'),
|
|
'name' => 'Firefox',
|
|
'last_seen_at' => now()->subDays(30),
|
|
]);
|
|
|
|
$this->artisan('clupilot:prune-dormant');
|
|
|
|
Mail::assertNothingQueued();
|
|
expect(User::query()->whereKey($user->id)->exists())->toBeTrue();
|
|
});
|
|
|
|
it('counts a device of another guard as nobody, because operator 1 is not customer 1', function () {
|
|
// R21. A device row of the operator guard with the same id says nothing
|
|
// about this customer, and treating it as activity would keep an abandoned
|
|
// account alive for as long as an operator keeps signing in.
|
|
Mail::fake();
|
|
|
|
$user = dormantUser(PruneDormantAccounts::AFTER_DAYS - 1);
|
|
UserDevice::query()->create([
|
|
'guard' => 'operator',
|
|
'authenticatable_id' => $user->id,
|
|
'token_hash' => hash('sha256', 'anders'),
|
|
'name' => 'Konsole',
|
|
'last_seen_at' => now(),
|
|
]);
|
|
|
|
$this->artisan('clupilot:prune-dormant');
|
|
|
|
Mail::assertQueued(DormantAccountWarningMail::class);
|
|
});
|
|
|
|
it('never touches an unconfirmed account, which is the other sweep business', function () {
|
|
Mail::fake();
|
|
|
|
$user = dormantUser(PruneDormantAccounts::AFTER_DAYS * 2, ['email_verified_at' => null]);
|
|
|
|
$this->artisan('clupilot:prune-dormant');
|
|
|
|
Mail::assertNothingQueued();
|
|
expect(User::query()->whereKey($user->id)->exists())->toBeTrue();
|
|
});
|
|
|
|
it('never touches an account with a customer record, by id or by address', function () {
|
|
// A customer record means somebody in the business knows this person, and
|
|
// everything commercial — orders, contracts, invoices kept for seven years —
|
|
// hangs off one. Both links count: see Customer::emailTaken.
|
|
Mail::fake();
|
|
|
|
$byId = dormantUser(PruneDormantAccounts::AFTER_DAYS * 2);
|
|
Customer::factory()->create(['user_id' => $byId->id, 'email' => 'andere@example.test']);
|
|
|
|
$byEmail = dormantUser(PruneDormantAccounts::AFTER_DAYS * 2, ['email' => 'gleich@example.test']);
|
|
Customer::factory()->create(['email' => 'gleich@example.test', 'user_id' => null]);
|
|
|
|
$this->artisan('clupilot:prune-dormant');
|
|
|
|
Mail::assertNothingQueued();
|
|
expect(User::query()->whereKey($byId->id)->exists())->toBeTrue()
|
|
->and(User::query()->whereKey($byEmail->id)->exists())->toBeTrue();
|
|
});
|
|
|
|
it('says what it would do without doing it', function () {
|
|
Mail::fake();
|
|
|
|
$user = dormantUser(PruneDormantAccounts::AFTER_DAYS - 1, ['email' => 'ruht@example.test']);
|
|
|
|
$this->artisan('clupilot:prune-dormant --dry-run')
|
|
->expectsOutputToContain('ruht@example.test')
|
|
->assertSuccessful();
|
|
|
|
Mail::assertNothingQueued();
|
|
expect($user->fresh()->dormant_warned_at)->toBeNull();
|
|
});
|
|
|
|
it('runs on the schedule, or it never runs at all', function () {
|
|
// Daily even though the window is a year: the notice is what permits the
|
|
// deletion, so the run that sends it has to happen every day.
|
|
expect(File::get(base_path('routes/console.php')))
|
|
->toContain("Schedule::command('clupilot:prune-dormant')")
|
|
->toContain('withoutOverlapping');
|
|
});
|
|
|
|
// ---- What a customer is told ----
|
|
|
|
it('states both deadlines in the portal, not just the five days', function () {
|
|
// The question this answers: "wird nun der account gelöscht nach 5 tagen?"
|
|
// Reading only the verification page, the answer looked like yes.
|
|
$settings = File::get(resource_path('views/livewire/settings.blade.php'));
|
|
|
|
expect($settings)->toContain("__('settings.lifecycle_unverified'")
|
|
->and($settings)->toContain("__('settings.lifecycle_dormant'")
|
|
// From the commands themselves, so the page cannot promise one number
|
|
// while the sweep uses another.
|
|
->and($settings)->toContain('PruneUnverifiedAccounts::AFTER_DAYS')
|
|
->and($settings)->toContain('PruneDormantAccounts::WARN_DAYS_BEFORE');
|
|
});
|
|
|
|
it('regulates both deadlines in the terms, because that is where it was promised', function () {
|
|
$terms = $this->get(route('legal.agb'))->assertOk()->getContent();
|
|
|
|
expect($terms)->toContain('Löschfristen')
|
|
->and($terms)->toContain('Nicht bestätigte Registrierungen')
|
|
->and($terms)->toContain('Bestätigte Konten ohne Paket')
|
|
// And the number in the text is the number the command uses.
|
|
->and($terms)->toContain(PruneUnverifiedAccounts::AFTER_DAYS.' Tagen');
|
|
});
|