Protect recordings per user, seed a demo login on container start, and bind-mount the app with Vite HMR for local Compose development.
132 lines
4.3 KiB
PHP
132 lines
4.3 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Jobs\TranscribeRecording;
|
|
use App\Models\Recording;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Bus;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Tests\TestCase;
|
|
|
|
class RecordingDuplicateUploadTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected User $user;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->user = User::factory()->create();
|
|
$this->actingAs($this->user);
|
|
}
|
|
|
|
public function test_duplicate_content_hash_is_skipped_on_upload(): void
|
|
{
|
|
Storage::fake('local');
|
|
Bus::fake();
|
|
|
|
$first = UploadedFile::fake()->createWithContent('meeting.mp3', 'identical-audio-bytes');
|
|
$duplicate = UploadedFile::fake()->createWithContent('meeting-copy.mp3', 'identical-audio-bytes');
|
|
|
|
$this->post(route('recordings.store'), [
|
|
'audio' => [$first],
|
|
])->assertRedirect();
|
|
|
|
$this->assertSame(1, Recording::query()->count());
|
|
Bus::assertDispatched(TranscribeRecording::class, 1);
|
|
|
|
$response = $this->post(route('recordings.store'), [
|
|
'audio' => [$duplicate],
|
|
]);
|
|
|
|
$response->assertRedirect(route('recordings.create'));
|
|
$response->assertSessionHas('error');
|
|
$this->assertSame(1, Recording::query()->count());
|
|
Bus::assertDispatched(TranscribeRecording::class, 1);
|
|
}
|
|
|
|
public function test_batch_upload_skips_duplicate_content_within_request(): void
|
|
{
|
|
Storage::fake('local');
|
|
Bus::fake();
|
|
|
|
$one = UploadedFile::fake()->createWithContent('one.mp3', 'same-bytes');
|
|
$two = UploadedFile::fake()->createWithContent('two.mp3', 'same-bytes');
|
|
$three = UploadedFile::fake()->createWithContent('three.mp3', 'different-bytes');
|
|
|
|
$response = $this->post(route('recordings.store'), [
|
|
'audio' => [$one, $two, $three],
|
|
]);
|
|
|
|
$response->assertRedirect(route('recordings.index'));
|
|
$response->assertSessionHas('success');
|
|
$this->assertStringContainsString('Skipped 1 duplicate', session('success'));
|
|
$this->assertSame(2, Recording::query()->count());
|
|
Bus::assertDispatched(TranscribeRecording::class, 2);
|
|
}
|
|
|
|
public function test_upload_page_includes_existing_fingerprints_for_client_dedupe(): void
|
|
{
|
|
Recording::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'title' => 'Existing',
|
|
'original_filename' => 'note.mp3',
|
|
'file_path' => 'recordings/note.mp3',
|
|
'file_size_bytes' => 2048,
|
|
'content_hash' => str_repeat('a', 64),
|
|
'transcription_status' => 'done',
|
|
]);
|
|
|
|
$this->get(route('recordings.create'))
|
|
->assertOk()
|
|
->assertSee('note.mp3:2048', false)
|
|
->assertSee('Duplicate files', false);
|
|
}
|
|
|
|
public function test_duplicate_filename_and_size_is_skipped_without_content_hash(): void
|
|
{
|
|
Storage::fake('local');
|
|
Bus::fake();
|
|
|
|
Recording::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'title' => 'Legacy',
|
|
'original_filename' => 'legacy.mp3',
|
|
'file_path' => 'recordings/legacy.mp3',
|
|
'file_size_bytes' => strlen('legacy-audio'),
|
|
'content_hash' => null,
|
|
'transcription_status' => 'done',
|
|
]);
|
|
|
|
$response = $this->post(route('recordings.store'), [
|
|
'audio' => [UploadedFile::fake()->createWithContent('legacy.mp3', 'legacy-audio')],
|
|
]);
|
|
|
|
$response->assertRedirect(route('recordings.create'));
|
|
$response->assertSessionHas('error');
|
|
$this->assertSame(1, Recording::query()->count());
|
|
Bus::assertNothingDispatched();
|
|
}
|
|
|
|
public function test_stored_recording_persists_content_hash(): void
|
|
{
|
|
Storage::fake('local');
|
|
Bus::fake();
|
|
|
|
$file = UploadedFile::fake()->createWithContent('hash-me.mp3', 'payload-for-hash');
|
|
|
|
$this->post(route('recordings.store'), [
|
|
'audio' => [$file],
|
|
])->assertRedirect();
|
|
|
|
$recording = Recording::query()->first();
|
|
$this->assertNotNull($recording);
|
|
$this->assertSame(hash('sha256', 'payload-for-hash'), $recording->content_hash);
|
|
}
|
|
}
|