mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-16 16:45:07 +00:00
cab9291d29
Failed queue jobs and read notifications both grow with use, and neither ever shrank on its own. The failed-jobs list waited for somebody to press "Delete all failed" — a fine tool for a backlog you are looking at, and the only thing that ever emptied it. Notifications had nothing at all: one row per recipient per event, kept for the life of the installation, on what is easily the fastest-growing table here. Both now have a retention window, set together on the Scheduler screen under Housekeeping, and a nightly purge that honours it. Thirty days for failed jobs and ninety for read notifications, and zero means keep everything — the explicit choice somebody makes when a failure is evidence rather than debris. Unread notifications are never deleted, whatever their age. A notification nobody has looked at is the one row in that table still doing its job, and somebody back from four months away should find their news rather than a clean slate. The activity log is untouched by any of this: it is an audit trail, and it is never pruned. Two things came out of building it. The API request log purge has been running nightly since it shipped without ever appearing on the Scheduler screen — so a failure of it was invisible on the screen that exists to make failures visible — and there is now a test asserting the screen's list and the schedule are the same list, because they had already drifted once and would again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
143 lines
5.0 KiB
PHP
143 lines
5.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\User;
|
|
use App\Modules\Notifications\InAppNotification;
|
|
use App\Modules\Platform\Capabilities\Edition;
|
|
use App\Modules\Platform\Settings\Setting;
|
|
use App\Modules\Platform\Settings\Settings;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Inertia\Testing\AssertableInertia;
|
|
|
|
/**
|
|
* The two tables that grow on their own — failed queue jobs and read
|
|
* notifications — and the daily purges that now keep them from growing
|
|
* forever on an installation nobody tends.
|
|
*/
|
|
beforeEach(function () {
|
|
config()->set('projectsend.edition', Edition::Community);
|
|
$this->admin = User::factory()->create();
|
|
|
|
// Settings outlive the per-test rollback in the cache, so both
|
|
// windows are stated rather than assumed.
|
|
$settings = app(Settings::class);
|
|
$settings->set(Setting::FailedJobRetentionDays, 30);
|
|
$settings->set(Setting::NotificationRetentionDays, 90);
|
|
});
|
|
|
|
function insertFailedJob(string $failedAt): string
|
|
{
|
|
$uuid = (string) Str::uuid();
|
|
|
|
DB::table('failed_jobs')->insert([
|
|
'uuid' => $uuid,
|
|
'connection' => 'redis',
|
|
'queue' => 'default',
|
|
'payload' => '{}',
|
|
'exception' => 'Something went wrong.',
|
|
'failed_at' => $failedAt,
|
|
]);
|
|
|
|
return $uuid;
|
|
}
|
|
|
|
test('it deletes failed jobs past the window and keeps the recent ones', function () {
|
|
$old = insertFailedJob(now()->subDays(45)->toDateTimeString());
|
|
$recent = insertFailedJob(now()->subDays(3)->toDateTimeString());
|
|
|
|
$this->artisan('projectsend:purge-failed-jobs')->assertSuccessful();
|
|
|
|
expect(DB::table('failed_jobs')->where('uuid', $old)->exists())->toBeFalse()
|
|
->and(DB::table('failed_jobs')->where('uuid', $recent)->exists())->toBeTrue();
|
|
});
|
|
|
|
test('a retention of zero keeps every failed job, however old', function () {
|
|
app(Settings::class)->set(Setting::FailedJobRetentionDays, 0);
|
|
$old = insertFailedJob(now()->subYears(3)->toDateTimeString());
|
|
|
|
$this->artisan('projectsend:purge-failed-jobs')->assertSuccessful();
|
|
|
|
expect(DB::table('failed_jobs')->where('uuid', $old)->exists())->toBeTrue();
|
|
});
|
|
|
|
test('it deletes read notifications past the window', function () {
|
|
$old = InAppNotification::query()->create([
|
|
'user_id' => $this->admin->id,
|
|
'type' => 'file_shared',
|
|
'read_at' => now()->subDays(100),
|
|
'created_at' => now()->subDays(100),
|
|
]);
|
|
$recent = InAppNotification::query()->create([
|
|
'user_id' => $this->admin->id,
|
|
'type' => 'file_shared',
|
|
'read_at' => now()->subDay(),
|
|
'created_at' => now()->subDay(),
|
|
]);
|
|
|
|
$this->artisan('projectsend:purge-notifications')->assertSuccessful();
|
|
|
|
expect(InAppNotification::query()->whereKey($old->id)->exists())->toBeFalse()
|
|
->and(InAppNotification::query()->whereKey($recent->id)->exists())->toBeTrue();
|
|
});
|
|
|
|
test('an unread notification survives at any age', function () {
|
|
$ancient = InAppNotification::query()->create([
|
|
'user_id' => $this->admin->id,
|
|
'type' => 'file_shared',
|
|
'read_at' => null,
|
|
'created_at' => now()->subYears(2),
|
|
]);
|
|
|
|
$this->artisan('projectsend:purge-notifications')->assertSuccessful();
|
|
|
|
expect(InAppNotification::query()->whereKey($ancient->id)->exists())->toBeTrue();
|
|
});
|
|
|
|
test('a retention of zero keeps read notifications too', function () {
|
|
app(Settings::class)->set(Setting::NotificationRetentionDays, 0);
|
|
$old = InAppNotification::query()->create([
|
|
'user_id' => $this->admin->id,
|
|
'type' => 'file_shared',
|
|
'read_at' => now()->subYears(2),
|
|
'created_at' => now()->subYears(2),
|
|
]);
|
|
|
|
$this->artisan('projectsend:purge-notifications')->assertSuccessful();
|
|
|
|
expect(InAppNotification::query()->whereKey($old->id)->exists())->toBeTrue();
|
|
});
|
|
|
|
test('both windows are set from the scheduler screen', function () {
|
|
$this->actingAs($this->admin)->get('/system/settings/scheduler')->assertInertia(
|
|
fn (AssertableInertia $page) => $page
|
|
->where('retention.failed_jobs', 30)
|
|
->where('retention.notifications', 90),
|
|
);
|
|
|
|
$this->actingAs($this->admin)
|
|
->patch('/system/settings/scheduler/retention', ['failed_jobs' => 7, 'notifications' => 0])
|
|
->assertRedirect();
|
|
|
|
$settings = app(Settings::class);
|
|
expect($settings->get(Setting::FailedJobRetentionDays))->toBe(7)
|
|
->and($settings->get(Setting::NotificationRetentionDays))->toBe(0);
|
|
});
|
|
|
|
test('a negative window is refused rather than stored', function () {
|
|
$this->actingAs($this->admin)
|
|
->patch('/system/settings/scheduler/retention', ['failed_jobs' => -1, 'notifications' => 90])
|
|
->assertSessionHasErrors('failed_jobs');
|
|
|
|
expect(app(Settings::class)->get(Setting::FailedJobRetentionDays))->toBe(30);
|
|
});
|
|
|
|
test('the cloud edition has no scheduler screen to set them on', function () {
|
|
config()->set('projectsend.edition', Edition::Cloud);
|
|
|
|
$this->actingAs($this->admin)
|
|
->patch('/system/settings/scheduler/retention', ['failed_jobs' => 7, 'notifications' => 7])
|
|
->assertNotFound();
|
|
});
|