mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-17 00:55:07 +00:00
cca3d9c314
The grouped filter had a second member, "All previews", built on a public-preview action that does not exist: previewing is recorded one way today, so its own option already answers "who previewed this?" in full. Static analysis caught the reference; the group would have been unreachable even if it had compiled, since a group with a single present member is deliberately not offered. Previews get a group here the day a second way to preview a file is recorded separately, and the test now pins the single-member case on a file whose log holds one flavour of download.
280 lines
11 KiB
PHP
280 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\User;
|
|
use App\Modules\Audit\Action;
|
|
use App\Modules\Audit\ActivityLog;
|
|
use App\Modules\Files\Models\Folder;
|
|
use App\Modules\Identity\Models\Role;
|
|
use App\Modules\Identity\Models\RolePermission;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Inertia\Testing\AssertableInertia;
|
|
|
|
beforeEach(function () {
|
|
Storage::fake('files');
|
|
$this->admin = User::factory()->create();
|
|
});
|
|
|
|
test('the details panel activity tab is capped at 20 entries and reports the true total', function () {
|
|
$file = uploadImageFile($this->admin);
|
|
|
|
// The upload itself already logged one entry; pad past the cap.
|
|
for ($i = 0; $i < 24; $i++) {
|
|
ActivityLog::create([
|
|
'action' => Action::FileUpdated,
|
|
'subject_type' => $file->getMorphClass(),
|
|
'subject_id' => $file->id,
|
|
'subject_name' => $file->name,
|
|
'actor_id' => $this->admin->id,
|
|
'actor_name' => $this->admin->name,
|
|
'actor_type' => 'staff',
|
|
'created_at' => now(),
|
|
]);
|
|
}
|
|
|
|
$response = $this->actingAs($this->admin)->getJson("/files/{$file->id}/activity");
|
|
|
|
$response->assertOk();
|
|
expect($response->json('entries'))->toHaveCount(20)
|
|
->and($response->json('total'))->toBe(25);
|
|
});
|
|
|
|
test('the full activity history page paginates every entry for a file', function () {
|
|
$file = uploadImageFile($this->admin);
|
|
|
|
for ($i = 0; $i < 29; $i++) {
|
|
ActivityLog::create([
|
|
'action' => Action::FileUpdated,
|
|
'subject_type' => $file->getMorphClass(),
|
|
'subject_id' => $file->id,
|
|
'subject_name' => $file->name,
|
|
'actor_id' => $this->admin->id,
|
|
'actor_name' => $this->admin->name,
|
|
'actor_type' => 'staff',
|
|
'created_at' => now(),
|
|
]);
|
|
}
|
|
|
|
$this->actingAs($this->admin)->get("/files/{$file->id}/activity/history")->assertInertia(
|
|
fn (AssertableInertia $page) => $page
|
|
->component('activity/subject')
|
|
->has('entries', 25)
|
|
->where('pagination.total', 30)
|
|
->where('pagination.last_page', 2)
|
|
->where('subject_name', $file->name),
|
|
);
|
|
|
|
$this->actingAs($this->admin)->get("/files/{$file->id}/activity/history?page=2")->assertInertia(
|
|
fn (AssertableInertia $page) => $page->has('entries', 5),
|
|
);
|
|
});
|
|
|
|
test('the full activity history page paginates every entry for a folder', function () {
|
|
$this->actingAs($this->admin)->post('/folders', ['name' => 'History Folder'])->assertRedirect();
|
|
$folder = Folder::query()->where('name', 'History Folder')->sole();
|
|
|
|
for ($i = 0; $i < 5; $i++) {
|
|
ActivityLog::create([
|
|
'action' => Action::FolderRenamed,
|
|
'subject_type' => $folder->getMorphClass(),
|
|
'subject_id' => $folder->id,
|
|
'subject_name' => $folder->name,
|
|
'actor_id' => $this->admin->id,
|
|
'actor_name' => $this->admin->name,
|
|
'actor_type' => 'staff',
|
|
'created_at' => now(),
|
|
]);
|
|
}
|
|
|
|
// Folder creation itself logs one entry, so 5 padding entries plus that one.
|
|
$this->actingAs($this->admin)->get("/folders/{$folder->id}/activity/history")->assertInertia(
|
|
fn (AssertableInertia $page) => $page
|
|
->component('activity/subject')
|
|
->has('entries', 6)
|
|
->where('subject_name', $folder->name),
|
|
);
|
|
});
|
|
|
|
test('a staff member without view_actions_log cannot open a file activity history page', function () {
|
|
$file = uploadImageFile($this->admin);
|
|
|
|
$role = Role::query()->create(['name' => 'No Activity Log', 'is_administrator' => false, 'is_system' => false]);
|
|
RolePermission::query()->insert([
|
|
['role_id' => $role->id, 'permission' => 'upload'],
|
|
]);
|
|
$restricted = User::factory()->create(['role_id' => $role->id]);
|
|
|
|
$this->actingAs($restricted)->get("/files/{$file->id}/activity/history")->assertForbidden();
|
|
});
|
|
|
|
test('the full download history page paginates every download and labels public downloads', function () {
|
|
$file = uploadImageFile($this->admin);
|
|
|
|
for ($i = 0; $i < 24; $i++) {
|
|
ActivityLog::create([
|
|
'action' => Action::FileDownloaded,
|
|
'subject_type' => $file->getMorphClass(),
|
|
'subject_id' => $file->id,
|
|
'subject_name' => $file->name,
|
|
'actor_id' => $this->admin->id,
|
|
'actor_name' => $this->admin->name,
|
|
'actor_type' => 'staff',
|
|
'ip_address' => '10.0.0.1',
|
|
'created_at' => now(),
|
|
]);
|
|
}
|
|
ActivityLog::create([
|
|
'action' => Action::ShareLinkDownloaded,
|
|
'subject_type' => $file->getMorphClass(),
|
|
'subject_id' => $file->id,
|
|
'subject_name' => $file->name,
|
|
'ip_address' => '10.0.0.2',
|
|
'created_at' => now(),
|
|
]);
|
|
ActivityLog::create([
|
|
'action' => Action::PublicFileDownloaded,
|
|
'subject_type' => $file->getMorphClass(),
|
|
'subject_id' => $file->id,
|
|
'subject_name' => $file->name,
|
|
'ip_address' => '10.0.0.3',
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
$this->actingAs($this->admin)->get("/files/{$file->id}/downloads/history")->assertInertia(
|
|
fn (AssertableInertia $page) => $page
|
|
->component('activity/downloads')
|
|
->has('entries', 25)
|
|
->where('pagination.total', 26)
|
|
->where('pagination.last_page', 2)
|
|
->where('subject_name', $file->name)
|
|
// Same-second timestamps tie-break on id desc, and the two
|
|
// public-download rows were inserted last, so they lead.
|
|
->where('entries.0.actor_name', __('Public listing'))
|
|
->where('entries.1.actor_name', __('Public link')),
|
|
);
|
|
|
|
$this->actingAs($this->admin)->get("/files/{$file->id}/downloads/history?page=2")->assertInertia(
|
|
fn (AssertableInertia $page) => $page->has('entries', 1),
|
|
);
|
|
});
|
|
|
|
test('a staff member without view_actions_log cannot open a file download history page', function () {
|
|
$file = uploadImageFile($this->admin);
|
|
|
|
$role = Role::query()->create(['name' => 'No Activity Log 2', 'is_administrator' => false, 'is_system' => false]);
|
|
RolePermission::query()->insert([
|
|
['role_id' => $role->id, 'permission' => 'upload'],
|
|
]);
|
|
$restricted = User::factory()->create(['role_id' => $role->id]);
|
|
|
|
$this->actingAs($restricted)->get("/files/{$file->id}/downloads/history")->assertForbidden();
|
|
});
|
|
|
|
test('the details panel downloads summary is bounded but reports the true total', function () {
|
|
$file = uploadImageFile($this->admin);
|
|
|
|
for ($i = 0; $i < 502; $i++) {
|
|
ActivityLog::create([
|
|
'action' => Action::FileDownloaded,
|
|
'subject_type' => $file->getMorphClass(),
|
|
'subject_id' => $file->id,
|
|
'subject_name' => $file->name,
|
|
'actor_id' => $this->admin->id,
|
|
'actor_name' => $this->admin->name,
|
|
'actor_type' => 'staff',
|
|
'created_at' => now(),
|
|
]);
|
|
}
|
|
|
|
$response = $this->actingAs($this->admin)->getJson("/files/{$file->id}/downloads");
|
|
|
|
$response->assertOk();
|
|
// The grouped summary caps the raw rows it considers, so a single
|
|
// heavy downloader's group count saturates at that cap — but the
|
|
// top-level total (and the full download history page) stay accurate.
|
|
expect($response->json('total'))->toBe(502)
|
|
->and($response->json('downloaders'))->toHaveCount(1)
|
|
->and($response->json('downloaders.0.count'))->toBe(500);
|
|
});
|
|
|
|
test('the file activity history filters by action, and offers only the actions that happened', function () {
|
|
$file = uploadImageFile($this->admin);
|
|
$client = User::factory()->client()->create(['name' => 'Downloading Client']);
|
|
|
|
$log = function (Action $action, ?User $actor = null) use ($file): void {
|
|
ActivityLog::create([
|
|
'action' => $action,
|
|
'subject_type' => $file->getMorphClass(),
|
|
'subject_id' => $file->id,
|
|
'subject_name' => $file->name,
|
|
'actor_id' => $actor?->id,
|
|
'actor_name' => $actor?->name,
|
|
'actor_type' => $actor === null ? null : 'client',
|
|
'created_at' => now(),
|
|
]);
|
|
};
|
|
|
|
$log(Action::FileDownloaded, $client);
|
|
$log(Action::FileDownloaded, $client);
|
|
$log(Action::ShareLinkDownloaded);
|
|
$log(Action::FilePreviewed, $client);
|
|
|
|
$options = $this->actingAs($this->admin)->get("/files/{$file->id}/activity/history")
|
|
->assertInertia(fn (AssertableInertia $page) => $page->component('activity/subject'))
|
|
->viewData('page')['props']['action_options'];
|
|
|
|
$byKey = collect($options)->keyBy('key');
|
|
|
|
// The upload happened; the eighty-odd actions that cannot apply to a
|
|
// file are not offered at all.
|
|
expect($byKey->keys()->all())->toEqualCanonicalizing(['downloads', 'file.uploaded', 'file.downloaded', 'file.previewed', 'share_link.downloaded'])
|
|
->and($byKey['downloads']['count'])->toBe(3)
|
|
->and($byKey['file.downloaded']['count'])->toBe(2);
|
|
|
|
// A file whose log holds only one flavour of download gets no group:
|
|
// it would filter to exactly what its single member already offers.
|
|
$simple = uploadImageFile($this->admin, 'simple.jpg');
|
|
ActivityLog::create([
|
|
'action' => Action::FileDownloaded,
|
|
'subject_type' => $simple->getMorphClass(),
|
|
'subject_id' => $simple->id,
|
|
'subject_name' => $simple->name,
|
|
'actor_id' => $this->admin->id,
|
|
'actor_name' => $this->admin->name,
|
|
'actor_type' => 'staff',
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
$simpleOptions = $this->actingAs($this->admin)->get("/files/{$simple->id}/activity/history")
|
|
->viewData('page')['props']['action_options'];
|
|
|
|
expect(collect($simpleOptions)->pluck('key')->all())->toEqualCanonicalizing(['file.uploaded', 'file.downloaded']);
|
|
|
|
// The group covers all three download flavours, one of them anonymous.
|
|
$this->actingAs($this->admin)->get("/files/{$file->id}/activity/history?action=downloads")->assertInertia(
|
|
fn (AssertableInertia $page) => $page->has('entries', 3)->where('filters.action', 'downloads'),
|
|
);
|
|
|
|
$this->actingAs($this->admin)->get("/files/{$file->id}/activity/history?action=file.previewed")->assertInertia(
|
|
fn (AssertableInertia $page) => $page->has('entries', 1)->where('entries.0.actor_name', 'Downloading Client'),
|
|
);
|
|
|
|
// Narrowing by who acted, and by day, works the same as the main log.
|
|
$this->actingAs($this->admin)->get("/files/{$file->id}/activity/history?actor=Downloading")->assertInertia(
|
|
fn (AssertableInertia $page) => $page->has('entries', 3),
|
|
);
|
|
|
|
$this->actingAs($this->admin)->get("/files/{$file->id}/activity/history?from=".now()->addDay()->toDateString())->assertInertia(
|
|
fn (AssertableInertia $page) => $page->has('entries', 0),
|
|
);
|
|
|
|
// An action that exists but never touched this file filters to nothing
|
|
// rather than erroring; a value that is neither action nor group is
|
|
// rejected outright.
|
|
$this->actingAs($this->admin)->get("/files/{$file->id}/activity/history?action=user.created")->assertInertia(
|
|
fn (AssertableInertia $page) => $page->has('entries', 0),
|
|
);
|
|
$this->actingAs($this->admin)->get("/files/{$file->id}/activity/history?action=nonsense")->assertSessionHasErrors('action');
|
|
});
|