Files
projectsend/tests/Feature/Files/DownloadSettingsTest.php
ignacionelson 073101d184 Put a ceiling on a zip download, and clean up after the ones that fail
Follow-up to #1687, which made a zip build report failure honestly. Four
things it passed near, none of them regressions it introduced.

A zip has never had a size limit — only a cap of 10,000 files, which
bounds nothing that costs anything. Ten thousand spreadsheets zip in
seconds; two hundred videos is an hour of stream-copying and an archive
that fills the disk. Bytes are what a build actually costs, so the new
Settings → Downloads screen caps the total size instead, at 2 GB out of
the box. It is a setting rather than a constant because the safe figure
depends on free disk, on whether sources live on a remote disk, and on
the plan a hosted tenant is on — the file count stays fixed, since it is
a foot-gun rail and not a knob anybody needs. The controller measures
the selection at request time and names both numbers when it refuses;
the job measures again, because it re-derives the selection at run time
and a folder can grow while the job waits in the queue.

Every shipped topology runs exactly one queue worker, and everything
shares the default queue, so raising the job timeout to an hour handed
any signed-in person an hour of everyone else's notification mail. There
is now one build in progress per requester and a named throttle bucket
on the endpoint, which had neither. A pending row older than an hour is
treated as abandoned rather than in progress, so a worker killed hard
enough to skip failed() cannot lock somebody out for good. Giving zip
builds their own queue is the structural fix and wants its own change:
it touches compose, supervisord and the systemd unit in INSTALL.md, and
an install that upgrades without changing its worker command would stop
building zips silently.

zip_downloads.requested_by cascades on delete, so removing a user takes
their rows with it and strands every archive they built — invisible to a
purge that walks rows, and to OrphanFileScanner, which skips zips/ on
purpose. The purge now also sweeps files in zips/ that no row explains,
after a day's grace so a build in progress is never taken out from under
itself.

Two smaller things while in here. A build that failed because every file
had already hit its download limit said only that nothing was available,
and dropped the skipped list — the same distinction the store guard goes
out of its way to draw at request time. And a failed close() now logs
libzip's reason, which the @ silencing had been discarding: "the disk is
full" and "the source vanished" are different problems for whoever has
to fix one, while the requester still sees a message with no server
paths in it.
2026-08-25 21:44:27 -03:00

59 lines
2.1 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\User;
use App\Modules\Audit\Action;
use App\Modules\Audit\ActivityLog;
use App\Modules\Identity\Models\Role;
use App\Modules\Platform\Settings\Setting;
use App\Modules\Platform\Settings\Settings;
use Inertia\Testing\AssertableInertia;
beforeEach(function () {
$this->admin = User::factory()->create();
});
test('staff can view and update download settings', function () {
// Settings are cached across tests (Cache::rememberForever survives
// the per-test DB rollback), so the starting value is set rather than
// assumed.
app(Settings::class)->set(Setting::MaxZipDownloadSizeMb, 2048);
$this->actingAs($this->admin)->get('/system/settings/downloads')->assertInertia(
fn (AssertableInertia $page) => $page
->component('system/settings/downloads')
->where('max_zip_download_size_mb', 2048),
);
$this->actingAs($this->admin)
->patch('/system/settings/downloads', ['max_zip_download_size_mb' => 512])
->assertRedirect();
expect(app(Settings::class)->get(Setting::MaxZipDownloadSizeMb))->toBe(512);
expect(ActivityLog::query()->where('action', Action::SettingsUpdated)->where('context->section', 'downloads')->exists())
->toBeTrue();
});
test('a negative zip download limit is rejected', function () {
$this->actingAs($this->admin)
->patch('/system/settings/downloads', ['max_zip_download_size_mb' => -1])
->assertSessionHasErrors('max_zip_download_size_mb');
});
test('clients cannot access download settings', function () {
$this->admin; // setup complete
$this->actingAs(User::factory()->client()->create())
->get('/system/settings/downloads')
->assertRedirect(route('dashboard'));
});
test('staff without edit_settings cannot access download settings', function () {
$role = Role::query()->create(['name' => 'No Settings', 'is_administrator' => false, 'is_system' => false]);
$staffer = User::factory()->create(['role_id' => $role->id]);
$this->actingAs($staffer)->get('/system/settings/downloads')->assertForbidden();
});