fix(domains): HTTP 5xx nie als Ablehnung werten, Verwerfen mit Neupruefung, Entwurfsstand im Anspruch (CR-01, WR-01, WR-02, WR-03, IN-03)

CR-01: 5xx, 408, 425 (auch mit AutoDNS-Huelle) sind kein "abgelehnt" mehr,
Bestellung endet in UNKNOWN und der Abgleich wertet sie nicht als "Domain gibt
es nicht". 429 gilt bei der Bestellung ebenfalls als unklar, weil nicht belegt
ist, dass AutoDNS vor der Verarbeitung abgelehnt hat.
WR-01: Verwerfen eines unklaren Auftrags fragt im Augenblick des Verwerfens
erneut bei AutoDNS nach; nur ein ausdrueckliches "nicht gefunden" verwirft.
WR-02: Die Bestaetigung traegt den Entwurfsstand (updatedAt); der Anspruch
greift nur bei gleichem Stand, sonst 409 orderChanged.
WR-03: Auftraege ohne lesbares Datum/Objekt oder eine volle Trefferliste
machen "nichts gefunden" unklar statt verwerfbar.
IN-03: Scheitert das Speichern nach POST /domain, wird die Auftragsnummer
protokolliert und der Auftrag auf UNKNOWN gebracht (nie DRAFT), ohne 500.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-08 11:29:01 +02:00
parent e1dd996438
commit cc1c83ab0a
13 changed files with 632 additions and 68 deletions
@@ -102,6 +102,8 @@ export function OrdersTab({ canManage }: { canManage: boolean }) {
await refresh();
} catch (e) {
setDiscardError(e instanceof DomainsRequestError ? e.message : tc('requestFailed'));
// Beim Verwerfen hat Tessera frisch bei AutoDNS nachgesehen; der Zustand kann sich geaendert haben.
void refresh();
}
}
@@ -124,6 +124,7 @@ function summary(over: Partial<OrderSummary> = {}): OrderSummary {
},
nameServers: ['ns1.example.com', 'ns2.example.com'],
periodYears: 1,
version: '2026-10-08T10:00:00.000Z',
...over,
};
}
@@ -324,7 +325,8 @@ describe('RegisterTab — Zusammenfassung und verbindliche Bestaetigung', () =>
fireEvent.click(button);
fireEvent.click(button);
expect(mockSubmit).toHaveBeenCalledTimes(1);
expect(mockSubmit).toHaveBeenCalledWith('o1');
// Der Stand aus der Zusammenfassung geht mit: bestellt wird nur, was der Benutzer gesehen hat.
expect(mockSubmit).toHaveBeenCalledWith('o1', '2026-10-08T10:00:00.000Z');
expect(screen.getByRole('button', { name: 'Wird übermittelt …' })).toBeTruthy();
resolveSubmit(order({ status: 'SUBMITTED', jobStatus: 'RUNNING' }));
await screen.findByText(/wird bearbeitet/);
@@ -383,6 +385,18 @@ describe('RegisterTab — Zusammenfassung und verbindliche Bestaetigung', () =>
expect(screen.queryByRole('button', { name: 'Jetzt verbindlich registrieren' })).toBeNull();
});
it('hat sich der Entwurf geaendert, zeigt der Dialog die Meldung des Servers und bestellt nichts', async () => {
const message =
'Der Auftrag wurde inzwischen geändert. Bitte prüfen Sie die Zusammenfassung erneut, bevor Sie bestellen.';
mockSubmit.mockRejectedValue(new DomainsRequestError(409, 'orderChanged', message));
renderTab();
await reachSummary();
fireEvent.click(screen.getByRole('checkbox'));
fireEvent.click(screen.getByRole('button', { name: 'Jetzt verbindlich registrieren' }));
expect(await screen.findByText(message)).toBeTruthy();
expect(mockSubmit).toHaveBeenCalledTimes(1);
});
it('"Abbrechen" ruft cancelOrder auf und beginnt neu', async () => {
renderTab();
await reachSummary();
@@ -194,7 +194,7 @@ export function RegisterTab({
setSubmitting(true);
setSubmitError(null);
try {
setResult(await submitOrder(summary.order.id));
setResult(await submitOrder(summary.order.id, summary.version));
} catch (e) {
// Die Sperre bleibt: der Auftrag koennte angekommen sein. Nur "Zurueck" hebt sie auf.
setSubmitError(e instanceof DomainsRequestError ? e.message : t('submitFailed'));
+12 -3
View File
@@ -304,6 +304,8 @@ export interface OrderSummary {
};
nameServers: string[];
periodYears: 1;
/** Stand des Entwurfs; geht beim verbindlichen Bestellen unveraendert zurueck. */
version: string;
}
export interface CreateOrderInput {
@@ -323,9 +325,16 @@ export function createOrder(input: CreateOrderInput): Promise<OrderSummary> {
return request<OrderSummary>('/orders', { method: 'POST', json: input });
}
/** Verbindlich: schickt die Registrierung an AutoDNS (kostet im Live-System Geld). */
export function submitOrder(id: string): Promise<DomainsOrder> {
return request<DomainsOrder>(`/orders/${encodeURIComponent(id)}/submit`, { method: 'POST' });
/**
* Verbindlich: schickt die Registrierung an AutoDNS (kostet im Live-System Geld).
* `version` ist der Stand aus der Zusammenfassung; hat sich der Entwurf seither
* geaendert, lehnt der Server mit 409 ab.
*/
export function submitOrder(id: string, version: string): Promise<DomainsOrder> {
return request<DomainsOrder>(`/orders/${encodeURIComponent(id)}/submit`, {
method: 'POST',
json: { version },
});
}
export function cancelOrder(id: string): Promise<DomainsOrder> {