CluPilotCloud/tests/Feature/RescueTunnelTest.php

132 lines
5.0 KiB
PHP

<?php
use App\Livewire\Admin\Settings;
use App\Models\Operator;
use App\Services\Deployment\UpdateChannel;
use Illuminate\Support\Facades\File;
use Livewire\Livewire;
/**
* `deploy/rescue-tunnel.sh` stand in zwei Runbooks als Handarbeit. Es läuft
* ohnehin als Dienstbenutzer und tut nichts Zerstörerisches — der Agent kann
* es also fahren wie jede andere Anfrage.
*/
beforeEach(function () {
File::ensureDirectoryExists(storage_path('app/deploy'));
File::delete(File::glob(storage_path('app/deploy/*')));
});
afterEach(function () {
File::deleteDirectory(storage_path('app/deploy'));
});
it('puts a rescue request in the same mailbox as every other kind', function () {
// Kein zweiter Weg: dieselbe Datei, derselbe Agent, derselbe Lauf.
expect(app(UpdateChannel::class)->requestRescueTunnel('chef@example.com'))->toBeTrue();
$request = json_decode(File::get(storage_path('app/deploy/update-request.json')), true);
expect($request['kind'])->toBe('rescue-tunnel')
->and($request['requested_by'])->toBe('chef@example.com');
});
it('takes only one request at a time', function () {
$channel = app(UpdateChannel::class);
expect($channel->requestRescueTunnel('chef@example.com'))->toBeTrue()
->and($channel->requestRescueTunnel('chef@example.com'))->toBeFalse();
});
it('rescues the tunnel from the console', function () {
$owner = Operator::factory()->role('Owner')->create();
Livewire::actingAs($owner, 'operator')
->test(Settings::class)
->call('rescueTunnel');
expect(File::exists(storage_path('app/deploy/update-request.json')))->toBeTrue();
});
it('refuses to rescue without site.manage', function () {
// Eine Livewire-Aktion ist ein oeffentlicher Endpunkt.
$staff = Operator::factory()->create();
Livewire::actingAs($staff, 'operator')
->test(Settings::class)
->call('rescueTunnel')
->assertForbidden();
});
it('reads the outcome without falling over when it is rubbish', function () {
File::put(storage_path('app/deploy/rescue-last-run.json'), 'kein json {');
expect(app(UpdateChannel::class)->state()['rescue_last_run'])->toBeNull();
});
it('agent knows the kind', function () {
// Der Agent muss die Art kennen, sonst liegt die Anfrage bis zum Ablauf
// im Postkasten und niemand erfaehrt warum.
expect(File::get(base_path('deploy/update-agent.sh')))
->toContain('rescue-tunnel')
->toContain('rescue-tunnel.sh');
});
it('agent passes the rescue error through instead of swallowing it', function () {
// Fix-Runde 1: `write_status idle` (ohne zweites Argument) verschluckte
// RESCUE_ERROR. Der Zweig muss den Fehler weiterreichen, wie proxy-hosts
// es daneben schon tut.
expect(File::get(base_path('deploy/update-agent.sh')))
->toContain('write_status idle "$RESCUE_ERROR"');
});
it('shapes the last rescue outcome for the view, with a local timestamp and a translated error', function () {
// Fix-Runde 1: das Feld existierte, aber nichts las es. Hier der
// Nachweis auf Datenebene, dass die Form stimmt, die das Blade braucht —
// `finished_at` als Carbon (R19: die View parst nicht selbst) und
// `error` schon uebersetzt (dieselbe errorMessage(), die auch STATUS und
// LAST_RUN uebersetzt).
File::put(storage_path('app/deploy/rescue-last-run.json'), json_encode([
'state' => 'failed',
'finished_at' => now()->utc()->toIso8601String(),
'error' => 'rescue_failed',
]));
$run = app(UpdateChannel::class)->state()['rescue_last_run'];
expect($run['state'])->toBe('failed')
->and($run['finished_at'])->toBeInstanceOf(\Illuminate\Support\Carbon::class)
->and($run['error'])->toBe(__('admin_settings.update_error.rescue_failed'));
});
it('shows a failed rescue attempt in the console, instead of silence', function () {
// Das eigentliche Befund-Szenario: Betreiber drueckt "Tunnel retten",
// bekommt "angefordert", der Versuch scheitert auf dem Wirt. Vorher
// zeigte die Konsole danach exakt nichts.
File::put(storage_path('app/deploy/rescue-last-run.json'), json_encode([
'state' => 'failed',
'finished_at' => now()->utc()->toIso8601String(),
'error' => 'rescue_failed',
]));
$owner = Operator::factory()->role('Owner')->create();
Livewire::actingAs($owner, 'operator')
->test(Settings::class)
->assertSee(__('admin_settings.update_state.failed'))
->assertSee(__('admin_settings.update_error.rescue_failed'), false)
->assertSeeHtml('text-danger');
});
it('shows a successful rescue attempt too', function () {
File::put(storage_path('app/deploy/rescue-last-run.json'), json_encode([
'state' => 'ok',
'finished_at' => now()->utc()->toIso8601String(),
'error' => '',
]));
$owner = Operator::factory()->role('Owner')->create();
Livewire::actingAs($owner, 'operator')
->test(Settings::class)
->assertSee(__('admin_settings.update_state.succeeded'));
});