Files
projectsend/tests/Feature/Files/ClientFilesTest.php
ignacionelson 6e47d76ba6 ProjectSend 2.0.0
Client file sharing, rebuilt from the ground up: a private area per
client, resumable uploads, folders, groups and categories, sharing with
expiry dates and download limits, comments, file versions, an activity
log, a REST API, and sixteen languages.

This repository begins here. ProjectSend 2 was developed privately, and
that development history is not published — the previous generation
remains available, with its own history, at projectsend/legacy.

Free software under the GNU General Public License v2, or (at your
option) any later version.
2026-08-14 01:38:12 -03:00

124 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\User;
use App\Modules\Files\Folders\FolderService;
use App\Modules\Files\Models\File;
use App\Modules\Groups\Models\Group;
use App\Modules\Identity\Models\Role;
use App\Modules\Identity\Models\RolePermission;
use App\Modules\Identity\Permissions\SystemRole;
use Illuminate\Support\Facades\Storage;
use Illuminate\Testing\TestResponse;
use Inertia\Testing\AssertableInertia;
beforeEach(function () {
Storage::fake('files');
$this->admin = User::factory()->create();
});
/**
* @return array<string, mixed>
*/
function clientFilesProps(TestResponse $response): array
{
$page = json_decode(json_encode($response->viewData('page')), true);
return $page['props'];
}
test('the page lists everything the client can reach: own uploads, direct, group, and folder assignments', function () {
$client = User::factory()->client()->create();
// Uploaded by the client themself (bypassing the real upload endpoint,
// same fixture convention FilesTest.php uses).
$mine = File::factory()->create([
'name' => 'mine', 'original_name' => 'mine.pdf', 'path' => 'test/mine.pdf',
'mime_type' => 'application/pdf', 'size' => 10, 'checksum' => str_repeat('0', 64),
'uploaded_by' => $client->id, 'public' => false,
]);
$direct = uploadNamedFile($this->admin, 'direct');
test()->actingAs($this->admin)->post("/files/{$direct->id}/assignments", ['type' => 'client', 'id' => $client->id]);
$group = Group::query()->create(['name' => 'Recipients', 'public' => false]);
$group->members()->attach($client->id);
$viaGroup = uploadNamedFile($this->admin, 'via-group');
test()->actingAs($this->admin)->post("/files/{$viaGroup->id}/assignments", ['type' => 'group', 'id' => $group->id]);
$folder = app(FolderService::class)->create('Shared Folder', null);
test()->actingAs($this->admin)->post("/folders/{$folder->id}/assignments", ['type' => 'client', 'id' => $client->id]);
$viaFolder = uploadNamedFile($this->admin, 'via-folder', $folder->id);
$unrelated = uploadNamedFile($this->admin, 'unrelated');
$response = test()->actingAs($this->admin)->get("/clients/{$client->id}/files");
$response->assertOk()->assertInertia(
fn (AssertableInertia $page) => $page
->component('clients/files')
->where('client.id', $client->id)
->has('files', 4)
);
$files = collect(clientFilesProps($response)['files']);
$ids = $files->pluck('id');
expect($ids)->toContain($mine->id, $direct->id, $viaGroup->id, $viaFolder->id)
->not->toContain($unrelated->id);
expect($files->firstWhere('id', $mine->id)['uploaded_by_client'])->toBeTrue();
expect($files->firstWhere('id', $direct->id)['uploaded_by_client'])->toBeFalse();
});
test('staff without edit_clients cannot reach the page', function () {
$client = User::factory()->client()->create();
$uploader = User::factory()->role(SystemRole::Uploader)->create();
test()->actingAs($uploader)->get("/clients/{$client->id}/files")->assertForbidden();
});
test('a non-client id 404s', function () {
$staffer = User::factory()->role(SystemRole::Uploader)->create();
test()->actingAs($this->admin)->get("/clients/{$staffer->id}/files")->assertNotFound();
});
test('a client-scoped staff member may only view files for their assigned clients', function () {
// None of the seeded system roles combine client_scoped with
// edit_clients (Client Manager is file-focused, not client-management),
// so a bespoke role stands in for "a scoped staffer who may reach this
// page at all".
$role = Role::query()->create(['name' => 'Scoped Client Editor', 'is_administrator' => false, 'is_system' => false, 'client_scoped' => true]);
RolePermission::query()->create(['role_id' => $role->id, 'permission' => 'edit_clients']);
$manager = User::factory()->create(['role_id' => $role->id]);
$assigned = User::factory()->client()->create();
$unassigned = User::factory()->client()->create();
$manager->assignedClients()->sync([$assigned->id]);
test()->actingAs($manager)->get("/clients/{$assigned->id}/files")->assertOk();
test()->actingAs($manager)->get("/clients/{$unassigned->id}/files")->assertNotFound();
});
test('download is only offered when the viewer has file permissions', function () {
$client = User::factory()->client()->create();
$direct = uploadNamedFile($this->admin, 'direct');
test()->actingAs($this->admin)->post("/files/{$direct->id}/assignments", ['type' => 'client', 'id' => $client->id]);
// A role that can manage clients but holds no file permissions at all.
$role = Role::query()->create(['name' => 'Files-Blind Manager', 'is_administrator' => false, 'is_system' => false, 'client_scoped' => false]);
RolePermission::query()->create(['role_id' => $role->id, 'permission' => 'edit_clients']);
$blindManager = User::factory()->create(['role_id' => $role->id]);
$response = test()->actingAs($blindManager)->get("/clients/{$client->id}/files");
$response->assertOk();
$row = collect(clientFilesProps($response)['files'])->firstWhere('id', $direct->id);
expect($row['can_download'])->toBeFalse();
$adminResponse = test()->actingAs($this->admin)->get("/clients/{$client->id}/files");
$adminRow = collect(clientFilesProps($adminResponse)['files'])->firstWhere('id', $direct->id);
expect($adminRow['can_download'])->toBeTrue();
});