127 lines
5.2 KiB
PHP
127 lines
5.2 KiB
PHP
<?php
|
||
|
||
use App\Support\OfficialDomains;
|
||
use App\Support\Settings;
|
||
|
||
/**
|
||
* Telling a customer which addresses are ours.
|
||
*
|
||
* The failure mode is specific and bad in both directions: a stale list tells
|
||
* somebody that a phishing domain is one of ours, or that one of ours is not.
|
||
* So the list is derived from configuration and nothing here is allowed to be
|
||
* a literal.
|
||
*/
|
||
beforeEach(function () {
|
||
config()->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."'");
|
||
}
|
||
}
|
||
});
|