A failed zone list is not an empty Hetzner account
The owner asked whether an empty token was being sent. It was not — an empty one returns `missing` — but the question was worth following, and it found a fault in the message I had just added. The check classified 401 and 403 as `rejected` and then read the body. Every OTHER unsuccessful response — 404, 429, 500, a cache's status page — has no `zones` key, `?? []` turned that into no zones, and the console then stated "this account holds no zones at all. The token probably belongs to a different Hetzner project." A claim about somebody's account, derived from an error nobody looked at, delivered with more confidence than the working case gets. `successful()` is asked first now, and an unexpected status is reported as what it is, with the number beside it: it says nothing about the zones, and it says so. That is the distinction this check already draws between `unreachable` and `read_only` — both are failures, only one of them tells you anything about the token. A genuinely empty list still means what it meant: 200 with zero zones is a token for a project without zones. The test uses a dataset rather than a loop. Http::fake() ADDS stubs instead of replacing them, so a loop would have had the first status answer all four iterations and the test would have proved one case three times over. 2050 tests pass, assets build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feature/host-bootstrap
parent
f9620f29a3
commit
c81a89ce5a
|
|
@ -47,6 +47,24 @@ final class DnsTokenCheck
|
|||
return ['ok' => false, 'reason' => 'rejected'];
|
||||
}
|
||||
|
||||
// Erst der Erfolg, DANN der Rumpf.
|
||||
//
|
||||
// Ohne diese Zeile wurde jede nicht klassifizierte Fehlerantwort — 404,
|
||||
// 429, 500, eine Statusseite eines Zwischenspeichers — zu einer leeren
|
||||
// Zonenliste: der Rumpf hat kein `zones`, `?? []` macht daraus keine
|
||||
// Zonen, und die Anzeige behauptete daraufhin „in diesem Konto liegt
|
||||
// keine einzige Zone". Eine Aussage über ein fremdes Konto, hergeleitet
|
||||
// aus einem Fehler, den niemand angesehen hat.
|
||||
//
|
||||
// Das ist derselbe Fehler, den diese Prüfung an anderer Stelle
|
||||
// vermeidet: `unreachable` und `read_only` sind getrennt, weil beide
|
||||
// Fehlschläge sind, aber nur einer etwas über den Token sagt. Ein
|
||||
// unerwarteter Status sagt über die Zonen gar nichts — also sagt er das
|
||||
// jetzt auch, mit der Zahl daneben.
|
||||
if (! $zones->successful()) {
|
||||
return ['ok' => false, 'reason' => 'zone_list_failed', 'status' => $zones->status()];
|
||||
}
|
||||
|
||||
$verfuegbar = collect($zones->json('zones') ?? [])->pluck('name')->filter()->values();
|
||||
$zoneId = collect($zones->json('zones') ?? [])->firstWhere('name', $zone)['id'] ?? null;
|
||||
|
||||
|
|
|
|||
|
|
@ -118,4 +118,5 @@ return [
|
|||
'zone_wanted' => 'Der Token ist gültig — er hat die Zonenliste geholt. Gesucht wurde die Zone :zone.',
|
||||
'zone_available' => 'In diesem Konto liegen: :zones. Entweder CLUPILOT_DNS_ZONE anpassen oder die Zone bei Hetzner anlegen.',
|
||||
'zone_none' => 'In diesem Konto liegt keine einzige Zone. Der Token gehört vermutlich zu einem anderen Hetzner-Projekt.',
|
||||
'zone_list_unclear' => 'Hetzner hat die Zonenliste nicht ausgeliefert (HTTP :status). Über die Zonen dieses Kontos sagt das nichts — der Token kann trotzdem richtig sein.',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -113,4 +113,5 @@ return [
|
|||
'zone_wanted' => 'The token is valid — it fetched the zone list. It looked for the zone :zone.',
|
||||
'zone_available' => 'This account holds: :zones. Either adjust CLUPILOT_DNS_ZONE or create the zone at Hetzner.',
|
||||
'zone_none' => 'This account holds no zones at all. The token probably belongs to a different Hetzner project.',
|
||||
'zone_list_unclear' => 'Hetzner did not return the zone list (HTTP :status). That says nothing about this account\'s zones — the token may still be correct.',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -89,6 +89,12 @@
|
|||
aus, obwohl der Token gerade eben die Zonenliste geholt hat.
|
||||
Gesucht und gefunden nebeneinander beantwortet die Frage
|
||||
selbst. --}}
|
||||
@if (($result['reason'] ?? null) === 'zone_list_failed')
|
||||
<p class="mt-1 text-xs text-muted">
|
||||
{{ __('readiness.zone_list_unclear', ['status' => $result['status'] ?? '?']) }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
@if (($result['reason'] ?? null) === 'zone_not_found')
|
||||
<p class="mt-1 text-xs text-muted">
|
||||
{{ __('readiness.zone_wanted', ['zone' => $result['zone'] ?? '—']) }}
|
||||
|
|
|
|||
|
|
@ -428,3 +428,39 @@ it('names the zone it wanted and the zones it found', function () {
|
|||
->assertSee(__('readiness.zone_wanted', ['zone' => 'clupilot.cloud']))
|
||||
->assertSee('clupilot.com, example.org');
|
||||
});
|
||||
|
||||
/**
|
||||
* Eine Fehlerantwort ist keine leere Zonenliste.
|
||||
*
|
||||
* Ohne diese Unterscheidung wurde aus jedem nicht klassifizierten Status — 404,
|
||||
* 429, 500 — die Aussage „in diesem Konto liegt keine einzige Zone". Eine
|
||||
* Behauptung über ein fremdes Hetzner-Konto, hergeleitet aus einem Fehler, den
|
||||
* niemand angesehen hat. Dieselbe Trennung, die diese Prüfung zwischen
|
||||
* `unreachable` und `read_only` schon zieht: beides sind Fehlschläge, aber nur
|
||||
* einer sagt etwas über den Token.
|
||||
*/
|
||||
it('does not call a failed zone list an empty account', function (int $status) {
|
||||
// Ein Datensatz statt einer Schleife: `Http::fake()` ERGÄNZT die Attrappen,
|
||||
// es ersetzt sie nicht — in einer Schleife hätte die erste alle folgenden
|
||||
// beantwortet, und der Test hätte dreimal dasselbe geprüft.
|
||||
Http::fake(['dns.hetzner.com/api/v1/zones' => Http::response('<html>nope</html>', $status)]);
|
||||
|
||||
$ergebnis = (new App\Services\Dns\DnsTokenCheck)->run('ein-token');
|
||||
|
||||
expect($ergebnis['ok'])->toBeFalse()
|
||||
->and($ergebnis['reason'])->toBe('zone_list_failed')
|
||||
->and($ergebnis['status'])->toBe($status);
|
||||
})->with([404, 429, 500, 502]);
|
||||
|
||||
/**
|
||||
* Eine ECHTE leere Liste bleibt dagegen, was sie ist: 200 mit null Zonen heißt,
|
||||
* dass der Token zu einem Projekt ohne Zonen gehört.
|
||||
*/
|
||||
it('still calls a genuinely empty account empty', function () {
|
||||
Http::fake(['dns.hetzner.com/api/v1/zones' => Http::response(['zones' => []], 200)]);
|
||||
|
||||
$ergebnis = (new App\Services\Dns\DnsTokenCheck)->run('ein-token');
|
||||
|
||||
expect($ergebnis['reason'])->toBe('zone_not_found')
|
||||
->and($ergebnis['available'])->toBe([]);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue