167 lines
7.0 KiB
PHP
167 lines
7.0 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Admin\Settings;
|
|
use App\Support\Navigation;
|
|
use Livewire\Livewire;
|
|
|
|
/**
|
|
* The settings page is tabbed, and the tab survives a reload.
|
|
*
|
|
* It had grown to three headed sections in one column with the operator's own
|
|
* password somewhere in the middle. Which part somebody wants is a property of
|
|
* what they came to do, not of how far they scroll — and the installation tab
|
|
* refreshes itself while an update runs, so a tab kept only in the component
|
|
* would drop them back to the first one mid-deployment.
|
|
*/
|
|
it('opens the installation tab by default', function () {
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(Settings::class)
|
|
->assertSet('tab', 'installation')
|
|
->assertSee(__('admin_settings.update_title'));
|
|
});
|
|
|
|
it('remembers the tab across a reload, because it is in the URL', function () {
|
|
// The whole point of #[Url]. Without it a refresh — or the browser's back
|
|
// button — lands on the first tab every time.
|
|
Livewire::withQueryParams(['tab' => 'account'])
|
|
->actingAs(operator('Owner'), 'operator')
|
|
->test(Settings::class)
|
|
->assertSet('tab', 'account')
|
|
->assertSee(__('admin_settings.account_title'))
|
|
->assertDontSee(__('admin_settings.update_title'));
|
|
});
|
|
|
|
it('falls back to the first tab when the URL names one that does not exist', function () {
|
|
// A query string is a string a stranger typed. Anything unknown must not
|
|
// render a settings page with no section on it at all.
|
|
Livewire::withQueryParams(['tab' => 'wat'])
|
|
->actingAs(operator('Owner'), 'operator')
|
|
->test(Settings::class)
|
|
->assertSet('tab', 'installation')
|
|
->assertSee(__('admin_settings.update_title'));
|
|
});
|
|
|
|
it('carries two-factor enrolment as a tab rather than a page of its own', function () {
|
|
Livewire::withQueryParams(['tab' => 'two-factor'])
|
|
->actingAs(operator('Owner'), 'operator')
|
|
->test(Settings::class)
|
|
->assertSet('tab', 'two-factor')
|
|
->assertSee(__('two_factor_setup.title'));
|
|
});
|
|
|
|
it('does not offer the team tab to somebody who cannot manage staff', function () {
|
|
// A tab that raises a 403 is worse than one that is not there — and an
|
|
// operator who reaches ?tab=team anyway lands on the first tab rather than
|
|
// on a page with nothing on it.
|
|
$support = operator('Support');
|
|
|
|
expect($support->can('staff.manage'))->toBeFalse();
|
|
|
|
Livewire::withQueryParams(['tab' => 'team'])
|
|
->actingAs($support, 'operator')
|
|
->test(Settings::class)
|
|
->assertSet('tab', 'installation')
|
|
->assertViewHas('tabs', fn (array $tabs) => ! in_array('team', $tabs, true));
|
|
});
|
|
|
|
it('lists every tab it can render, and renders every tab it lists', function () {
|
|
// The const IS the schema: it validates the query string, builds the bar
|
|
// and decides what renders. A tab in the list with no label is a button
|
|
// that says "admin_settings.tab.x".
|
|
foreach (Settings::TABS as $tab) {
|
|
$key = 'admin_settings.tab.'.str_replace('-', '_', $tab);
|
|
|
|
expect(__($key))->not->toBe($key);
|
|
}
|
|
});
|
|
|
|
it('moves the tunnel to System and drops the two-factor entry from the menu', function () {
|
|
// VPN is not a task of the day's operations — it is how the console reaches
|
|
// the estate at all, so it belongs beside the other things that have to
|
|
// work first. Two-factor left the menu entirely: it is a tab now.
|
|
$groups = collect(Navigation::console());
|
|
|
|
$routesIn = fn (string $label) => $groups
|
|
->firstWhere('label', __($label))['items'] ?? [];
|
|
|
|
$system = collect($routesIn('admin.nav_group.system'))->pluck(0);
|
|
$operations = collect($routesIn('admin.nav_group.operations'))->pluck(0);
|
|
|
|
expect($system)->toContain('admin.vpn')
|
|
->and($operations)->not->toContain('admin.vpn')
|
|
->and($groups->pluck('items')->flatten(1)->pluck(0))
|
|
->not->toContain('admin.two-factor-setup');
|
|
});
|
|
|
|
it('keeps the standalone enrolment route, because the gate sends people there', function () {
|
|
// RequireOperatorTwoFactor redirects an operator who has not enrolled yet,
|
|
// and that operator may not open anything else — the settings page
|
|
// included. Folding the page away entirely would have locked them out.
|
|
expect(route('admin.two-factor-setup'))->toBeString();
|
|
|
|
$this->actingAs(operator('Owner'), 'operator')
|
|
->get(route('admin.two-factor-setup'))
|
|
->assertOk()
|
|
->assertSee(__('two_factor_setup.title'));
|
|
});
|
|
|
|
it('tabs the integrations page the same way, and keeps the raw file to the Owner', function () {
|
|
// Same treatment, same reason: one narrow column of six cards had an
|
|
// operator scrolling past Stripe to reach DNS, with the raw .env editor at
|
|
// the bottom of everything.
|
|
$owner = operator('Owner');
|
|
|
|
Livewire::actingAs($owner, 'operator')
|
|
->test(App\Livewire\Admin\Integrations::class)
|
|
->assertSet('tab', 'services')
|
|
->assertViewHas('tabs', fn (array $tabs) => in_array('env', $tabs, true));
|
|
|
|
foreach (App\Livewire\Admin\Integrations::TABS as $tab) {
|
|
expect(__('integrations.tab.'.$tab))->not->toBe('integrations.tab.'.$tab);
|
|
}
|
|
|
|
// An Admin can reach the page for the DNS zone but not the file that holds
|
|
// every credential on the machine.
|
|
$admin = operator('Admin');
|
|
|
|
expect($admin->can('hosts.manage'))->toBeTrue()
|
|
->and($admin->can('secrets.manage'))->toBeFalse();
|
|
|
|
Livewire::withQueryParams(['tab' => 'env'])
|
|
->actingAs($admin, 'operator')
|
|
->test(App\Livewire\Admin\Integrations::class)
|
|
->assertSet('tab', 'services')
|
|
->assertViewHas('tabs', fn (array $tabs) => ! in_array('env', $tabs, true));
|
|
});
|
|
|
|
it('puts the open tab in the address bar from the first render, on every tabbed page', function () {
|
|
// Reported as "die Tabs fehlen in der Query beim Kunden". The binding was
|
|
// there, but `except` hid the parameter while the default tab was open — so
|
|
// a link to the page could not say which tab it meant unless the tab
|
|
// happened not to be the first one, and the address bar looked like the
|
|
// feature was missing.
|
|
//
|
|
// Asserted against the rendered page rather than the component: the
|
|
// parameter is written by Livewire's JS from the `url` memo in the snapshot,
|
|
// so the memo IS the behaviour.
|
|
$customer = App\Models\Customer::factory()->create();
|
|
|
|
$pages = [
|
|
route('admin.settings'),
|
|
route('admin.integrations'),
|
|
route('admin.customer', $customer->uuid),
|
|
];
|
|
|
|
foreach ($pages as $url) {
|
|
$html = html_entity_decode(
|
|
(string) $this->actingAs(operator('Owner'), 'operator')->get($url)->getContent()
|
|
);
|
|
|
|
expect($html)->toContain('"url":{"tab":')
|
|
// No `except`: shown even on the default tab.
|
|
->and($html)->toMatch('/"tab":\{[^}]*"except":null/')
|
|
// push, not replace: each tab is a step the back button can take.
|
|
->and($html)->toMatch('/"tab":\{[^}]*"use":"push"/');
|
|
}
|
|
});
|