set('admin_access.site_hosts', ['www.clupilot.com', 'clupilot.com']); config()->set('admin_access.app_host', 'app.clupilot.com'); config()->set('admin_access.status_host', 'status.clupilot.com'); Settings::set('provisioning.dns_zone', 'clupilot.cloud'); }); it('names both registrable domains, and says what each is for', function () { // Keyed by domain and valued by role, so the page never works the role out // from the order the list happens to come back in. expect(OfficialDomains::registrable())->toBe([ 'clupilot.com' => 'company', 'clupilot.cloud' => 'instances', ]); }); it('follows the configuration rather than a list somebody typed', function () { // The whole point. An installation moved to another zone must not go on // vouching for the old one. Settings::set('provisioning.dns_zone', 'kunden.example.test'); expect(array_keys(OfficialDomains::registrable()))->toContain('example.test') ->and(array_keys(OfficialDomains::registrable()))->not->toContain('clupilot.cloud'); }); it('shows the instance zone as a pattern, never one customer’s address', function () { $hosts = collect(OfficialDomains::hosts())->pluck('host'); expect($hosts)->toContain('*.clupilot.cloud'); }); it('does not print where the operator console lives', function () { // A public page listing the operator interface is a public page telling an // attacker where to aim. config()->set('admin_access.host', 'admin.clupilot.com'); $hosts = collect(OfficialDomains::hosts())->pluck('host'); expect($hosts)->not->toContain('admin.clupilot.com'); }); it('serves the page to somebody who is not signed in', function () { // Somebody who has just given their password to a copy of our sign-in form // is signed in nowhere. The page they need cannot be behind the thing they // lost. // The path is English (R13). /sicherheit shipped for two releases and // still redirects, which is asserted below. $this->get('/security') ->assertOk() ->assertSee('clupilot.com') ->assertSee('clupilot.cloud') ->assertSee(__('security.compromised_title')); // The German path went out in mail footers before the rule was applied. $this->get('/sicherheit')->assertRedirect('/security'); }); it('carries the domains in every mail footer', function () { // The mail is the vector, so the line travels with the mail. $layout = Illuminate\Support\Facades\File::get(resource_path('views/components/mail/layout.blade.php')); expect($layout)->toContain('OfficialDomains::registrable()') ->toContain("route('security')"); }); it('says on the sign-in form which host it is', function () { // This is the page a phishing kit copies, so the real one says how to tell. $this->get(route('login')) ->assertOk() ->assertSee(__('auth.phishing_link')); }); it('puts customer instances on their own registrable domain out of the box', function () { // Asked for four times, and "change it in the console" was the wrong // answer each time: an installation with nothing to migrate should simply // arrive on the new zone. The migration does it, guarded by exactly the // condition clupilot:check-zone reports on. expect(App\Support\ProvisioningSettings::dnsZone())->toBe('clupilot.cloud'); }); it('keeps every public path in English', function () { // R13, applied to ALL of them this time rather than to the one that was // pointed at. A fragment is part of a URL too, and the section anchors were // German for weeks after the rule was first cited. $routes = collect(app('router')->getRoutes()) ->map(fn ($route) => $route->uri()) ->filter(fn (string $uri) => ! str_starts_with($uri, '_')); $german = ['sicherheit', 'impressum', 'datenschutz', 'agb', 'preise', 'produkt', 'ablauf', 'fragen', 'kontakt', 'einstellungen', 'rechnungen']; foreach ($routes as $uri) { foreach ($german as $word) { // The redirects that keep old addresses alive are the exception, // and they are the only routes allowed to carry these words. $isRedirect = collect(app('router')->getRoutes()) ->first(fn ($r) => $r->uri() === $uri)?->getActionName() === 'Closure'; if (str_contains($uri, $word) && ! $isRedirect) { expect($uri)->toBe("no German path: {$uri}"); } } } // And the fragments the site links to. foreach (['landing.blade.php', 'components/layouts/site.blade.php'] as $view) { $source = Illuminate\Support\Facades\File::get(resource_path('views/'.$view)); foreach (['#produkt', '#ablauf', '#sicherheit', '#preise', '#fragen', '#kontakt'] as $fragment) { expect($source)->not->toContain($fragment.'"') ->and($source)->not->toContain($fragment."'"); } } });