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_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')); } }