ZipStream writes via fwrite(php://output) which FrankenPHP silently drops, resulting in 0-byte ZIP downloads. Switch to ZipArchive to build the ZIP as a temp file on disk, then serve with response()->download(). - Rewrite download() to use ZipArchive + BinaryFileResponse - Remove decryptFileToCallback() from FileEncryptionService - Update tests for BinaryFileResponse instead of StreamedResponse - Remove maennchen/zipstream-php dependency Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
212 lines
6.2 KiB
PHP
212 lines
6.2 KiB
PHP
<?php
|
|
|
|
use App\Models\Share;
|
|
use App\Services\FileEncryptionService;
|
|
use App\Services\ShareService;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Livewire\Livewire;
|
|
|
|
test('share download page renders for valid share', function () {
|
|
Storage::fake('shares');
|
|
|
|
$share = createShareWithFile();
|
|
|
|
$response = $this->get(route('share.download', $share));
|
|
|
|
$response->assertOk();
|
|
});
|
|
|
|
test('share download page returns 404 for expired share', function () {
|
|
$share = Share::factory()->expired()->create();
|
|
|
|
$response = $this->get(route('share.download', $share));
|
|
|
|
$response->assertNotFound();
|
|
});
|
|
|
|
test('share download page returns 404 when download limit reached', function () {
|
|
$share = Share::factory()->withMaxDownloads(1)->create(['download_count' => 1]);
|
|
|
|
$response = $this->get(route('share.download', $share));
|
|
|
|
$response->assertNotFound();
|
|
});
|
|
|
|
test('share download page shows password form for password-protected share', function () {
|
|
Storage::fake('shares');
|
|
|
|
$share = createShareWithFile('secret-pass');
|
|
|
|
$response = $this->get(route('share.download', $share));
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('password');
|
|
});
|
|
|
|
test('password verification works for protected share', function () {
|
|
Storage::fake('shares');
|
|
|
|
$share = createShareWithFile('my-password');
|
|
|
|
Livewire::test(\App\Livewire\ShareDownload::class, ['share' => $share])
|
|
->assertSet('authenticated', false)
|
|
->set('password', 'my-password')
|
|
->call('verifyPassword')
|
|
->assertSet('authenticated', true)
|
|
->assertHasNoErrors();
|
|
});
|
|
|
|
test('wrong password is rejected', function () {
|
|
Storage::fake('shares');
|
|
|
|
$share = createShareWithFile('my-password');
|
|
|
|
Livewire::test(\App\Livewire\ShareDownload::class, ['share' => $share])
|
|
->set('password', 'wrong-password')
|
|
->call('verifyPassword')
|
|
->assertSet('authenticated', false)
|
|
->assertHasErrors(['password']);
|
|
});
|
|
|
|
test('non-password share shows files directly', function () {
|
|
Storage::fake('shares');
|
|
|
|
$share = createShareWithFile();
|
|
|
|
Livewire::test(\App\Livewire\ShareDownload::class, ['share' => $share])
|
|
->assertSet('authenticated', true);
|
|
});
|
|
|
|
test('download counter increments on zip download', function () {
|
|
Storage::fake('shares');
|
|
|
|
$share = createShareWithFile();
|
|
$share->load('files');
|
|
|
|
$encryptionService = app(FileEncryptionService::class);
|
|
$key = $share->encryption_key;
|
|
$content = 'test content';
|
|
|
|
foreach ($share->files as $file) {
|
|
$dir = Storage::disk('shares')->path($share->token);
|
|
if (! is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
$encryptedPath = $dir.'/'.basename($file->stored_path);
|
|
$tempSource = tempnam(sys_get_temp_dir(), 'test');
|
|
file_put_contents($tempSource, $content);
|
|
$encryptionService->encryptFile($tempSource, $encryptedPath, $key);
|
|
unlink($tempSource);
|
|
$file->update(['file_size' => strlen($content)]);
|
|
}
|
|
|
|
$response = $this->withSession(['share_password_'.$share->token => null])
|
|
->get(route('share.download.all', $share));
|
|
|
|
$response->assertDownload();
|
|
|
|
expect($share->fresh()->download_count)->toBe(1);
|
|
});
|
|
|
|
test('zip download produces a valid archive', function () {
|
|
Storage::fake('shares');
|
|
|
|
$share = createShareWithFile();
|
|
$share->load('files');
|
|
|
|
$encryptionService = app(FileEncryptionService::class);
|
|
$key = $share->encryption_key;
|
|
$content = 'hello zip content';
|
|
|
|
foreach ($share->files as $file) {
|
|
$dir = Storage::disk('shares')->path($share->token);
|
|
if (! is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
$encryptedPath = $dir.'/'.basename($file->stored_path);
|
|
$tempSource = tempnam(sys_get_temp_dir(), 'test');
|
|
file_put_contents($tempSource, $content);
|
|
$encryptionService->encryptFile($tempSource, $encryptedPath, $key);
|
|
unlink($tempSource);
|
|
$file->update(['file_size' => strlen($content)]);
|
|
}
|
|
|
|
$response = $this->get(route('share.download.all', $share));
|
|
$response->assertDownload();
|
|
|
|
$zipPath = $response->getFile()->getPathname();
|
|
|
|
$zip = new \ZipArchive;
|
|
$result = $zip->open($zipPath);
|
|
|
|
expect($result)->toBe(true);
|
|
expect($zip->numFiles)->toBe(1);
|
|
expect($zip->statIndex(0)['size'])->toBe(strlen($content));
|
|
|
|
$zip->close();
|
|
});
|
|
|
|
test('last download streams successfully before auto-delete', function () {
|
|
Storage::fake('shares');
|
|
|
|
$share = createShareWithFile();
|
|
$share->update(['max_downloads' => 1]);
|
|
$share->load('files');
|
|
|
|
$encryptionService = app(FileEncryptionService::class);
|
|
$key = $share->encryption_key;
|
|
$content = 'last download content';
|
|
|
|
foreach ($share->files as $file) {
|
|
$dir = Storage::disk('shares')->path($share->token);
|
|
if (! is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
$encryptedPath = $dir.'/'.basename($file->stored_path);
|
|
$tempSource = tempnam(sys_get_temp_dir(), 'test');
|
|
file_put_contents($tempSource, $content);
|
|
$encryptionService->encryptFile($tempSource, $encryptedPath, $key);
|
|
unlink($tempSource);
|
|
$file->update(['file_size' => strlen($content)]);
|
|
}
|
|
|
|
$response = $this->get(route('share.download.all', $share));
|
|
$response->assertDownload();
|
|
|
|
// Share was deleted after download
|
|
expect(Share::query()->find($share->id))->toBeNull();
|
|
});
|
|
|
|
test('share auto-deletes after reaching download limit', function () {
|
|
Storage::fake('shares');
|
|
|
|
$service = app(ShareService::class);
|
|
$file = UploadedFile::fake()->create('file.txt', 100);
|
|
|
|
$share = $service->createShare([
|
|
['file' => $file, 'relativePath' => null],
|
|
], [
|
|
'max_downloads' => 1,
|
|
]);
|
|
|
|
$service->recordDownload($share);
|
|
|
|
expect(Share::query()->find($share->id))->toBeNull();
|
|
});
|
|
|
|
/**
|
|
* Helper to create a share with an actual encrypted file.
|
|
*/
|
|
function createShareWithFile(?string $password = null): Share
|
|
{
|
|
$service = app(ShareService::class);
|
|
$file = UploadedFile::fake()->create('testfile.txt', 100);
|
|
|
|
return $service->createShare([
|
|
['file' => $file, 'relativePath' => null],
|
|
], [
|
|
'password' => $password,
|
|
]);
|
|
}
|