withoutMiddleware(EnsurePlatformSession::class); $this->user = User::create([ 'public_id' => 'test-user-egress', 'name' => 'Test User', 'email' => 'egress@example.com', ]); $this->organization = Organization::create([ 'owner_ref' => $this->user->public_id, 'name' => 'Test Org', 'slug' => 'test-org-egress', 'timezone' => 'UTC', 'settings' => ['onboarded' => true], ]); Member::create([ 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $this->user->public_id, 'role' => 'owner', ]); $base = rtrim((string) config('billing.api_url'), '/'); Http::fake([ $base.'/can-afford*' => Http::response(['affordable' => true]), $base.'/debit' => Http::response(['ok' => true]), ]); } public function test_start_recording_returns_egress_capture_mode_when_available(): void { config([ 'meet.recordings.egress.enabled' => true, 'meet.media.livekit.url' => 'wss://livekit.example.com', 'meet.media.livekit.api_key' => 'key', 'meet.media.livekit.api_secret' => 'secret', ]); $egressMock = Mockery::mock(LiveKitEgressService::class); $egressMock->shouldReceive('isAvailable')->andReturn(true); $egressMock->shouldReceive('tryStart')->once()->andReturn([ 'egress_id' => 'EG_test123', 'egress_filepath' => 'recordings/session/recording.mp4', 'capture_mode' => 'egress', 'audio_only' => false, ]); $egressMock->shouldReceive('tryStop')->never(); $this->app->instance(LiveKitEgressService::class, $egressMock); [$session, $participant] = $this->liveSession(); $this->withSession(["meet.participant.{$session->uuid}" => $participant->uuid]) ->actingAs($this->user) ->postJson("/room/{$session->uuid}/recording/start") ->assertOk() ->assertJson([ 'status' => 'recording', 'capture_mode' => 'egress', ]); $recording = Recording::first(); $this->assertSame('egress', $recording->metadata['capture_mode']); $this->assertSame('EG_test123', $recording->metadata['egress_id']); } public function test_stop_recording_stops_egress_and_returns_processing(): void { config(['meet.recordings.egress.enabled' => true]); $egressMock = Mockery::mock(LiveKitEgressService::class); $egressMock->shouldReceive('tryStop')->once()->with('EG_stop123'); $this->app->instance(LiveKitEgressService::class, $egressMock); [$session, $participant] = $this->liveSession(); $recording = Recording::create([ 'owner_ref' => $this->user->public_id, 'session_id' => $session->id, 'status' => 'recording', 'layout' => 'gallery', 'started_by_ref' => $this->user->public_id, 'started_at' => now()->subMinute(), 'metadata' => [ 'capture_mode' => 'egress', 'egress_id' => 'EG_stop123', 'egress_filepath' => 'recordings/'.$session->uuid.'/test.mp4', ], ]); $this->withSession(["meet.participant.{$session->uuid}" => $participant->uuid]) ->actingAs($this->user) ->postJson("/room/{$session->uuid}/recording/stop") ->assertOk() ->assertJson([ 'status' => 'processing', 'capture_mode' => 'egress', ]); $recording->refresh(); $this->assertSame('processing', $recording->status); } public function test_complete_from_egress_marks_recording_ready(): void { [$session] = $this->liveSession(); $storagePath = 'recordings/'.$session->uuid.'/ready.mp4'; $recording = Recording::create([ 'owner_ref' => $this->user->public_id, 'session_id' => $session->id, 'status' => 'processing', 'layout' => 'gallery', 'started_by_ref' => $this->user->public_id, 'started_at' => now()->subMinutes(2), 'ended_at' => now(), 'metadata' => [ 'capture_mode' => 'egress', 'egress_id' => 'EG_complete', 'egress_filepath' => $storagePath, ], ]); $file = new FileInfo([ 'filename' => $storagePath, 'size' => 2048, 'duration' => 120, 'location' => 's3://bucket/'.$storagePath, ]); $info = new EgressInfo([ 'egress_id' => 'EG_complete', 'status' => EgressStatus::EGRESS_COMPLETE, ]); $info->setFileResults([$file]); $recording = app(RecordingService::class)->completeFromEgress($recording, $info); $this->assertSame('ready', $recording->status); $this->assertSame($storagePath, $recording->storage_path); $this->assertSame(2048, $recording->file_size); } public function test_complete_from_egress_marks_failed_on_error_status(): void { [$session] = $this->liveSession(); $recording = Recording::create([ 'owner_ref' => $this->user->public_id, 'session_id' => $session->id, 'status' => 'processing', 'layout' => 'gallery', 'started_by_ref' => $this->user->public_id, 'started_at' => now()->subMinute(), 'metadata' => [ 'capture_mode' => 'egress', 'egress_id' => 'EG_failed', ], ]); $info = new EgressInfo([ 'egress_id' => 'EG_failed', 'status' => EgressStatus::EGRESS_FAILED, 'error' => 'encoder crashed', ]); $recording = app(RecordingService::class)->completeFromEgress($recording, $info); $this->assertSame('failed', $recording->status); $this->assertSame('encoder crashed', $recording->failure_reason); } /** * @return array{0: Session, 1: Participant} */ protected function liveSession(): array { $room = Room::create([ 'organization_id' => $this->organization->id, 'owner_ref' => $this->user->public_id, 'host_user_ref' => $this->user->public_id, 'title' => 'Egress meeting', 'type' => 'instant', 'status' => 'live', 'timezone' => 'UTC', ]); $session = Session::create([ 'room_id' => $room->id, 'owner_ref' => $this->user->public_id, 'media_room_name' => 'room-'.$room->uuid, 'status' => 'live', 'started_at' => now()->subMinutes(5), ]); $participant = Participant::create([ 'owner_ref' => $this->user->public_id, 'session_id' => $session->id, 'user_ref' => $this->user->public_id, 'role' => 'host', 'display_name' => 'Test User', 'status' => 'joined', 'joined_at' => now(), ]); return [$session, $participant]; } }