CluPilotCloud/tests/Feature/PortalHostTest.php

123 lines
4.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Route;
/**
* The website and the portal each answer on their own hostname, and nowhere
* else.
*
* A route belongs to a host. One that is not registered for the host being
* asked cannot answer, and the honest reply is 404 — that is the whole
* mechanism, and Route::domain() is all of it.
*
* The first attempt bound the landing page alone and left everything else
* host-agnostic. The website duly vanished from the portal, and the portal
* stayed reachable from the website: www.clupilot.com/dashboard kept working.
* Half a separation is no separation, which is why these tests check BOTH
* directions for every route rather than only the one that was reported.
*
* Routes are built once at boot, so changing config alone changes nothing.
* These re-register them against a fresh router — the idiom WelcomeTest already
* uses for the status host.
*/
function routerWithHosts(?string $siteHost, ?string $appHost = null): Router
{
config()->set('admin_access.site_host', $siteHost ?? '');
config()->set('admin_access.app_host', $appHost ?? '');
$router = new Router(app('events'), app());
Route::swap($router);
require base_path('routes/web.php');
$router->getRoutes()->refreshNameLookups();
return $router;
}
/** Does this host answer this path at all? */
function answers(Router $router, string $host, string $path): bool
{
try {
$router->getRoutes()->match(Request::create("http://{$host}{$path}", 'GET'));
return true;
} catch (Symfony\Component\HttpKernel\Exception\NotFoundHttpException) {
return false;
}
}
it('does not serve the portal on the websites hostname', function () {
// The half that was missing, and the one that was reported back.
$router = routerWithHosts('www.example.test', 'app.example.test');
foreach (['/dashboard', '/cloud', '/invoices', '/settings', '/login', '/register'] as $path) {
expect(answers($router, 'app.example.test', $path))->toBeTrue("app should answer {$path}")
->and(answers($router, 'www.example.test', $path))->toBeFalse("www must not answer {$path}");
}
});
it('does not serve the website on the portals hostname', function () {
$router = routerWithHosts('www.example.test', 'app.example.test');
foreach (['/robots.txt', '/legal/impressum', '/legal/agb'] as $path) {
expect(answers($router, 'www.example.test', $path))->toBeTrue("www should answer {$path}")
->and(answers($router, 'app.example.test', $path))->toBeFalse("app must not answer {$path}");
}
expect($router->getRoutes()->match(Request::create('http://www.example.test/', 'GET'))->getActionName())
->toContain('LandingController');
});
it('answers "/" on the portal host with the product, never a 404', function () {
// "/" is what somebody types from memory. Turning that into an error page
// to make a point about hostnames helps nobody.
$router = routerWithHosts('www.example.test', 'app.example.test');
$response = $router->dispatch(Request::create('http://app.example.test/', 'GET'));
expect($response->getStatusCode())->toBe(302)
->and($response->headers->get('Location'))->toContain('/login');
});
it('sends a signed-in customer on the portal host to their dashboard', function () {
$this->actingAs(User::factory()->create());
$router = routerWithHosts('www.example.test', 'app.example.test');
$response = $router->dispatch(Request::create('http://app.example.test/', 'GET'));
expect($response->getStatusCode())->toBe(302)
->and($response->headers->get('Location'))->toContain('/dashboard');
});
it('keeps the Stripe webhook reachable on any host', function () {
// Stripe posts to whichever URL it was configured with, and a hostname
// mismatch there loses payments rather than showing a 404 to a person.
$router = routerWithHosts('www.example.test', 'app.example.test');
$matches = function (string $host) use ($router) {
try {
$router->getRoutes()->match(Request::create("http://{$host}/webhooks/stripe", 'POST'));
return true;
} catch (Symfony\Component\HttpKernel\Exception\NotFoundHttpException) {
return false;
}
};
expect($matches('www.example.test'))->toBeTrue()
->and($matches('app.example.test'))->toBeTrue()
->and($matches('anything.example.test'))->toBeTrue();
});
it('binds nothing at all while no hostnames are configured', function () {
// The installed base, and every development machine reached by a bare IP.
// Switching this on by default would lock those out of everything at once.
$router = routerWithHosts(null, null);
foreach (['/', '/dashboard', '/legal/impressum'] as $path) {
expect(answers($router, 'anything.example.test', $path))->toBeTrue("everything should answer {$path}");
}
});