clusev/tests/Feature/ExternalTlsModeTest.php

118 lines
4.4 KiB
PHP

<?php
namespace Tests\Feature;
use App\Http\Middleware\PanelScheme;
use App\Models\Setting;
use App\Services\DeploymentService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Tests\TestCase;
class ExternalTlsModeTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// Remove the active-domain snapshot so domain() falls back to the DB Setting
// (configuredDomain()), letting tests control the domain via Setting::put().
$snapshot = storage_path('framework/active-domain');
if (is_file($snapshot)) {
unlink($snapshot);
}
}
protected function tearDown(): void
{
// Restore an empty snapshot (bare-IP) after each test so the dev stack is
// unaffected; the entrypoint will overwrite it on next container start anyway.
file_put_contents(storage_path('framework/active-domain'), '');
parent::tearDown();
}
private function runPanelScheme(Request $request): SymfonyResponse
{
$mw = new PanelScheme(app(DeploymentService::class));
return $mw->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://<domain> 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'));
}
}