app->forgetInstance(IceCandidateManager::class); } public function test_manager_resolves_metered_as_default_driver(): void { Http::fake([ '*metered*' => Http::response([ ['urls' => 'stun:stun.relay.metered.ca:80'], ], 200), ]); $manager = app(IceCandidateManager::class); $candidates = $manager->getIceCandidates(); $this->assertCount(1, $candidates); $this->assertEquals('stun:stun.relay.metered.ca:80', $candidates->toArray()[0]['urls']); } public function test_manager_resolves_convexsol_when_configured(): void { config([ 'services.ice.default' => 'convexsol', 'services.ice.providers.convexsol' => [ 'username' => 'convex_usr', 'password' => 'convex_pwd', 'turn_urls' => ['turn:turn.convexsol.com:3478'], 'stun_urls' => ['stun:turn.convexsol.com:3478'], ], ]); $manager = app(IceCandidateManager::class); $candidates = $manager->getIceCandidates(); $this->assertCount(2, $candidates); $array = $candidates->toArray(); $this->assertEquals('convex_usr', $array[1]['username']); $this->assertEquals('convex_pwd', $array[1]['credential']); } public function test_manager_falls_back_and_logs_warning_when_primary_fails(): void { Log::shouldReceive('warning') ->once() ->withArgs(function ($message, $context) { return str_contains($message, 'Primary ICE candidate provider [metered] failed') && $context['primary_driver'] === 'metered' && $context['fallback_driver'] === 'convexsol'; }); // Fail primary Metered HTTP request Http::fake([ '*metered*' => Http::response('Gateway Error', 502), ]); config([ 'services.ice.default' => 'metered', 'services.ice.fallback' => 'convexsol', 'services.ice.providers.convexsol' => [ 'username' => 'fallback_user', 'password' => 'fallback_password', 'turn_urls' => ['turn:turn.convexsol.com:3478'], 'stun_urls' => ['stun:turn.convexsol.com:3478'], ], ]); $manager = app(IceCandidateManager::class); $candidates = $manager->getIceCandidates(); $this->assertCount(2, $candidates); $array = $candidates->toArray(); $this->assertEquals('fallback_user', $array[1]['username']); } public function test_manager_falls_back_to_default_stun_and_logs_error_when_both_fail(): void { Log::shouldReceive('warning')->once(); Log::shouldReceive('error') ->once() ->withArgs(function ($message) { return str_contains($message, 'Fallback ICE candidate provider [convexsol] also failed'); }); // Fail primary Metered HTTP request Http::fake([ '*metered*' => Http::response('Gateway Error', 502), ]); $manager = app(IceCandidateManager::class); $manager->extend('convexsol', function () { return new class implements IceCandidateProviderInterface { public function getIceCandidates(): IceCandidatesDto { throw new RuntimeException('ConvexSol Server Error'); } }; }); config([ 'services.ice.default' => 'metered', 'services.ice.fallback' => 'convexsol', ]); $candidates = $manager->getIceCandidates(); $this->assertCount(1, $candidates); $this->assertContains('stun:stun.l.google.com:19302', $candidates->toArray()[0]['urls']); } public function test_container_resolves_interface_to_manager(): void { $resolved = app(IceCandidateProviderInterface::class); $this->assertInstanceOf(IceCandidateManager::class, $resolved); } }