From fe1110de718e8530f7912dbc1d9a578d17fd92b3 Mon Sep 17 00:00:00 2001 From: nexxo Date: Thu, 30 Jul 2026 15:32:16 +0200 Subject: [PATCH] Fix round: name the slot on the card, and cover the modal that already worked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lang/de/secrets.php | 11 ++++ lang/en/secrets.php | 11 ++++ .../components/admin/secret-field.blade.php | 31 ++++++++--- .../livewire/admin/integrations.blade.php | 10 ++-- tests/Feature/SwitchOperatingModeTest.php | 55 +++++++++++++++++++ 5 files changed, 105 insertions(+), 13 deletions(-) diff --git a/lang/de/secrets.php b/lang/de/secrets.php index 234f6c5..0cddd04 100644 --- a/lang/de/secrets.php +++ b/lang/de/secrets.php @@ -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', diff --git a/lang/en/secrets.php b/lang/en/secrets.php index ffe9c07..40fb833 100644 --- a/lang/en/secrets.php +++ b/lang/en/secrets.php @@ -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', diff --git a/resources/views/components/admin/secret-field.blade.php b/resources/views/components/admin/secret-field.blade.php index 9e3ec5d..7aefcb7 100644 --- a/resources/views/components/admin/secret-field.blade.php +++ b/resources/views/components/admin/secret-field.blade.php @@ -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 @@

{{ $entry['label'] }}

{{ $entry['key'] }}

- - - {{ __('secrets.source_'.$entry['source']) }} - +
+ {{-- 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. --}} + + {{ __('secrets.slot.'.$mode->value) }} + + + + {{ __('secrets.source_'.$entry['source']) }} + +
@if (! $unlocked) diff --git a/resources/views/livewire/admin/integrations.blade.php b/resources/views/livewire/admin/integrations.blade.php index 4a68714..04b335d 100644 --- a/resources/views/livewire/admin/integrations.blade.php +++ b/resources/views/livewire/admin/integrations.blade.php @@ -103,7 +103,7 @@

{{ __('integrations.payments_title') }}

{{ __('integrations.payments_body') }}

- + @if ($unlocked) {{ __('secrets.webhook_secret_note') }} @endif @@ -127,7 +127,7 @@ @if ($canSecrets) @if ($canInfra)
@endif - + @endif @endif @@ -153,7 +153,7 @@ @if ($canSecrets) @if ($canInfra)
@endif - + @endif @endif @@ -172,7 +172,7 @@ @if ($canSecrets) @if ($canInfra)
@endif - + @endif @endif @@ -218,7 +218,7 @@ @if ($canSecrets) @if ($canInfra)
@endif - + @endif @endif diff --git a/tests/Feature/SwitchOperatingModeTest.php b/tests/Feature/SwitchOperatingModeTest.php index 15e5bd0..a995dc7 100644 --- a/tests/Feature/SwitchOperatingModeTest.php +++ b/tests/Feature/SwitchOperatingModeTest.php @@ -1,5 +1,6 @@ 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')); +});