mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-16 16:45:07 +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.
175 lines
7.1 KiB
PHP
175 lines
7.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\User;
|
|
use App\Modules\Files\Models\File;
|
|
use App\Modules\Files\Models\FileAssignment;
|
|
use App\Modules\Files\Versions\FileVersions;
|
|
use App\Modules\Identity\Permissions\Permission;
|
|
use App\Modules\Identity\Permissions\SystemRole;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
beforeEach(function () {
|
|
Storage::fake('files');
|
|
$this->admin = User::factory()->create();
|
|
$this->versions = app(FileVersions::class);
|
|
$this->token = $this->admin->createToken('t', [
|
|
Permission::Upload->value,
|
|
Permission::EditFiles->value,
|
|
Permission::EditOthersFiles->value,
|
|
])->plainTextToken;
|
|
});
|
|
|
|
test('the resource reports both ends of a version link', function () {
|
|
$original = File::factory()->create(['uploaded_by' => $this->admin->id, 'name' => 'Rev C']);
|
|
$revision = File::factory()->create(['uploaded_by' => $this->admin->id, 'name' => 'Rev D']);
|
|
|
|
$this->versions->link($revision, $original, $this->admin);
|
|
|
|
$response = $this->withToken($this->token)->getJson("/api/v1/files/{$revision->id}");
|
|
|
|
$response->assertOk();
|
|
|
|
expect($response->json('data.is_revision'))->toBeTrue()
|
|
->and($response->json('data.sharing_root_id'))->toBe($original->id)
|
|
->and($response->json('data.previous_version.name'))->toBe('Rev C')
|
|
->and($response->json('data.next_version'))->toBeNull();
|
|
});
|
|
|
|
test('the resource reports an ordinary file as not a revision', function () {
|
|
$file = File::factory()->create(['uploaded_by' => $this->admin->id]);
|
|
|
|
$response = $this->withToken($this->token)->getJson("/api/v1/files/{$file->id}");
|
|
|
|
expect($response->json('data.is_revision'))->toBeFalse()
|
|
->and($response->json('data.sharing_root_id'))->toBeNull()
|
|
->and($response->json('data.previous_version'))->toBeNull();
|
|
});
|
|
|
|
test('the index carries the version fields too', function () {
|
|
$original = File::factory()->create(['uploaded_by' => $this->admin->id, 'name' => 'Rev C']);
|
|
$revision = File::factory()->create(['uploaded_by' => $this->admin->id, 'name' => 'Rev D']);
|
|
|
|
$this->versions->link($revision, $original, $this->admin);
|
|
|
|
$response = $this->withToken($this->token)->getJson('/api/v1/files');
|
|
|
|
$response->assertOk();
|
|
|
|
$row = collect($response->json('data'))->firstWhere('name', 'Rev D');
|
|
|
|
expect($row['previous_version']['name'])->toBe('Rev C');
|
|
});
|
|
|
|
test('a token can link and unlink a version', function () {
|
|
$original = File::factory()->create(['uploaded_by' => $this->admin->id]);
|
|
$revision = File::factory()->create(['uploaded_by' => $this->admin->id]);
|
|
|
|
$this->withToken($this->token)
|
|
->putJson("/api/v1/files/{$revision->id}/version", ['previous_file_id' => $original->id])
|
|
->assertOk();
|
|
|
|
expect($revision->fresh()->previous_file_id)->toBe($original->id);
|
|
|
|
$this->withToken($this->token)
|
|
->deleteJson("/api/v1/files/{$revision->id}/version")
|
|
->assertOk();
|
|
|
|
expect($revision->fresh()->previous_file_id)->toBeNull();
|
|
});
|
|
|
|
test('re-submitting an existing link is a no-op, not a 404', function () {
|
|
$original = File::factory()->create(['uploaded_by' => $this->admin->id]);
|
|
$revision = File::factory()->create(['uploaded_by' => $this->admin->id]);
|
|
|
|
$this->versions->link($revision, $original, $this->admin);
|
|
|
|
// An integration retrying a request that already succeeded must not be
|
|
// told the original does not exist. Caught by a live API smoke test,
|
|
// not by the suite: link() is idempotent, but the candidate lookup in
|
|
// front of it was excluding the file's own current original.
|
|
$this->withToken($this->token)
|
|
->putJson("/api/v1/files/{$revision->id}/version", ['previous_file_id' => $original->id])
|
|
->assertOk();
|
|
|
|
expect($revision->fresh()->previous_file_id)->toBe($original->id);
|
|
});
|
|
|
|
test('a token without edit permissions cannot link a version', function () {
|
|
$original = File::factory()->create(['uploaded_by' => $this->admin->id]);
|
|
$revision = File::factory()->create(['uploaded_by' => $this->admin->id]);
|
|
|
|
$readOnly = $this->admin->createToken('r', [Permission::Upload->value])->plainTextToken;
|
|
|
|
$this->withToken($readOnly)
|
|
->putJson("/api/v1/files/{$revision->id}/version", ['previous_file_id' => $original->id])
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('an out-of-scope original 404s rather than confirming it exists', function () {
|
|
$mine = User::factory()->client()->create();
|
|
$theirs = User::factory()->client()->create();
|
|
|
|
$staffer = User::factory()->role(SystemRole::ClientManager)->create();
|
|
$staffer->assignedClients()->attach($mine->id);
|
|
$token = $staffer->createToken('t', [Permission::EditFiles->value])->plainTextToken;
|
|
|
|
$subject = File::factory()->create(['uploaded_by' => $staffer->id]);
|
|
shareFileWith($subject, $mine);
|
|
|
|
$outOfScope = File::factory()->create(['uploaded_by' => $this->admin->id]);
|
|
shareFileWith($outOfScope, $theirs);
|
|
|
|
$this->withToken($token)
|
|
->putJson("/api/v1/files/{$subject->id}/version", ['previous_file_id' => $outOfScope->id])
|
|
->assertNotFound();
|
|
});
|
|
|
|
test('linking rejects an original that already has a revision', function () {
|
|
$original = File::factory()->create(['uploaded_by' => $this->admin->id]);
|
|
$taken = File::factory()->create(['uploaded_by' => $this->admin->id]);
|
|
$revision = File::factory()->create(['uploaded_by' => $this->admin->id]);
|
|
|
|
$this->versions->link($taken, $original, $this->admin);
|
|
|
|
// Excluded by the candidate rule, so it never reaches link().
|
|
$this->withToken($this->token)
|
|
->putJson("/api/v1/files/{$revision->id}/version", ['previous_file_id' => $original->id])
|
|
->assertNotFound();
|
|
});
|
|
|
|
test('assigning a client to a revision is refused and names the original', function () {
|
|
$client = User::factory()->client()->create();
|
|
|
|
$original = File::factory()->create(['uploaded_by' => $this->admin->id, 'name' => 'Rev C']);
|
|
$revision = File::factory()->create(['uploaded_by' => $this->admin->id]);
|
|
|
|
$this->versions->link($revision, $original, $this->admin);
|
|
|
|
$response = $this->withToken($this->token)
|
|
->postJson("/api/v1/files/{$revision->id}/assignments", ['type' => 'client', 'id' => $client->id]);
|
|
|
|
$response->assertStatus(422);
|
|
|
|
expect(json_encode($response->json()))->toContain('Rev C')
|
|
->and(FileAssignment::query()->where('file_id', $revision->id)->count())->toBe(0);
|
|
});
|
|
|
|
test('linking through the api moves recipients onto the original', function () {
|
|
$client = User::factory()->client()->create();
|
|
|
|
$original = File::factory()->create(['uploaded_by' => $this->admin->id]);
|
|
$revision = File::factory()->create(['uploaded_by' => $this->admin->id]);
|
|
shareFileWith($revision, $client);
|
|
|
|
$this->withToken($this->token)
|
|
->putJson("/api/v1/files/{$revision->id}/version", ['previous_file_id' => $original->id])
|
|
->assertOk();
|
|
|
|
expect(FileAssignment::query()->where('file_id', $revision->id)->count())->toBe(0)
|
|
->and(FileAssignment::query()->where('file_id', $original->id)->count())->toBe(1)
|
|
// …and the client still reaches the file, now by inheritance.
|
|
->and(File::query()->whereKey($revision->id)->visibleToClient($client)->exists())->toBeTrue();
|
|
});
|