Move the zone by migration, and put every path in English
tests / pest (push) Failing after 8m8s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details

── The zone, for the fourth time ────────────────────────────────────────────
"Change it in the console" was the wrong answer three times running. An
installation with nothing to migrate should simply arrive on the new zone, so
a migration does it — guarded by exactly the condition clupilot:check-zone
reports on: no instance in service, therefore nothing carrying the old name in
DNS, in a certificate or in trusted_domains. With even one instance it does
nothing and logs why, because re-addressing a running Nextcloud is a migration
with an outage and a migration is not the place to start one. It leaves an
installation already on another zone alone: the setting exists so somebody
else can run this on their own domain.

That fixes both reports at once — the security page and the price sheet read
the same setting, which is the entire reason neither of them has the domain
typed into it.

── R13, applied to all of them this time ────────────────────────────────────
"Ich schreibe das nun zum dritten Mal" — fair. The rule was cited, I changed
the one route that had been pointed at, and left every other German path
standing. A fragment is part of a URL too.

  #produkt → #product      /legal/impressum   → /legal/imprint
  #ablauf  → #process      /legal/datenschutz → /legal/privacy
  #preise  → #pricing      /legal/agb         → /legal/terms
  #fragen  → #faq          (old paths redirect, permanently — they have been
  #kontakt → #contact       in the footer and in mail for weeks)

The headings stay German. "Impressum" is the legal term an Austrian company
has to use; the address is not part of the copy.

A test now walks every registered route and every fragment the site links to,
so the next German path fails the suite instead of being reported a fourth
time.

Three existing tests set config('provisioning.dns.zone') and read it back
through the accessor. That worked while the setting was empty and stopped the
moment the migration filled it — they set the setting now, and the one whose
premise is "nothing saved yet" establishes that premise itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feature/betriebsmodus
nexxo 2026-07-29 17:26:20 +02:00
parent 6c3cdffc4e
commit 5f74a5f370
8 changed files with 143 additions and 24 deletions

View File

@ -0,0 +1,59 @@
<?php
use App\Models\Instance;
use App\Support\ProvisioningSettings;
use App\Support\Settings;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Log;
/**
* Puts customer instances on their own registrable domain.
*
* The zone has been a setting all along, and saying "change it in the console"
* has now been said four times without the thing actually changing which is
* a fair complaint. An installation that has nothing to migrate should simply
* arrive on the new zone.
*
* Guarded by exactly the condition `clupilot:check-zone` reports on: with no
* instance in service there is nothing carrying the old name no DNS record,
* no certificate, no trusted_domains entry so the change is free. With even
* one, this does nothing and says so: re-addressing a running Nextcloud is a
* migration with an outage, and a migration is not the place to start one.
*/
return new class extends Migration
{
private const OLD = 'clupilot.com';
private const NEW = 'clupilot.cloud';
public function up(): void
{
$current = ProvisioningSettings::dnsZone();
// Somebody else's installation, or one already moved. Not ours to
// touch — the whole point of the zone being a setting is that another
// operator may run this on their own domain.
if ($current !== self::OLD) {
return;
}
$inService = Instance::query()
->whereIn('status', ['active', 'provisioning', 'cancellation_scheduled'])
->count();
if ($inService > 0) {
Log::warning('Instance zone left on '.self::OLD.': '.$inService.' instance(s) in service carry it in DNS, certificates and trusted_domains. Run clupilot:check-zone and plan a window.');
return;
}
Settings::set('provisioning.dns_zone', self::NEW);
}
public function down(): void
{
if (ProvisioningSettings::dnsZone() === self::NEW) {
Settings::set('provisioning.dns_zone', self::OLD);
}
}
};

View File

@ -42,14 +42,14 @@
<a href="{{ route('home') }}"><x-ui.brand size="md" /></a> <a href="{{ route('home') }}"><x-ui.brand size="md" /></a>
<nav class="hidden items-center gap-1 text-sm text-muted lg:flex"> <nav class="hidden items-center gap-1 text-sm text-muted lg:flex">
@foreach (['#produkt' => 'Produkt', '#ablauf' => 'Ablauf', '#sicherheit' => 'Sicherheit', '#preise' => 'Preise', '#fragen' => 'Fragen'] as $href => $label) @foreach (['#product' => 'Produkt', '#process' => 'Ablauf', '#security' => 'Sicherheit', '#pricing' => 'Preise', '#faq' => 'Fragen'] as $href => $label)
<a href="{{ route('home') }}{{ $href }}" class="rounded px-3 py-1.5 transition-colors hover:bg-surface-hover hover:text-ink">{{ $label }}</a> <a href="{{ route('home') }}{{ $href }}" class="rounded px-3 py-1.5 transition-colors hover:bg-surface-hover hover:text-ink">{{ $label }}</a>
@endforeach @endforeach
</nav> </nav>
<div class="ml-auto flex items-center gap-2"> <div class="ml-auto flex items-center gap-2">
<a href="{{ route('login') }}" class="hidden rounded px-3 py-1.5 text-sm text-muted transition-colors hover:bg-surface-hover hover:text-ink sm:block">Anmelden</a> <a href="{{ route('login') }}" class="hidden rounded px-3 py-1.5 text-sm text-muted transition-colors hover:bg-surface-hover hover:text-ink sm:block">Anmelden</a>
<x-ui.button href="{{ route('home') }}#preise" variant="ink" size="sm">Preise</x-ui.button> <x-ui.button href="{{ route('home') }}#pricing" variant="ink" size="sm">Preise</x-ui.button>
<button type="button" class="grid size-9 place-items-center rounded text-muted transition-colors hover:bg-surface-hover hover:text-ink lg:hidden" @click="open = !open" :aria-expanded="open" aria-label="Menü"> <button type="button" class="grid size-9 place-items-center rounded text-muted transition-colors hover:bg-surface-hover hover:text-ink lg:hidden" @click="open = !open" :aria-expanded="open" aria-label="Menü">
<x-ui.icon name="menu" class="size-5" x-show="!open" /> <x-ui.icon name="menu" class="size-5" x-show="!open" />
<x-ui.icon name="x" class="size-5" x-show="open" x-cloak /> <x-ui.icon name="x" class="size-5" x-show="open" x-cloak />
@ -58,7 +58,7 @@
</div> </div>
<nav x-show="open" x-cloak @click="open = false" class="border-t border-line px-4 pb-2 lg:hidden"> <nav x-show="open" x-cloak @click="open = false" class="border-t border-line px-4 pb-2 lg:hidden">
@foreach (['#produkt' => 'Produkt', '#ablauf' => 'Ablauf', '#sicherheit' => 'Sicherheit', '#preise' => 'Preise', '#fragen' => 'Fragen', route('login') => 'Anmelden'] as $href => $label) @foreach (['#product' => 'Produkt', '#process' => 'Ablauf', '#security' => 'Sicherheit', '#pricing' => 'Preise', '#faq' => 'Fragen', route('login') => 'Anmelden'] as $href => $label)
<a href="{{ Str::startsWith($href, '#') ? route('home').$href : $href }}" class="flex min-h-11 items-center border-b border-line text-sm text-body last:border-0">{{ $label }}</a> <a href="{{ Str::startsWith($href, '#') ? route('home').$href : $href }}" class="flex min-h-11 items-center border-b border-line text-sm text-body last:border-0">{{ $label }}</a>
@endforeach @endforeach
</nav> </nav>
@ -86,10 +86,10 @@
<div> <div>
<p class="lbl">Produkt</p> <p class="lbl">Produkt</p>
<div class="mt-4 flex flex-col gap-2.5 text-sm text-muted"> <div class="mt-4 flex flex-col gap-2.5 text-sm text-muted">
<a href="{{ route('home') }}#produkt" class="transition-colors hover:text-ink">Was enthalten ist</a> <a href="{{ route('home') }}#product" class="transition-colors hover:text-ink">Was enthalten ist</a>
<a href="{{ route('home') }}#ablauf" class="transition-colors hover:text-ink">Ablauf</a> <a href="{{ route('home') }}#process" class="transition-colors hover:text-ink">Ablauf</a>
<a href="{{ route('home') }}#sicherheit" class="transition-colors hover:text-ink">Sicherheit</a> <a href="{{ route('home') }}#security" class="transition-colors hover:text-ink">Sicherheit</a>
<a href="{{ route('home') }}#preise" class="transition-colors hover:text-ink">Preise</a> <a href="{{ route('home') }}#pricing" class="transition-colors hover:text-ink">Preise</a>
</div> </div>
</div> </div>

View File

@ -25,7 +25,7 @@
<div class="pointer-events-none absolute -top-56 left-1/2 size-[720px] -translate-x-1/2 rounded-pill bg-accent opacity-[0.09] blur-3xl" aria-hidden="true"></div> <div class="pointer-events-none absolute -top-56 left-1/2 size-[720px] -translate-x-1/2 rounded-pill bg-accent opacity-[0.09] blur-3xl" aria-hidden="true"></div>
<div class="relative mx-auto max-w-[1120px] px-5 pt-36 text-center sm:px-6 lg:pt-44"> <div class="relative mx-auto max-w-[1120px] px-5 pt-36 text-center sm:px-6 lg:pt-44">
<a href="#sicherheit" class="lbl inline-flex items-center gap-2 rounded-pill border border-line bg-surface px-3.5 py-2 shadow-xs transition-colors hover:border-accent-border"> <a href="#security" class="lbl inline-flex items-center gap-2 rounded-pill border border-line bg-surface px-3.5 py-2 shadow-xs transition-colors hover:border-accent-border">
<span class="relative flex size-1.5"> <span class="relative flex size-1.5">
<span class="absolute inline-flex size-full animate-ping rounded-pill bg-success-bright opacity-60"></span> <span class="absolute inline-flex size-full animate-ping rounded-pill bg-success-bright opacity-60"></span>
<span class="relative inline-flex size-1.5 rounded-pill bg-success-bright"></span> <span class="relative inline-flex size-1.5 rounded-pill bg-success-bright"></span>
@ -42,8 +42,8 @@
</p> </p>
<div class="mt-9 flex flex-wrap justify-center gap-3"> <div class="mt-9 flex flex-wrap justify-center gap-3">
<x-ui.button href="#preise" variant="ink" size="lg">Preise ansehen</x-ui.button> <x-ui.button href="#pricing" variant="ink" size="lg">Preise ansehen</x-ui.button>
<x-ui.button href="#kontakt" variant="secondary" size="lg">Erstgespräch vereinbaren</x-ui.button> <x-ui.button href="#contact" variant="secondary" size="lg">Erstgespräch vereinbaren</x-ui.button>
</div> </div>
<p class="mt-4 text-sm text-muted">Fixer Preis pro Firma · keine Abrechnung pro Kopf</p> <p class="mt-4 text-sm text-muted">Fixer Preis pro Firma · keine Abrechnung pro Kopf</p>
</div> </div>
@ -150,7 +150,7 @@
Six equal cards in two rows was the old page's answer to every section. Six equal cards in two rows was the old page's answer to every section.
Tiles of different weight let the important thing be bigger than the rest, Tiles of different weight let the important thing be bigger than the rest,
which is the whole point of having a layout. --}} which is the whole point of having a layout. --}}
<section id="produkt" class="mx-auto max-w-[1120px] px-5 py-24 sm:px-6 lg:py-32"> <section id="product" class="mx-auto max-w-[1120px] px-5 py-24 sm:px-6 lg:py-32">
<div class="rv max-w-[46ch]"> <div class="rv max-w-[46ch]">
<p class="lbl !text-accent-text">Was enthalten ist</p> <p class="lbl !text-accent-text">Was enthalten ist</p>
<h2 class="mt-3 text-[clamp(2rem,4vw,3.1rem)] font-bold leading-[1.06] tracking-[-0.035em] text-ink"> <h2 class="mt-3 text-[clamp(2rem,4vw,3.1rem)] font-bold leading-[1.06] tracking-[-0.035em] text-ink">
@ -260,7 +260,7 @@
</section> </section>
{{-- ════ The dark hinge how it arrives ════════════════════════════════ --}} {{-- ════ The dark hinge how it arrives ════════════════════════════════ --}}
<section id="ablauf" class="bg-plate py-24 text-plate-text lg:py-32"> <section id="process" class="bg-plate py-24 text-plate-text lg:py-32">
<div class="mx-auto max-w-[1120px] px-5 sm:px-6"> <div class="mx-auto max-w-[1120px] px-5 sm:px-6">
{{-- No clock anywhere in this section any more. {{-- No clock anywhere in this section any more.
"In 20 Minuten betriebsbereit", a log with minute marks down the "In 20 Minuten betriebsbereit", a log with minute marks down the
@ -341,7 +341,7 @@
A three-column table was the old page's version of this. What a visitor A three-column table was the old page's version of this. What a visitor
needs from it is the pairing measure, rhythm, and the piece of paper needs from it is the pairing measure, rhythm, and the piece of paper
they can hand to somebody so the pairing is what the row shows. --}} they can hand to somebody so the pairing is what the row shows. --}}
<section id="sicherheit" class="mx-auto max-w-[1120px] px-5 py-24 sm:px-6 lg:py-32"> <section id="security" class="mx-auto max-w-[1120px] px-5 py-24 sm:px-6 lg:py-32">
<div class="rv max-w-[46ch]"> <div class="rv max-w-[46ch]">
<p class="lbl !text-accent-text">Sicherheit &amp; Nachweis</p> <p class="lbl !text-accent-text">Sicherheit &amp; Nachweis</p>
<h2 class="mt-3 text-[clamp(2rem,4vw,3.1rem)] font-bold leading-[1.06] tracking-[-0.035em] text-ink"> <h2 class="mt-3 text-[clamp(2rem,4vw,3.1rem)] font-bold leading-[1.06] tracking-[-0.035em] text-ink">
@ -456,7 +456,7 @@
Cards, with the matrix folded away underneath. The matrix was the first Cards, with the matrix folded away underneath. The matrix was the first
thing a visitor met on the old page: twelve rows of ticks before a single thing a visitor met on the old page: twelve rows of ticks before a single
price was legible. --}} price was legible. --}}
<section id="preise" class="mx-auto max-w-[1120px] px-5 py-24 sm:px-6 lg:py-32"> <section id="pricing" class="mx-auto max-w-[1120px] px-5 py-24 sm:px-6 lg:py-32">
<div class="rv mx-auto max-w-[46ch] text-center"> <div class="rv mx-auto max-w-[46ch] text-center">
<p class="lbl !text-accent-text">Preise</p> <p class="lbl !text-accent-text">Preise</p>
<h2 class="mt-3 text-[clamp(2rem,4vw,3.1rem)] font-bold leading-[1.06] tracking-[-0.035em] text-ink"> <h2 class="mt-3 text-[clamp(2rem,4vw,3.1rem)] font-bold leading-[1.06] tracking-[-0.035em] text-ink">
@ -475,7 +475,7 @@
<p class="mx-auto mt-3 max-w-[52ch] text-sm leading-relaxed text-muted"> <p class="mx-auto mt-3 max-w-[52ch] text-sm leading-relaxed text-muted">
Die Paketübersicht steht gerade nicht zur Verfügung. Wir nennen Ihnen die aktuellen Konditionen gerne direkt. Die Paketübersicht steht gerade nicht zur Verfügung. Wir nennen Ihnen die aktuellen Konditionen gerne direkt.
</p> </p>
<x-ui.button href="#kontakt" variant="ink" size="lg" class="mt-7">Preise anfragen</x-ui.button> <x-ui.button href="#contact" variant="ink" size="lg" class="mt-7">Preise anfragen</x-ui.button>
</div> </div>
@else @else
@php @php
@ -540,7 +540,7 @@
@endif @endif
</ul> </ul>
<x-ui.button href="#kontakt" :variant="$plan['recommended'] ? 'ink' : 'secondary'" class="mt-7 w-full"> <x-ui.button href="#contact" :variant="$plan['recommended'] ? 'ink' : 'secondary'" class="mt-7 w-full">
{{ $plan['name'] }} anfragen {{ $plan['name'] }} anfragen
</x-ui.button> </x-ui.button>
</div> </div>
@ -669,7 +669,7 @@
</section> </section>
{{-- ════ Questions ══════════════════════════════════════════════════════ --}} {{-- ════ Questions ══════════════════════════════════════════════════════ --}}
<section id="fragen" class="border-t border-line bg-surface-2 py-24 lg:py-32"> <section id="faq" class="border-t border-line bg-surface-2 py-24 lg:py-32">
<div class="mx-auto grid max-w-[1120px] gap-12 px-5 sm:px-6 lg:grid-cols-[.75fr_1.25fr] lg:gap-20"> <div class="mx-auto grid max-w-[1120px] gap-12 px-5 sm:px-6 lg:grid-cols-[.75fr_1.25fr] lg:gap-20">
<div class="rv lg:sticky lg:top-28 lg:self-start"> <div class="rv lg:sticky lg:top-28 lg:self-start">
<p class="lbl !text-accent-text">Häufige Fragen</p> <p class="lbl !text-accent-text">Häufige Fragen</p>
@ -709,7 +709,7 @@
{{-- ════ Close ══════════════════════════════════════════════════════════ {{-- ════ Close ══════════════════════════════════════════════════════════
The one large accent area. Orange appears in few places on this page, and The one large accent area. Orange appears in few places on this page, and
this is allowed to be all of it. --}} this is allowed to be all of it. --}}
<section id="kontakt" class="bg-accent-active"> <section id="contact" class="bg-accent-active">
<div class="mx-auto max-w-[1120px] px-5 py-24 text-center sm:px-6 lg:py-32"> <div class="mx-auto max-w-[1120px] px-5 py-24 text-center sm:px-6 lg:py-32">
{{-- The old headline asked who you call when a folder is missing on a {{-- The old headline asked who you call when a folder is missing on a
Monday morning. It promised the wrong thing: what happens inside a Monday morning. It promised the wrong thing: what happens inside a
@ -736,7 +736,7 @@
<a href="mailto:office&#64;clupilot.com" class="inline-flex min-h-[46px] items-center justify-center gap-2 rounded-[10px] bg-white px-[22px] text-[15px] font-semibold leading-none text-accent-active transition hover:bg-accent-subtle"> <a href="mailto:office&#64;clupilot.com" class="inline-flex min-h-[46px] items-center justify-center gap-2 rounded-[10px] bg-white px-[22px] text-[15px] font-semibold leading-none text-accent-active transition hover:bg-accent-subtle">
<x-ui.icon name="mail" class="size-4" />Erstgespräch anfragen <x-ui.icon name="mail" class="size-4" />Erstgespräch anfragen
</a> </a>
<a href="#preise" class="inline-flex min-h-[46px] items-center justify-center rounded-[10px] border border-white/40 px-[22px] text-[15px] font-semibold leading-none text-white transition hover:bg-white/10"> <a href="#pricing" class="inline-flex min-h-[46px] items-center justify-center rounded-[10px] border border-white/40 px-[22px] text-[15px] font-semibold leading-none text-white transition hover:bg-white/10">
Preise ansehen Preise ansehen
</a> </a>
</div> </div>

View File

@ -203,9 +203,18 @@ $publicSite = function () {
// what makes route('legal.impressum') generate a www URL from a queued // what makes route('legal.impressum') generate a www URL from a queued
// mail, where there is no request to take a hostname from. // mail, where there is no request to take a hostname from.
Route::prefix('legal')->name('legal.')->group(function () { Route::prefix('legal')->name('legal.')->group(function () {
Route::get('/impressum', fn () => view('legal', ['title' => 'Impressum']))->name('impressum'); // R13: paths are English, headings are not. The page still says
Route::get('/datenschutz', fn () => view('legal', ['title' => 'Datenschutz']))->name('datenschutz'); // "Impressum" — that is the legal term an Austrian company must use —
Route::get('/agb', fn () => view('legal', ['title' => 'AGB']))->name('agb'); // but the address is not part of the copy.
Route::get('/imprint', fn () => view('legal', ['title' => 'Impressum']))->name('impressum');
Route::get('/privacy', fn () => view('legal', ['title' => 'Datenschutz']))->name('datenschutz');
Route::get('/terms', fn () => view('legal', ['title' => 'AGB']))->name('agb');
// The German paths these shipped under. Permanent, because they have
// been in mail footers and in the site footer for weeks.
foreach (['impressum' => 'impressum', 'datenschutz' => 'datenschutz', 'agb' => 'agb'] as $old => $to) {
Route::get('/'.$old, fn () => redirect()->route('legal.'.$to, status: 301));
}
// Kept so existing links and bookmarks do not 404 — permanently, // Kept so existing links and bookmarks do not 404 — permanently,
// because the page has genuinely moved. // because the page has genuinely moved.
Route::get('/status', fn () => redirect()->to(route('status'), 301))->name('status'); Route::get('/status', fn () => redirect()->to(route('status'), 301))->name('status');

View File

@ -309,6 +309,10 @@ it('binds the form to a key Livewire can actually write to', function () {
// ---- Plain settings: ported from the former InfrastructureSettingsTest. ---- // ---- Plain settings: ported from the former InfrastructureSettingsTest. ----
it('loads the .env-derived value when nothing has been saved from the console yet', function () { it('loads the .env-derived value when nothing has been saved from the console yet', function () {
// The premise has to be established now: a migration moves customer
// instances onto their own zone on a fresh install, so "nothing saved" is
// no longer the state a new database is in.
App\Support\Settings::forget('provisioning.dns_zone');
config()->set('provisioning.dns.zone', 'env-zone.example'); config()->set('provisioning.dns.zone', 'env-zone.example');
Livewire::actingAs(operator('Owner'), 'operator') Livewire::actingAs(operator('Owner'), 'operator')

View File

@ -233,7 +233,10 @@ it('shows one support level per plan, and never a dash for the best of them', fu
it('shows the platform address in the shape a customer will type it, under the configured zone', function () { it('shows the platform address in the shape a customer will type it, under the configured zone', function () {
// "Eigene CluPilot-Subdomain" as a table row was meaningless twice over: // "Eigene CluPilot-Subdomain" as a table row was meaningless twice over:
// every instance gets one, and the phrase does not say what it looks like. // every instance gets one, and the phrase does not say what it looks like.
config()->set('provisioning.dns.zone', 'wolke.test'); // Through the setting, not the config: the setting is what
// ProvisioningSettings::dnsZone() reads first, and since instances moved to
// their own zone there is a row in it on every installation.
App\Support\Settings::set('provisioning.dns_zone', 'wolke.test');
$this->get('/') $this->get('/')
->assertOk() ->assertOk()

View File

@ -82,3 +82,45 @@ it('says on the sign-in form which host it is', function () {
->assertOk() ->assertOk()
->assertSee(__('auth.phishing_link')); ->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."'");
}
}
});

View File

@ -32,7 +32,9 @@ it('binds the cloud card to the real instance', function () {
$this->actingAs($user)->get(route('cloud')) $this->actingAs($user)->get(route('cloud'))
->assertOk() ->assertOk()
->assertSee('acme-ag.'.config('provisioning.dns.zone')) // real subdomain, not a fixture // Through the same accessor the page uses. Reading config() directly
// stopped matching when the zone moved into the settings table.
->assertSee('acme-ag.'.App\Support\ProvisioningSettings::dnsZone()) // real subdomain, not a fixture
->assertSee(__('billing.plan.business')) ->assertSee(__('billing.plan.business'))
->assertSee(__('billing.perf.high')); ->assertSee(__('billing.perf.high'));
}); });