The Laravel 13 upgrade pulled Pint 1.27.1 to 1.29.3, which changed the laravel preset's default rules. composer lint and the CI lint job fail without this, so it is required rather than cosmetic. Formatting only, applied by `vendor/bin/pint`. The rules that fired were fully_qualified_strict_types, ordered_imports, single_blank_line_at_eof, unary_operator_spaces, not_operator_with_successor_space, braces_position, single_line_empty_body, single_line_after_imports and no_extra_blank_lines. Kept separate from the upgrade commit to keep that diff reviewable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
190 lines
5.9 KiB
PHP
190 lines
5.9 KiB
PHP
<?php
|
|
|
|
use App\Models\Setting;
|
|
use App\Models\Share;
|
|
use App\Models\ShareFile;
|
|
use App\Services\ShareService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Tests\TestCase;
|
|
|
|
pest()->extend(TestCase::class)
|
|
->use(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
Storage::fake('shares');
|
|
$this->service = app(ShareService::class);
|
|
});
|
|
|
|
test('create share without password stores encryption key', function () {
|
|
$file = UploadedFile::fake()->create('document.pdf', 1024);
|
|
|
|
$share = $this->service->createShare([
|
|
['file' => $file, 'relativePath' => null],
|
|
]);
|
|
|
|
expect($share)->toBeInstanceOf(Share::class);
|
|
expect($share->token)->toHaveLength(16);
|
|
expect($share->password)->toBeNull();
|
|
expect($share->encryption_key)->not->toBeNull();
|
|
expect($share->encryption_salt)->not->toBeNull();
|
|
expect($share->files)->toHaveCount(1);
|
|
expect($share->files->first()->original_name)->toBe('document.pdf');
|
|
});
|
|
|
|
test('create share with password does not store encryption key', function () {
|
|
$file = UploadedFile::fake()->create('secret.txt', 512);
|
|
|
|
$share = $this->service->createShare([
|
|
['file' => $file, 'relativePath' => null],
|
|
], [
|
|
'password' => 'my-password',
|
|
]);
|
|
|
|
expect($share->password)->not->toBeNull();
|
|
expect($share->encryption_key)->toBeNull();
|
|
expect(Hash::check('my-password', $share->password))->toBeTrue();
|
|
});
|
|
|
|
test('create share with options sets expiration and max downloads', function () {
|
|
$file = UploadedFile::fake()->create('file.txt', 256);
|
|
|
|
$share = $this->service->createShare([
|
|
['file' => $file, 'relativePath' => null],
|
|
], [
|
|
'expires_at' => now()->addDay(),
|
|
'max_downloads' => 5,
|
|
]);
|
|
|
|
expect($share->expires_at)->not->toBeNull();
|
|
expect($share->max_downloads)->toBe(5);
|
|
});
|
|
|
|
test('create share with multiple files', function () {
|
|
$file1 = UploadedFile::fake()->create('file1.txt', 100);
|
|
$file2 = UploadedFile::fake()->create('file2.txt', 200);
|
|
|
|
$share = $this->service->createShare([
|
|
['file' => $file1, 'relativePath' => 'folder/file1.txt'],
|
|
['file' => $file2, 'relativePath' => 'folder/file2.txt'],
|
|
]);
|
|
|
|
expect($share->files)->toHaveCount(2);
|
|
expect($share->files->first()->relative_path)->toBe('folder/file1.txt');
|
|
});
|
|
|
|
test('delete share removes files and database records', function () {
|
|
$file = UploadedFile::fake()->create('file.txt', 100);
|
|
|
|
$share = $this->service->createShare([
|
|
['file' => $file, 'relativePath' => null],
|
|
]);
|
|
|
|
$shareId = $share->id;
|
|
$token = $share->token;
|
|
|
|
$this->service->deleteShare($share);
|
|
|
|
expect(Share::query()->find($shareId))->toBeNull();
|
|
expect(ShareFile::query()->where('share_id', $shareId)->count())->toBe(0);
|
|
expect(Storage::disk('shares')->directories())->not->toContain($token);
|
|
});
|
|
|
|
test('verify password returns true for correct password', function () {
|
|
$file = UploadedFile::fake()->create('file.txt', 100);
|
|
|
|
$share = $this->service->createShare([
|
|
['file' => $file, 'relativePath' => null],
|
|
], [
|
|
'password' => 'correct-password',
|
|
]);
|
|
|
|
expect($this->service->verifyPassword($share, 'correct-password'))->toBeTrue();
|
|
expect($this->service->verifyPassword($share, 'wrong-password'))->toBeFalse();
|
|
});
|
|
|
|
test('verify password returns true for non-password share', function () {
|
|
$file = UploadedFile::fake()->create('file.txt', 100);
|
|
|
|
$share = $this->service->createShare([
|
|
['file' => $file, 'relativePath' => null],
|
|
]);
|
|
|
|
expect($this->service->verifyPassword($share, 'any'))->toBeTrue();
|
|
});
|
|
|
|
test('record download increments counter', function () {
|
|
$share = Share::factory()->create(['download_count' => 0]);
|
|
|
|
$this->service->recordDownload($share);
|
|
|
|
expect($share->fresh()->download_count)->toBe(1);
|
|
});
|
|
|
|
test('record download auto-deletes when limit reached', function () {
|
|
$share = Share::factory()->withMaxDownloads(1)->create(['download_count' => 0]);
|
|
|
|
$this->service->recordDownload($share);
|
|
|
|
expect(Share::query()->find($share->id))->toBeNull();
|
|
});
|
|
|
|
test('get total used space sums share sizes', function () {
|
|
Share::factory()->create(['total_size' => 1000]);
|
|
Share::factory()->create(['total_size' => 2000]);
|
|
|
|
expect($this->service->getTotalUsedSpace())->toBe(3000);
|
|
});
|
|
|
|
test('is storage full checks against quota', function () {
|
|
Setting::set('max_storage_quota', 1000);
|
|
|
|
Share::factory()->create(['total_size' => 999]);
|
|
expect($this->service->isStorageFull())->toBeFalse();
|
|
|
|
Share::factory()->create(['total_size' => 1]);
|
|
expect($this->service->isStorageFull())->toBeTrue();
|
|
});
|
|
|
|
test('get decryption key returns stored key for non-password share', function () {
|
|
$file = UploadedFile::fake()->create('file.txt', 100);
|
|
|
|
$share = $this->service->createShare([
|
|
['file' => $file, 'relativePath' => null],
|
|
]);
|
|
|
|
$key = $this->service->getDecryptionKey($share);
|
|
|
|
expect($key)->not->toBeNull();
|
|
expect(strlen($key))->toBe(64);
|
|
});
|
|
|
|
test('get decryption key derives key for password share', function () {
|
|
$file = UploadedFile::fake()->create('file.txt', 100);
|
|
|
|
$share = $this->service->createShare([
|
|
['file' => $file, 'relativePath' => null],
|
|
], [
|
|
'password' => 'test-password',
|
|
]);
|
|
|
|
$key = $this->service->getDecryptionKey($share, 'test-password');
|
|
|
|
expect($key)->not->toBeNull();
|
|
expect(strlen($key))->toBe(64);
|
|
});
|
|
|
|
test('get decryption key throws for password share without password', function () {
|
|
$file = UploadedFile::fake()->create('file.txt', 100);
|
|
|
|
$share = $this->service->createShare([
|
|
['file' => $file, 'relativePath' => null],
|
|
], [
|
|
'password' => 'test-password',
|
|
]);
|
|
|
|
$this->service->getDecryptionKey($share);
|
|
})->throws(RuntimeException::class, 'Password required');
|