123 lines
4.9 KiB
PHP
123 lines
4.9 KiB
PHP
<?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 website’s 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 portal’s 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}");
|
||
}
|
||
});
|