withoutMiddleware(EnsurePlatformSession::class); Storage::fake('local'); $this->user = User::create([ 'public_id' => 'test-user-recording', 'name' => 'Test User', 'email' => 'recording@example.com', ]); $this->organization = Organization::create([ 'owner_ref' => $this->user->public_id, 'name' => 'Test Org', 'slug' => 'test-org-recording', '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_host_can_upload_browser_recording_to_ladill_storage(): void { $room = Room::create([ 'organization_id' => $this->organization->id, 'owner_ref' => $this->user->public_id, 'host_user_ref' => $this->user->public_id, 'title' => 'Recorded 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(), ]); $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(), 'ended_at' => now(), 'duration_seconds' => 60, ]); $this->withSession(["meet.participant.{$session->uuid}" => $participant->uuid]) ->actingAs($this->user) ->post("/room/{$session->uuid}/recordings/{$recording->uuid}/upload", [ 'recording' => UploadedFile::fake()->create('meet.webm', 128, 'video/webm'), ]) ->assertOk() ->assertJson(['status' => 'ready']); $recording->refresh(); $this->assertSame('ready', $recording->status); $this->assertNotNull($recording->storage_path); Storage::disk('local')->assertExists($recording->storage_path); } public function test_host_can_upload_after_session_ended_without_participant_cookie(): void { $room = Room::create([ 'organization_id' => $this->organization->id, 'owner_ref' => $this->user->public_id, 'host_user_ref' => $this->user->public_id, 'title' => 'Ended meeting recording', 'type' => 'instant', 'status' => 'ended', 'timezone' => 'UTC', ]); $session = Session::create([ 'room_id' => $room->id, 'owner_ref' => $this->user->public_id, 'media_room_name' => 'room-'.$room->uuid, 'status' => 'ended', 'started_at' => now()->subMinutes(10), 'ended_at' => now()->subMinute(), ]); 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' => 'left', 'joined_at' => now()->subMinutes(10), 'left_at' => now()->subMinute(), ]); $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(5), 'ended_at' => now()->subMinute(), 'duration_seconds' => 240, ]); $this->actingAs($this->user) ->post("/room/{$session->uuid}/recordings/{$recording->uuid}/upload", [ 'recording' => UploadedFile::fake()->create('meet.webm', 128, 'video/webm'), ]) ->assertOk() ->assertJson(['status' => 'ready']); } public function test_host_can_mark_processing_recording_failed(): void { $room = Room::create([ 'organization_id' => $this->organization->id, 'owner_ref' => $this->user->public_id, 'host_user_ref' => $this->user->public_id, 'title' => 'Failed capture', 'type' => 'instant', 'status' => 'ended', 'timezone' => 'UTC', ]); $session = Session::create([ 'room_id' => $room->id, 'owner_ref' => $this->user->public_id, 'media_room_name' => 'room-'.$room->uuid, 'status' => 'ended', 'started_at' => now()->subMinutes(10), 'ended_at' => now(), ]); 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' => 'left', 'joined_at' => now()->subMinutes(10), 'left_at' => now(), ]); $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(5), 'ended_at' => now(), ]); $this->actingAs($this->user) ->postJson("/room/{$session->uuid}/recordings/{$recording->uuid}/fail", [ 'reason' => 'Browser capture failed.', ]) ->assertOk() ->assertJson(['status' => 'failed']); $recording->refresh(); $this->assertSame('failed', $recording->status); $this->assertSame('Browser capture failed.', $recording->failure_reason); } public function test_stale_processing_recording_is_marked_failed(): void { $room = Room::create([ 'organization_id' => $this->organization->id, 'owner_ref' => $this->user->public_id, 'host_user_ref' => $this->user->public_id, 'title' => 'Stale processing', 'type' => 'instant', 'status' => 'ended', 'timezone' => 'UTC', ]); $session = Session::create([ 'room_id' => $room->id, 'owner_ref' => $this->user->public_id, 'media_room_name' => 'room-'.$room->uuid, 'status' => 'ended', 'started_at' => now()->subHour(), 'ended_at' => now()->subMinutes(30), ]); $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()->subHour(), 'ended_at' => now()->subMinutes(30), ]); $recording = app(\App\Services\Meet\RecordingService::class)->resolveProcessingState($recording); $this->assertSame('failed', $recording->status); $this->assertNotNull($recording->failure_reason); } public function test_empty_ready_recording_is_marked_failed(): void { $room = Room::create([ 'organization_id' => $this->organization->id, 'owner_ref' => $this->user->public_id, 'host_user_ref' => $this->user->public_id, 'title' => 'Empty recording', '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' => 'ended', 'started_at' => now()->subHour(), 'ended_at' => now(), ]); $recording = Recording::create([ 'owner_ref' => $this->user->public_id, 'session_id' => $session->id, 'status' => 'ready', 'layout' => 'gallery', 'storage_path' => 'recordings/'.$session->uuid.'/empty.webm', 'file_size' => 0, 'started_at' => now()->subHour(), 'ended_at' => now(), ]); Storage::disk('local')->put($recording->storage_path, ''); $recording = app(\App\Services\Meet\RecordingService::class)->ensureStorageValid($recording); $this->assertSame('failed', $recording->status); $this->assertNull($recording->storage_path); } }