Protect recordings per user, seed a demo login on container start, and bind-mount the app with Vite HMR for local Compose development.
163 lines
5.9 KiB
PHP
163 lines
5.9 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Events\RecordingTranscriptionUpdated;
|
|
use App\Jobs\TranscribeRecording;
|
|
use App\Models\Recording;
|
|
use App\Models\User;
|
|
use App\Services\TranscriptionService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Event;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Laravel\Ai\Transcription;
|
|
use Tests\TestCase;
|
|
|
|
class TranscriptionBroadcastTest 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_report_progress_broadcasts_transcription_updated(): void
|
|
{
|
|
Event::fake([RecordingTranscriptionUpdated::class]);
|
|
|
|
$recording = Recording::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'title' => 'Broadcast progress',
|
|
'original_filename' => 'progress.mp3',
|
|
'file_path' => 'recordings/progress.mp3',
|
|
'file_size_bytes' => 100,
|
|
'transcription_status' => 'processing',
|
|
'transcription_driver' => 'local',
|
|
'transcription_started_at' => now(),
|
|
]);
|
|
|
|
$recording->reportProgress('Transcribing locally…', 50);
|
|
|
|
Event::assertDispatched(RecordingTranscriptionUpdated::class, function (RecordingTranscriptionUpdated $event) use ($recording): bool {
|
|
return $event->recording->is($recording)
|
|
&& $event->broadcastWith()['percent'] === 50
|
|
&& $event->broadcastWith()['progress'] === 'Transcribing locally…';
|
|
});
|
|
}
|
|
|
|
public function test_successful_transcription_broadcasts_done_status(): void
|
|
{
|
|
Event::fake([RecordingTranscriptionUpdated::class]);
|
|
Storage::fake('local');
|
|
Storage::disk('local')->put('recordings/sample.mp3', 'fake-audio-bytes');
|
|
|
|
Transcription::fake(['Hello from the recorder.']);
|
|
|
|
$recording = Recording::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'title' => 'Sample',
|
|
'original_filename' => 'sample.mp3',
|
|
'file_path' => 'recordings/sample.mp3',
|
|
'file_size_bytes' => 12,
|
|
'transcription_status' => 'pending',
|
|
'transcription_driver' => 'local',
|
|
'transcription_started_at' => now(),
|
|
]);
|
|
|
|
(new TranscribeRecording($recording))->handle(app(TranscriptionService::class));
|
|
|
|
Event::assertDispatched(RecordingTranscriptionUpdated::class, function (RecordingTranscriptionUpdated $event) use ($recording): bool {
|
|
$payload = $event->broadcastWith();
|
|
|
|
return $event->recording->is($recording)
|
|
&& $payload['status'] === 'done'
|
|
&& $payload['percent'] === 100
|
|
&& $payload['has_transcript'] === true
|
|
&& ($payload['word_count'] ?? 0) > 0;
|
|
});
|
|
}
|
|
|
|
public function test_failed_transcription_broadcasts_failure(): void
|
|
{
|
|
Event::fake([RecordingTranscriptionUpdated::class]);
|
|
Storage::fake('local');
|
|
Storage::disk('local')->put('recordings/bad.mp3', 'fake-audio-bytes');
|
|
|
|
Transcription::fake(function () {
|
|
throw new \RuntimeException('Provider unavailable');
|
|
});
|
|
|
|
$recording = Recording::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'title' => 'Bad',
|
|
'original_filename' => 'bad.mp3',
|
|
'file_path' => 'recordings/bad.mp3',
|
|
'file_size_bytes' => 12,
|
|
'transcription_status' => 'pending',
|
|
'transcription_driver' => 'local',
|
|
'transcription_started_at' => now(),
|
|
]);
|
|
|
|
try {
|
|
(new TranscribeRecording($recording))->handle(app(TranscriptionService::class));
|
|
$this->fail('Expected transcription to throw');
|
|
} catch (\RuntimeException $e) {
|
|
$this->assertSame('Provider unavailable', $e->getMessage());
|
|
}
|
|
|
|
Event::assertDispatched(RecordingTranscriptionUpdated::class, function (RecordingTranscriptionUpdated $event) use ($recording): bool {
|
|
$payload = $event->broadcastWith();
|
|
|
|
return $event->recording->is($recording)
|
|
&& $payload['status'] === 'failed'
|
|
&& $payload['error'] === 'Provider unavailable';
|
|
});
|
|
}
|
|
|
|
public function test_cancel_transcription_broadcasts_cancelled_status(): void
|
|
{
|
|
Event::fake([RecordingTranscriptionUpdated::class]);
|
|
|
|
$recording = Recording::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'title' => 'Cancel me',
|
|
'original_filename' => 'cancel.mp3',
|
|
'file_path' => 'recordings/cancel.mp3',
|
|
'file_size_bytes' => 100,
|
|
'transcription_status' => 'processing',
|
|
'transcription_driver' => 'local',
|
|
'transcription_started_at' => now(),
|
|
]);
|
|
|
|
$recording->cancelTranscription();
|
|
|
|
Event::assertDispatched(RecordingTranscriptionUpdated::class, function (RecordingTranscriptionUpdated $event) use ($recording): bool {
|
|
return $event->recording->is($recording)
|
|
&& $event->broadcastWith()['status'] === 'cancelled';
|
|
});
|
|
}
|
|
|
|
public function test_broadcast_event_uses_private_recording_channel(): void
|
|
{
|
|
$recording = Recording::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'title' => 'Channel',
|
|
'original_filename' => 'channel.mp3',
|
|
'file_path' => 'recordings/channel.mp3',
|
|
'file_size_bytes' => 100,
|
|
'transcription_status' => 'pending',
|
|
]);
|
|
|
|
$event = new RecordingTranscriptionUpdated($recording);
|
|
|
|
$this->assertSame('RecordingTranscriptionUpdated', $event->broadcastAs());
|
|
$this->assertSame('private-recording.'.$recording->id, $event->broadcastOn()[0]->name);
|
|
}
|
|
}
|