mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-17 17:15:08 +00:00
6e47d76ba6
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.
248 lines
9.6 KiB
PHP
248 lines
9.6 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\Category;
|
|
use App\Modules\Files\Models\Folder;
|
|
use App\Modules\Identity\Models\Role;
|
|
use App\Modules\Identity\Models\RolePermission;
|
|
use App\Modules\Platform\Settings\Setting;
|
|
use App\Modules\Platform\Settings\Settings;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
beforeEach(function () {
|
|
Storage::fake('files');
|
|
$this->admin = User::factory()->create();
|
|
app(Settings::class)->set(Setting::Theme, 'default');
|
|
});
|
|
|
|
/**
|
|
* A staff role limited to editing only files it owns — no
|
|
* set_file_expiration_date/set_file_categories either, mirroring the
|
|
* "File Editor Only" fixture FilesTest.php already uses for the equivalent
|
|
* single-edit permission tests.
|
|
*/
|
|
function ownFilesOnlyStaff(string $roleName = 'Own Files Only'): User
|
|
{
|
|
$role = Role::query()->create(['name' => $roleName, 'is_administrator' => false, 'is_system' => false]);
|
|
RolePermission::query()->insert([
|
|
['role_id' => $role->id, 'permission' => 'upload'],
|
|
['role_id' => $role->id, 'permission' => 'edit_files'],
|
|
]);
|
|
|
|
return User::factory()->create(['role_id' => $role->id]);
|
|
}
|
|
|
|
function noEditPayload(array $overrides = []): array
|
|
{
|
|
return array_merge([
|
|
'folder_action' => 'no_change',
|
|
'description_action' => 'no_change',
|
|
'expiration_action' => 'no_change',
|
|
'add_category_ids' => [],
|
|
'remove_category_ids' => [],
|
|
], $overrides);
|
|
}
|
|
|
|
test('a bulk edit only applies the fields that were actually touched', function () {
|
|
$file = uploadDocumentFile($this->admin);
|
|
$folder = Folder::query()->create(['name' => 'Reports']);
|
|
$category = Category::query()->create(['name' => 'Invoices']);
|
|
$file->update(['folder_id' => $folder->id, 'expires_at' => '2030-01-01']);
|
|
$file->categories()->attach($category->id);
|
|
|
|
$this->actingAs($this->admin)->patch('/files/bulk-edit', noEditPayload([
|
|
'file_ids' => [$file->id],
|
|
'description_action' => 'set',
|
|
'description' => 'Updated description',
|
|
]))->assertRedirect();
|
|
|
|
$file->refresh();
|
|
expect($file->description)->toBe('Updated description')
|
|
->and($file->folder_id)->toBe($folder->id)
|
|
->and($file->expires_at?->toDateString())->toBe('2030-01-01')
|
|
->and($file->categories->pluck('id')->all())->toBe([$category->id]);
|
|
});
|
|
|
|
test('adding categories in bulk does not remove categories not selected for removal', function () {
|
|
$file = uploadDocumentFile($this->admin);
|
|
$existing = Category::query()->create(['name' => 'Existing']);
|
|
$new = Category::query()->create(['name' => 'New']);
|
|
$file->categories()->attach($existing->id);
|
|
|
|
$this->actingAs($this->admin)->patch('/files/bulk-edit', noEditPayload([
|
|
'file_ids' => [$file->id],
|
|
'add_category_ids' => [$new->id],
|
|
]))->assertRedirect();
|
|
|
|
expect($file->refresh()->categories->pluck('id')->sort()->values()->all())
|
|
->toBe(collect([$existing->id, $new->id])->sort()->values()->all());
|
|
});
|
|
|
|
test('removing categories in bulk does not touch categories not selected for removal', function () {
|
|
$file = uploadDocumentFile($this->admin);
|
|
$keep = Category::query()->create(['name' => 'Keep']);
|
|
$drop = Category::query()->create(['name' => 'Drop']);
|
|
$file->categories()->attach([$keep->id, $drop->id]);
|
|
|
|
$this->actingAs($this->admin)->patch('/files/bulk-edit', noEditPayload([
|
|
'file_ids' => [$file->id],
|
|
'remove_category_ids' => [$drop->id],
|
|
]))->assertRedirect();
|
|
|
|
expect($file->refresh()->categories->pluck('id')->all())->toBe([$keep->id]);
|
|
});
|
|
|
|
test('bulk expiration set/clear/no-change behave independently per action', function () {
|
|
$withExpiry = uploadDocumentFile($this->admin, 'a.pdf');
|
|
$withExpiry->update(['expires_at' => '2030-06-01']);
|
|
$noExpiry = uploadDocumentFile($this->admin, 'b.pdf');
|
|
|
|
// "no_change" leaves differing existing values exactly as they were.
|
|
$this->actingAs($this->admin)->patch('/files/bulk-edit', noEditPayload([
|
|
'file_ids' => [$withExpiry->id, $noExpiry->id],
|
|
'description_action' => 'set',
|
|
'description' => 'touch something unrelated',
|
|
]))->assertRedirect();
|
|
|
|
expect($withExpiry->refresh()->expires_at?->toDateString())->toBe('2030-06-01')
|
|
->and($noExpiry->refresh()->expires_at)->toBeNull();
|
|
|
|
// "set" applies the same date to both, regardless of prior state.
|
|
$this->actingAs($this->admin)->patch('/files/bulk-edit', noEditPayload([
|
|
'file_ids' => [$withExpiry->id, $noExpiry->id],
|
|
'expiration_action' => 'set',
|
|
'expires_at' => '2031-01-01',
|
|
]))->assertRedirect();
|
|
|
|
expect($withExpiry->refresh()->expires_at?->toDateString())->toBe('2031-01-01')
|
|
->and($noExpiry->refresh()->expires_at?->toDateString())->toBe('2031-01-01');
|
|
|
|
// "clear" nulls both.
|
|
$this->actingAs($this->admin)->patch('/files/bulk-edit', noEditPayload([
|
|
'file_ids' => [$withExpiry->id, $noExpiry->id],
|
|
'expiration_action' => 'clear',
|
|
]))->assertRedirect();
|
|
|
|
expect($withExpiry->refresh()->expires_at)->toBeNull()
|
|
->and($noExpiry->refresh()->expires_at)->toBeNull();
|
|
});
|
|
|
|
test('bulk folder move reparents every selected file, including to root', function () {
|
|
$folder = Folder::query()->create(['name' => 'Reports']);
|
|
$fileA = uploadDocumentFile($this->admin, 'a.pdf');
|
|
$fileB = uploadDocumentFile($this->admin, 'b.pdf');
|
|
|
|
$this->actingAs($this->admin)->patch('/files/bulk-edit', noEditPayload([
|
|
'file_ids' => [$fileA->id, $fileB->id],
|
|
'folder_action' => 'move',
|
|
'folder_id' => $folder->id,
|
|
]))->assertRedirect();
|
|
|
|
expect($fileA->refresh()->folder_id)->toBe($folder->id)
|
|
->and($fileB->refresh()->folder_id)->toBe($folder->id);
|
|
|
|
$this->actingAs($this->admin)->patch('/files/bulk-edit', noEditPayload([
|
|
'file_ids' => [$fileA->id, $fileB->id],
|
|
'folder_action' => 'move',
|
|
'folder_id' => null,
|
|
]))->assertRedirect();
|
|
|
|
expect($fileA->refresh()->folder_id)->toBeNull()
|
|
->and($fileB->refresh()->folder_id)->toBeNull();
|
|
});
|
|
|
|
test('a file the acting staff member cannot edit is silently skipped, not 403d', function () {
|
|
$staff = ownFilesOnlyStaff();
|
|
expect($staff->can('edit_others_files'))->toBeFalse();
|
|
|
|
$ownFile = uploadDocumentFile($staff, 'own.pdf');
|
|
$othersFile = uploadDocumentFile($this->admin, 'others.pdf');
|
|
|
|
$response = $this->actingAs($staff)->patch('/files/bulk-edit', noEditPayload([
|
|
'file_ids' => [$ownFile->id, $othersFile->id],
|
|
'description_action' => 'set',
|
|
'description' => 'edited',
|
|
]));
|
|
|
|
$response->assertRedirect();
|
|
expect(session('success'))->toContain('1 of 2 selected files were updated');
|
|
|
|
expect($ownFile->refresh()->description)->toBe('edited')
|
|
->and($othersFile->refresh()->description)->not->toBe('edited');
|
|
});
|
|
|
|
test('a bulk edit where nothing is authorized returns 422, not a misleading success', function () {
|
|
$role = Role::query()->create(['name' => 'No Edit', 'is_administrator' => false, 'is_system' => false]);
|
|
RolePermission::query()->insert([
|
|
['role_id' => $role->id, 'permission' => 'upload'],
|
|
]);
|
|
$staff = User::factory()->create(['role_id' => $role->id]);
|
|
$file = uploadDocumentFile($this->admin);
|
|
|
|
$this->actingAs($staff)->patch('/files/bulk-edit', noEditPayload([
|
|
'file_ids' => [$file->id],
|
|
'description_action' => 'set',
|
|
'description' => 'nope',
|
|
]))->assertStatus(422);
|
|
});
|
|
|
|
test('expiration and category changes in a bulk edit are silently ignored without the matching permission', function () {
|
|
$staff = ownFilesOnlyStaff('File Editor Only, No Expiry Or Categories');
|
|
expect($staff->can('set_file_expiration_date'))->toBeFalse()
|
|
->and($staff->can('set_file_categories'))->toBeFalse();
|
|
|
|
$file = uploadDocumentFile($staff);
|
|
$category = Category::query()->create(['name' => 'Should Not Apply']);
|
|
|
|
$this->actingAs($staff)->patch('/files/bulk-edit', noEditPayload([
|
|
'file_ids' => [$file->id],
|
|
'expiration_action' => 'set',
|
|
'expires_at' => '2030-01-01',
|
|
'add_category_ids' => [$category->id],
|
|
]))->assertRedirect();
|
|
|
|
expect($file->refresh()->expires_at)->toBeNull()
|
|
->and($file->categories()->count())->toBe(0);
|
|
});
|
|
|
|
test('a bulk edit that changes nothing is rejected', function () {
|
|
$file = uploadDocumentFile($this->admin);
|
|
|
|
$this->actingAs($this->admin)->patch('/files/bulk-edit', noEditPayload([
|
|
'file_ids' => [$file->id],
|
|
]))->assertStatus(422);
|
|
});
|
|
|
|
test('each successfully updated file gets exactly one FileUpdated activity log entry', function () {
|
|
$staff = ownFilesOnlyStaff('Own Files Only 2');
|
|
|
|
$ownFile = uploadDocumentFile($staff, 'own.pdf');
|
|
$othersFile = uploadDocumentFile($this->admin, 'others.pdf');
|
|
|
|
$before = ActivityLog::query()->where('action', Action::FileUpdated)->count();
|
|
|
|
$this->actingAs($staff)->patch('/files/bulk-edit', noEditPayload([
|
|
'file_ids' => [$ownFile->id, $othersFile->id],
|
|
'description_action' => 'set',
|
|
'description' => 'edited',
|
|
]))->assertRedirect();
|
|
|
|
expect(ActivityLog::query()->where('action', Action::FileUpdated)->count())->toBe($before + 1);
|
|
});
|
|
|
|
test('the bulk-edit route is not swallowed by the single-file {file} route', function () {
|
|
$file = uploadDocumentFile($this->admin);
|
|
|
|
$this->actingAs($this->admin)->patch('/files/bulk-edit', noEditPayload([
|
|
'file_ids' => [$file->id],
|
|
'description_action' => 'set',
|
|
'description' => 'route sanity',
|
|
]))->assertRedirect();
|
|
|
|
expect($file->refresh()->description)->toBe('route sanity');
|
|
});
|