handle($request, fn () => new Response('ok')); } public function test_external_tls_flag_reflects_the_setting(): void { $deployment = app(DeploymentService::class); $this->assertFalse($deployment->externalTls(), 'default is caddy'); $deployment->setTlsMode('external'); $this->assertTrue(app(DeploymentService::class)->externalTls()); $deployment->setTlsMode('garbage'); // invalid → caddy $this->assertFalse(app(DeploymentService::class)->externalTls()); } public function test_caddy_ask_denies_in_external_mode_but_approves_in_caddy_mode(): void { Setting::put('panel_domain', 'panel.example.com'); // caddy mode: the configured domain is approved $this->get('/_caddy/ask?domain=panel.example.com')->assertOk(); // external mode: even the configured domain is denied (no ACME) app(DeploymentService::class)->setTlsMode('external'); $this->get('/_caddy/ask?domain=panel.example.com')->assertForbidden(); } public function test_panel_scheme_skips_the_https_redirect_in_external_mode(): void { Setting::put('panel_domain', 'panel.example.com'); app(DeploymentService::class)->setTlsMode('external'); $request = Request::create('http://panel.example.com/dashboard'); $response = $this->runPanelScheme($request); $this->assertSame(200, $response->getStatusCode(), 'external mode must NOT redirect to https'); $this->assertSame('ok', $response->getContent()); } public function test_panel_scheme_still_forces_https_in_caddy_mode(): void { Setting::put('panel_domain', 'panel.example.com'); // caddy mode (default) $request = Request::create('http://panel.example.com/dashboard'); $response = $this->runPanelScheme($request); $this->assertSame(301, $response->getStatusCode()); $this->assertStringStartsWith('https://panel.example.com', $response->headers->get('Location')); } public function test_panel_scheme_still_refuses_a_foreign_host_in_external_mode(): void { Setting::put('panel_domain', 'panel.example.com'); app(DeploymentService::class)->setTlsMode('external'); $this->expectException(HttpException::class); $this->runPanelScheme(Request::create('http://evil.example.com/dashboard')); } public function test_status_card_does_not_claim_lets_encrypt_in_external_mode(): void { $this->actingAs(User::factory()->create(['must_change_password' => false])); Setting::put('panel_domain', 'panel.example.com'); app(DeploymentService::class)->setTlsMode('external'); Livewire::test(SystemIndex::class) ->assertSee(__('system.tls_active_external_title')) ->assertDontSee('Let\'s Encrypt'); } public function test_reverb_client_uses_wss_on_443_for_the_active_domain(): void { Setting::put('panel_domain', 'panel.example.com'); app(DeploymentService::class)->setTlsMode('external'); // External proxy terminates TLS; the request arrives here as plain HTTP on :80. app()->instance('request', Request::create('http://panel.example.com/dashboard')); $rc = app(DeploymentService::class)->reverbClient(); $this->assertSame('panel.example.com', $rc['host']); $this->assertSame(443, $rc['port'], 'WS must use the HTTPS front door (443), not the internal :80'); $this->assertSame('https', $rc['scheme'], 'WS must be wss on the active domain'); } public function test_reverb_client_stays_plain_ws_on_the_bare_ip_recovery_path(): void { Setting::put('panel_domain', 'panel.example.com'); app()->instance('request', Request::create('http://10.0.0.5/dashboard')); $rc = app(DeploymentService::class)->reverbClient(); $this->assertSame('10.0.0.5', $rc['host']); $this->assertSame('http', $rc['scheme'], 'the bare-IP recovery path has no TLS'); } public function test_external_mode_forces_https_url_generation_for_the_active_domain(): void { Setting::put('panel_domain', 'panel.example.com'); app(DeploymentService::class)->setTlsMode('external'); // External proxy terminates TLS; the app itself sees a plain-HTTP request here. $response = $this->runPanelScheme(Request::create('http://panel.example.com/dashboard')); $this->assertSame(200, $response->getStatusCode()); // Generated URLs (assets, routes) must be https:// so the HTTPS page's CSP // ('self') does not block them — the bug that left the panel with no CSS/JS. $this->assertSame('https://panel.example.com/build/app.css', url('build/app.css')); $this->assertStringStartsWith('https://panel.example.com', route('login')); } }