Fix round: name the slot on the card, and cover the modal that already worked
Names which of the two slots a card actually reads and writes right now, at the card an operator types into (not only the page-top badge) — save() and render() resolve without a mode argument, so the card looked identical in both modes while quietly acting on whichever slot was active. Also adds the ConfirmSwitchMode coverage IntegrationsPageTest already carries for ConfirmSaveSecret/ConfirmForgetSecret: a modal is reachable without the page's route middleware, and reading the code is not the same as testing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feature/betriebsmodus
parent
96f171b3b3
commit
fe1110de71
|
|
@ -22,6 +22,17 @@ return [
|
|||
'unlocked_note' => 'Entsperrt. Änderungen wirken sofort auf den laufenden Betrieb.',
|
||||
'lock_again' => 'Wieder sperren',
|
||||
|
||||
// Welcher der zwei Plätze dieses Eintrags jetzt gilt — Speichern UND
|
||||
// Lesen, weil save()/render() ohne Modus-Argument auf
|
||||
// OperatingMode::current() auflösen. Benennt den PLATZ, nicht den Modus:
|
||||
// „Testbetrieb"/„Livebetrieb" steht schon in der Plakette oben auf der
|
||||
// Seite, hier geht es darum, in welche der zwei Zeilen ein neuer Wert
|
||||
// tatsächlich landet.
|
||||
'slot' => [
|
||||
'test' => 'Testplatz aktiv',
|
||||
'live' => 'Live-Platz aktiv',
|
||||
],
|
||||
|
||||
'source_stored' => 'Hier hinterlegt',
|
||||
'source_environment' => 'Aus der Serverdatei',
|
||||
'source_none' => 'Nicht gesetzt',
|
||||
|
|
|
|||
|
|
@ -22,6 +22,17 @@ return [
|
|||
'unlocked_note' => 'Unlocked. Changes take effect on the running system immediately.',
|
||||
'lock_again' => 'Lock again',
|
||||
|
||||
// Which of the two slots this entry resolves to right now — for both
|
||||
// saving and reading, since save()/render() resolve against
|
||||
// OperatingMode::current() without a mode argument. Names the SLOT, not
|
||||
// the mode: "Test mode"/"Live mode" already sits in the badge at the top
|
||||
// of the page; this is about which of the two rows a new value actually
|
||||
// lands in.
|
||||
'slot' => [
|
||||
'test' => 'Test slot active',
|
||||
'live' => 'Live slot active',
|
||||
],
|
||||
|
||||
'source_stored' => 'Stored here',
|
||||
'source_environment' => 'From the server file',
|
||||
'source_none' => 'Not set',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@props(['entry', 'unlocked'])
|
||||
@props(['entry', 'unlocked', 'mode'])
|
||||
{{--
|
||||
One vault entry — outline, new-value field, test/save/forget — reusable
|
||||
across every integration section on App\Livewire\Admin\Integrations. Kept
|
||||
|
|
@ -17,13 +17,28 @@
|
|||
<h3 class="text-sm font-semibold text-ink">{{ $entry['label'] }}</h3>
|
||||
<p class="mt-0.5 font-mono text-xs text-muted">{{ $entry['key'] }}</p>
|
||||
</div>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-pill border px-2.5 py-0.5 text-xs font-medium
|
||||
{{ $entry['source'] === 'stored' ? 'border-success-border bg-success-bg text-success'
|
||||
: ($entry['source'] === 'environment' ? 'border-info-border bg-info-bg text-info'
|
||||
: 'border-warning-border bg-warning-bg text-warning') }}">
|
||||
<span class="size-1.5 rounded-pill bg-current" aria-hidden="true"></span>
|
||||
{{ __('secrets.source_'.$entry['source']) }}
|
||||
</span>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{{-- Which of the two slots THIS card reads and writes right now —
|
||||
fix round, Important: save()/render() resolve without a mode
|
||||
argument, so they always act on OperatingMode::current()'s
|
||||
slot, but nothing on the card said so. The badge in the
|
||||
header above (Integrations::mode_title) is not enough: it is
|
||||
not visible at the card an operator is about to type into,
|
||||
and repeating "Testbetrieb"/"Livebetrieb" there would only
|
||||
restate the MODE, not name the PLATZ a value lands in. Same
|
||||
colours as that header badge, for one visual language. --}}
|
||||
<span class="inline-flex items-center gap-1.5 rounded-pill border px-2.5 py-0.5 text-xs font-medium
|
||||
{{ $mode->isTest() ? 'border-warning-border bg-warning-bg text-warning' : 'border-success-border bg-success-bg text-success' }}">
|
||||
{{ __('secrets.slot.'.$mode->value) }}
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-pill border px-2.5 py-0.5 text-xs font-medium
|
||||
{{ $entry['source'] === 'stored' ? 'border-success-border bg-success-bg text-success'
|
||||
: ($entry['source'] === 'environment' ? 'border-info-border bg-info-bg text-info'
|
||||
: 'border-warning-border bg-warning-bg text-warning') }}">
|
||||
<span class="size-1.5 rounded-pill bg-current" aria-hidden="true"></span>
|
||||
{{ __('secrets.source_'.$entry['source']) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (! $unlocked)
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@
|
|||
<h2 class="font-semibold text-ink">{{ __('integrations.payments_title') }}</h2>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('integrations.payments_body') }}</p>
|
||||
</div>
|
||||
<x-admin.secret-field :entry="$entries['stripe.secret']" :unlocked="$unlocked" />
|
||||
<x-admin.secret-field :entry="$entries['stripe.secret']" :unlocked="$unlocked" :mode="$mode" />
|
||||
@if ($unlocked)
|
||||
<x-ui.alert variant="info">{{ __('secrets.webhook_secret_note') }}</x-ui.alert>
|
||||
@endif
|
||||
|
|
@ -127,7 +127,7 @@
|
|||
|
||||
@if ($canSecrets)
|
||||
@if ($canInfra)<hr class="border-line" />@endif
|
||||
<x-admin.secret-field :entry="$entries['dns.token']" :unlocked="$unlocked" />
|
||||
<x-admin.secret-field :entry="$entries['dns.token']" :unlocked="$unlocked" :mode="$mode" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -153,7 +153,7 @@
|
|||
|
||||
@if ($canSecrets)
|
||||
@if ($canInfra)<hr class="border-line" />@endif
|
||||
<x-admin.secret-field :entry="$entries['inbound_mail.password']" :unlocked="$unlocked" />
|
||||
<x-admin.secret-field :entry="$entries['inbound_mail.password']" :unlocked="$unlocked" :mode="$mode" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -172,7 +172,7 @@
|
|||
|
||||
@if ($canSecrets)
|
||||
@if ($canInfra)<hr class="border-line" />@endif
|
||||
<x-admin.secret-field :entry="$entries['monitoring.token']" :unlocked="$unlocked" />
|
||||
<x-admin.secret-field :entry="$entries['monitoring.token']" :unlocked="$unlocked" :mode="$mode" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -218,7 +218,7 @@
|
|||
|
||||
@if ($canSecrets)
|
||||
@if ($canInfra)<hr class="border-line" />@endif
|
||||
<x-admin.secret-field :entry="$entries['ssh.private_key']" :unlocked="$unlocked" />
|
||||
<x-admin.secret-field :entry="$entries['ssh.private_key']" :unlocked="$unlocked" :mode="$mode" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Admin\ConfirmSwitchMode;
|
||||
use App\Livewire\Admin\Integrations;
|
||||
use App\Models\Operator;
|
||||
use App\Support\OperatingMode;
|
||||
|
|
@ -63,3 +64,57 @@ it('refuses an unknown mode', function () {
|
|||
|
||||
expect(OperatingMode::current())->toBe(OperatingMode::Live);
|
||||
});
|
||||
|
||||
// ---- ConfirmSwitchMode selbst — nach dem Vorbild der ConfirmSaveSecret/
|
||||
// ConfirmForgetSecret-Tests in tests/Feature/Admin/IntegrationsPageTest.php.
|
||||
// Ein Modal ist OHNE die Route-Middleware der Seite erreichbar (R23-
|
||||
// Kopfkommentar in CLAUDE.md) — "gelesen und für korrekt befunden" ist keine
|
||||
// Prüfung, ein eigener Test schon.
|
||||
it('does not open the switch confirmation without the capability', function () {
|
||||
$admin = Operator::factory()->role('Admin')->create(); // hosts.manage, nicht secrets.manage
|
||||
|
||||
Livewire::actingAs($admin, 'operator')
|
||||
->test(ConfirmSwitchMode::class, ['mode' => 'test'])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('confirms a switch through the modal without changing the mode itself', function () {
|
||||
$owner = Operator::factory()->role('Owner')->create();
|
||||
|
||||
Livewire::actingAs($owner, 'operator')
|
||||
->test(ConfirmSwitchMode::class, ['mode' => 'test'])
|
||||
->call('confirm')
|
||||
->assertDispatched('mode-switch-confirmed', mode: 'test');
|
||||
|
||||
// Das Modal selbst ruft OperatingMode::set() nirgends auf — nur
|
||||
// Integrations::switchMode() darf das, hinter guardSecrets(). Bestätigen
|
||||
// im Modal allein darf den Modus nicht bewegen.
|
||||
expect(OperatingMode::current())->toBe(OperatingMode::Live);
|
||||
});
|
||||
|
||||
// ---- Fix-Runde, Befund 1 (Important): keine Karte sagte, in welchen Platz
|
||||
// sie schreibt. save()/render() lösen ohne Modus-Argument gegen
|
||||
// OperatingMode::current() auf — funktional kein Problem, aber die Karte sah
|
||||
// in Test und Live identisch aus. Reine Darstellung (secret-field.blade.php
|
||||
// liest nur $mode, das Integrations::render() schon berechnet — kein neuer
|
||||
// Aufruf von source()/outline()/updatedAt()), deshalb genügt ein assertSee je
|
||||
// Modus statt eines erfundenen Verhaltens-Tests.
|
||||
it('names the slot a saved value would land in, on the card itself', function () {
|
||||
$owner = Operator::factory()->role('Owner')->create();
|
||||
|
||||
Livewire::actingAs($owner, 'operator')
|
||||
->test(Integrations::class)
|
||||
->set('confirmablePassword', 'passwort-fuer-tests')
|
||||
->call('confirmPassword')
|
||||
->assertSee(__('secrets.slot.live'))
|
||||
->assertDontSee(__('secrets.slot.test'));
|
||||
|
||||
OperatingMode::set(OperatingMode::Test);
|
||||
|
||||
Livewire::actingAs($owner, 'operator')
|
||||
->test(Integrations::class)
|
||||
->set('confirmablePassword', 'passwort-fuer-tests')
|
||||
->call('confirmPassword')
|
||||
->assertSee(__('secrets.slot.test'))
|
||||
->assertDontSee(__('secrets.slot.live'));
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue