181 lines
6.8 KiB
PHP
181 lines
6.8 KiB
PHP
<?php
|
|
|
|
use Illuminate\Container\Container;
|
|
use Illuminate\Contracts\Console\Kernel;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Facades\Facade;
|
|
|
|
/**
|
|
* Jede Route gehört zu genau einem Hostnamen.
|
|
*
|
|
* Gemessen wird die VOLLSTÄNDIGE Routentabelle, nicht nur routes/web.php. Der
|
|
* Unterschied ist der ganze Punkt: `storage/{path}`, `livewire/*`, `up` und
|
|
* `broadcasting/auth` meldet nicht diese Datei an, sondern ein Service-Provider
|
|
* — und `storage/{path}` war genau die Route, die niemandem aufgefallen ist.
|
|
* Ein Test, der die halbe Tabelle misst, prüft die Trennung nicht.
|
|
*
|
|
* Routen entstehen beim Booten, also ändert `config()->set()` daran nichts.
|
|
* PortalHostTest baut dafür einen frischen Router und lädt routes/web.php neu;
|
|
* das sieht die Provider-Routen nicht. Hier wird deshalb eine zweite
|
|
* Anwendungsinstanz gebootet — dasselbe, was `artisan route:list` tut.
|
|
*
|
|
* Was danach wieder zurückgestellt werden MUSS, und warum: Eloquent hält seinen
|
|
* Connection-Resolver STATISCH. Die zweite Anwendung zeigt ihn auf ihre eigene,
|
|
* leere :memory:-Datenbank, und der restliche Testlauf fände danach keine
|
|
* einzige Tabelle mehr. Das ist kein hypothetisches Risiko — genau so ist es
|
|
* beim Bau dieses Tests passiert.
|
|
*/
|
|
function hostSeparationTable(): array
|
|
{
|
|
$env = [
|
|
'APP_HOST' => 'app.clupilot.test',
|
|
'SITE_HOST' => 'www.clupilot.test,clupilot.test',
|
|
'STATUS_HOST' => 'status.clupilot.test',
|
|
'FILES_HOST' => 'files.clupilot.test',
|
|
'ADMIN_HOSTS' => 'admin.clupilot.test',
|
|
'ADMIN_HOST_EXCLUSIVE' => 'true',
|
|
];
|
|
|
|
// Gemerkt und zurückgestellt, nicht bloß gelöscht: phpunit.xml setzt
|
|
// FILES_HOST und ADMIN_HOSTS für den ganzen Lauf, und ein Test, der sie
|
|
// hinterher entfernt, kippt die Tests nach ihm.
|
|
$previous = [];
|
|
foreach ($env as $key => $value) {
|
|
$previous[$key] = $_SERVER[$key] ?? null;
|
|
putenv("{$key}={$value}");
|
|
$_ENV[$key] = $value;
|
|
$_SERVER[$key] = $value;
|
|
}
|
|
|
|
$original = Container::getInstance();
|
|
|
|
try {
|
|
$app = require base_path('bootstrap/app.php');
|
|
$app->make(Kernel::class)->bootstrap();
|
|
|
|
$table = [];
|
|
foreach ($app['router']->getRoutes()->getRoutes() as $route) {
|
|
$table[] = [
|
|
'uri' => $route->uri(),
|
|
'domain' => $route->getDomain(),
|
|
'name' => $route->getName(),
|
|
];
|
|
}
|
|
} finally {
|
|
Container::setInstance($original);
|
|
Facade::clearResolvedInstances();
|
|
Facade::setFacadeApplication($original);
|
|
Model::setConnectionResolver($original['db']);
|
|
Model::setEventDispatcher($original['events']);
|
|
|
|
foreach ($previous as $key => $value) {
|
|
if ($value === null) {
|
|
putenv($key);
|
|
unset($_ENV[$key], $_SERVER[$key]);
|
|
} else {
|
|
putenv("{$key}={$value}");
|
|
$_ENV[$key] = $value;
|
|
$_SERVER[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $table;
|
|
}
|
|
|
|
/** Die Tabelle nach Routennamen, für die gezielten Prüfungen unten. */
|
|
function hostSeparationByName(): array
|
|
{
|
|
$byName = [];
|
|
|
|
foreach (hostSeparationTable() as $route) {
|
|
if ($route['name'] !== null && $route['name'] !== '') {
|
|
$byName[$route['name']] = $route;
|
|
}
|
|
}
|
|
|
|
return $byName;
|
|
}
|
|
|
|
it('bindet jede Route an einen Hostnamen, außer den hier benannten', function () {
|
|
// Jeder Eintrag mit seinem Grund. Wer hier etwas hinzufügt, schreibt den
|
|
// Grund dazu — sonst ist die Liste in einem Jahr eine Liste von Ausnahmen,
|
|
// die niemand mehr prüfen kann.
|
|
$shared = [
|
|
// Konsole und Portal posten Komponenten-Aktionen an denselben
|
|
// Endpunkt. RestrictAdminHost ist die Schutzschicht dafür; eine
|
|
// Host-Bindung hier zerlegt die Konsole.
|
|
'livewire/update',
|
|
'livewire/upload-file',
|
|
'livewire/preview-file/{filename}',
|
|
'livewire/livewire.js',
|
|
'livewire/livewire.min.js.map',
|
|
// Gesundheitsprüfung, ebenfalls RestrictAdminHost::SHARED.
|
|
'up',
|
|
// Der Live-Feed der Konsole meldet sich hier an (routes/channels.php,
|
|
// admin.runs) — ebenfalls RestrictAdminHost::SHARED.
|
|
'broadcasting/auth',
|
|
// Stripe postet an die URL, die es einmal bekommen hat. Ein
|
|
// Hostname-Fehler an dieser Stelle verliert Zahlungen.
|
|
'webhooks/stripe',
|
|
// "/" auf einem Namen, der weder Website noch Portal noch Status ist:
|
|
// eine Weiterleitung ins Portal, kein 404. Wer "/" aus dem Gedächtnis
|
|
// tippt, soll nicht auf einer Fehlerseite über Hostnamen belehrt werden.
|
|
'/',
|
|
// Weiterleitung auf den Statushost. Eine Statusseite ist die Adresse,
|
|
// die in einem Lesezeichen steht, wenn ohnehin schon etwas kaputt ist.
|
|
'status',
|
|
];
|
|
|
|
$unbound = [];
|
|
foreach (hostSeparationTable() as $route) {
|
|
if ($route['domain'] === null) {
|
|
$unbound[] = $route['uri'];
|
|
}
|
|
}
|
|
|
|
$unbound = array_values(array_unique($unbound));
|
|
sort($unbound);
|
|
sort($shared);
|
|
|
|
expect($unbound)->toBe($shared);
|
|
});
|
|
|
|
it('legt jeden Weg in ein Konto hinein und wieder heraus auf den Portal-Hostnamen', function () {
|
|
// Kriterium 4, und die teuerste Falle des ganzen Umbaus: Fortify meldet
|
|
// diese Routen SELBST an, nicht routes/web.php. Gebunden werden sie über
|
|
// config/fortify.php ('domain' => env('APP_HOST')). Verschwindet diese eine
|
|
// Zeile beim Aufräumen, kann sich auf app. niemand mehr anmelden — und
|
|
// routes/web.php sieht dabei völlig unverdächtig aus.
|
|
$byName = hostSeparationByName();
|
|
|
|
$mustBeOnThePortal = [
|
|
'login', 'login.store', 'logout',
|
|
'password.request', 'password.email',
|
|
'password.reset', 'password.update',
|
|
'two-factor.login', 'two-factor.login.store',
|
|
'password.confirm.store',
|
|
'verification.notice', 'verification.verify',
|
|
];
|
|
|
|
foreach ($mustBeOnThePortal as $name) {
|
|
expect($byName[$name]['domain'] ?? null)
|
|
->toBe('app.clupilot.test', "Route [{$name}] liegt nicht auf dem Portal-Hostnamen.");
|
|
}
|
|
});
|
|
|
|
it('behält die Namen, an denen PublicSiteGate die Downloads erkennt', function () {
|
|
// PublicSiteGate::isDownload() prüft auf ROUTENNAMEN. Zieht eine Route um
|
|
// und ändert dabei ihren Namen, fällt die Ausnahme still weg — und ein
|
|
// Server im Rettungssystem schiebt die 503-Platzhalterseite in `tar`.
|
|
$byName = hostSeparationByName();
|
|
|
|
expect($byName['bootstrap.archive']['domain'] ?? null)->toBe('files.clupilot.test')
|
|
->and($byName['files.public']['domain'] ?? null)->toBe('files.clupilot.test');
|
|
|
|
$gate = file_get_contents(base_path('app/Http/Middleware/PublicSiteGate.php'));
|
|
|
|
expect($gate)->toContain("'bootstrap.archive'")
|
|
->and($gate)->toContain("'files.public'");
|
|
});
|