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:
@@ -67,7 +67,10 @@ interface OrderRow {
|
||||
|
||||
function matches(row: Record<string, any>, where: Record<string, any> = {}): boolean {
|
||||
return Object.entries(where).every(([key, expected]) => {
|
||||
if (expected && typeof expected === 'object' && !(expected instanceof Date)) {
|
||||
if (expected instanceof Date) {
|
||||
return row[key] instanceof Date && row[key].getTime() === expected.getTime();
|
||||
}
|
||||
if (expected && typeof expected === 'object') {
|
||||
if ('in' in expected) return (expected.in as unknown[]).includes(row[key]);
|
||||
if ('not' in expected) return row[key] !== expected.not;
|
||||
}
|
||||
@@ -150,7 +153,12 @@ function makeHarness(opts: { environment?: 'DEMO' | 'LIVE'; timeoutMs?: number }
|
||||
// Wie Postgres: prueft die Bedingung gegen den AKTUELLEN Zeilenstand, synchron.
|
||||
updateMany: vi.fn(async ({ where, data }: any) => {
|
||||
const rows = state.orders.filter((o) => matches(o, where));
|
||||
for (const r of rows) Object.assign(r, data);
|
||||
// Wie Prisma `@updatedAt`: jede Aenderung setzt einen neuen Stand.
|
||||
for (const r of rows) {
|
||||
Object.assign(r, data, {
|
||||
updatedAt: new Date(Math.max(Date.now(), r.updatedAt.getTime() + 1)),
|
||||
});
|
||||
}
|
||||
return { count: rows.length };
|
||||
}),
|
||||
};
|
||||
@@ -235,6 +243,9 @@ function studioReply(domain: string, status: string, prices?: unknown[]) {
|
||||
|
||||
const USER = { id: 'u1', username: 'anna' };
|
||||
|
||||
/** Der Stand, den der Browser aus der Zusammenfassung zurueckschickt. */
|
||||
const versionOf = (order: { updatedAt: Date }) => order.updatedAt.toISOString();
|
||||
|
||||
const EXPECTED_SUBMIT_BODY = {
|
||||
name: 'beispiel.de',
|
||||
period: { unit: 'YEAR', period: 1 },
|
||||
@@ -362,6 +373,7 @@ describe('createOrder', () => {
|
||||
expect(result.contacts.zone).toEqual({ id: 22, displayName: 'Erika Muster' });
|
||||
expect(result.nameServers).toEqual(['ns1.example.com', 'ns2.example.com']);
|
||||
expect(result.order).toMatchObject({ domainName: 'beispiel.de', status: 'DRAFT' });
|
||||
expect(result.version).toBe(h.state.orders[0].updatedAt.toISOString());
|
||||
// Der Entwurf selbst bestellt nichts.
|
||||
expect(h.calls.every((c) => !c.url.endsWith('/domain'))).toBe(true);
|
||||
});
|
||||
@@ -426,6 +438,7 @@ describe('createOrder', () => {
|
||||
expect(h.state.orders).toHaveLength(1);
|
||||
expect(h.state.orders[0].payload.ownerContactId).toBe(11);
|
||||
expect(h.orderApi.create).not.toHaveBeenCalled();
|
||||
expect(result.version).toBe(h.state.orders[0].updatedAt.toISOString());
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -466,6 +479,7 @@ describe('submitOrder — der Anspruch', () => {
|
||||
it('sendet nach dem Anspruch genau EINEN POST /domain ohne Query und mit dem erwarteten Rumpf', async () => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder();
|
||||
const seenVersion = new Date(versionOf(order));
|
||||
h.respond(() =>
|
||||
reply(
|
||||
200,
|
||||
@@ -475,7 +489,7 @@ describe('submitOrder — der Anspruch', () => {
|
||||
),
|
||||
);
|
||||
|
||||
const view = await h.service.submitOrder('t1', USER, order.id);
|
||||
const view = await h.service.submitOrder('t1', USER, order.id, seenVersion.toISOString());
|
||||
|
||||
expect(h.orderApi.updateMany).toHaveBeenCalledTimes(2);
|
||||
const claim = h.orderApi.updateMany.mock.calls[0][0];
|
||||
@@ -484,6 +498,7 @@ describe('submitOrder — der Anspruch', () => {
|
||||
tenantId: 't1',
|
||||
status: 'DRAFT',
|
||||
environment: 'DEMO',
|
||||
updatedAt: seenVersion,
|
||||
});
|
||||
expect(claim.data).toMatchObject({
|
||||
status: 'SUBMITTING',
|
||||
@@ -532,8 +547,8 @@ describe('submitOrder — der Anspruch', () => {
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
h.service.submitOrder('t1', USER, order.id),
|
||||
h.service.submitOrder('t1', USER, order.id),
|
||||
h.service.submitOrder('t1', USER, order.id, versionOf(order)),
|
||||
h.service.submitOrder('t1', USER, order.id, versionOf(order)),
|
||||
]);
|
||||
|
||||
const ok = results.filter((r) => r.status === 'fulfilled');
|
||||
@@ -549,7 +564,9 @@ describe('submitOrder — der Anspruch', () => {
|
||||
const h = makeHarness({ environment: 'DEMO' });
|
||||
const order = h.addOrder({ environment: 'DEMO' });
|
||||
h.state.environment = 'LIVE';
|
||||
await expect(h.service.submitOrder('t1', USER, order.id)).rejects.toMatchObject({
|
||||
await expect(
|
||||
h.service.submitOrder('t1', USER, order.id, versionOf(order)),
|
||||
).rejects.toMatchObject({
|
||||
status: 409,
|
||||
response: { code: 'environmentChanged' },
|
||||
});
|
||||
@@ -568,7 +585,9 @@ describe('submitOrder — der Anspruch', () => {
|
||||
reads += 1;
|
||||
return reads === 1 && row ? { ...row, environment: 'DEMO' } : row;
|
||||
});
|
||||
await expect(h.service.submitOrder('t1', USER, order.id)).rejects.toMatchObject({
|
||||
await expect(
|
||||
h.service.submitOrder('t1', USER, order.id, versionOf(order)),
|
||||
).rejects.toMatchObject({
|
||||
response: { code: 'environmentChanged' },
|
||||
});
|
||||
expect(h.fetchImpl).not.toHaveBeenCalled();
|
||||
@@ -585,7 +604,9 @@ describe('submitOrder — der Anspruch', () => {
|
||||
])('Auftrag im Zustand %s -> 409 alreadySubmitted, kein Netz', async (status) => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder({ status });
|
||||
await expect(h.service.submitOrder('t1', USER, order.id)).rejects.toMatchObject({
|
||||
await expect(
|
||||
h.service.submitOrder('t1', USER, order.id, versionOf(order)),
|
||||
).rejects.toMatchObject({
|
||||
status: 409,
|
||||
response: { code: 'alreadySubmitted' },
|
||||
});
|
||||
@@ -595,7 +616,58 @@ describe('submitOrder — der Anspruch', () => {
|
||||
it('fremde Nummer -> 404, kein Netz', async () => {
|
||||
const h = makeHarness();
|
||||
h.addOrder({ tenantId: 'andere' });
|
||||
await expect(h.service.submitOrder('t1', USER, 'o1')).rejects.toBeInstanceOf(NotFoundException);
|
||||
await expect(
|
||||
h.service.submitOrder('t1', USER, 'o1', new Date().toISOString()),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
expect(h.fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('anderer Stand als in der Zusammenfassung gesehen -> 409 orderChanged, kein Anspruch, kein Netz', async () => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder();
|
||||
const seen = versionOf(order);
|
||||
// Ein zweiter Verwalter hat den Entwurf inzwischen neu angelegt (Kontakte, Nameserver).
|
||||
order.updatedAt = new Date(order.updatedAt.getTime() + 5000);
|
||||
order.payload = { ...order.payload, ownerContactId: 22 };
|
||||
await expect(h.service.submitOrder('t1', USER, order.id, seen)).rejects.toMatchObject({
|
||||
status: 409,
|
||||
response: { code: 'orderChanged' },
|
||||
});
|
||||
expect(h.fetchImpl).not.toHaveBeenCalled();
|
||||
expect(h.orderApi.updateMany).not.toHaveBeenCalled();
|
||||
expect(h.state.orders[0].status).toBe('DRAFT');
|
||||
});
|
||||
|
||||
it('Aenderung zwischen Lesen und Anspruch -> der Anspruch greift nicht: 409 orderChanged, kein Netz', async () => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder();
|
||||
const seen = versionOf(order);
|
||||
const original = h.orderApi.findFirst.getMockImplementation() as (a: any) => Promise<any>;
|
||||
let reads = 0;
|
||||
h.orderApi.findFirst.mockImplementation(async (a: any) => {
|
||||
const row = await original(a);
|
||||
reads += 1;
|
||||
if (reads === 1) {
|
||||
// Nach dem Lesen, vor dem Anspruch: ein anderer ueberschreibt den Entwurf.
|
||||
order.updatedAt = new Date(order.updatedAt.getTime() + 5000);
|
||||
order.payload = { ...order.payload, ownerContactId: 22 };
|
||||
}
|
||||
return row;
|
||||
});
|
||||
await expect(h.service.submitOrder('t1', USER, order.id, seen)).rejects.toMatchObject({
|
||||
status: 409,
|
||||
response: { code: 'orderChanged' },
|
||||
});
|
||||
expect(h.fetchImpl).not.toHaveBeenCalled();
|
||||
expect(h.state.orders[0].status).toBe('DRAFT');
|
||||
});
|
||||
|
||||
it('ein unlesbarer Stand bestellt nichts', async () => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder();
|
||||
await expect(h.service.submitOrder('t1', USER, order.id, 'kein-datum')).rejects.toMatchObject({
|
||||
response: { code: 'orderChanged' },
|
||||
});
|
||||
expect(h.fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -605,7 +677,7 @@ describe('submitOrder — Ausgang', () => {
|
||||
const h = makeHarness({ timeoutMs: 20 });
|
||||
const order = h.addOrder();
|
||||
h.respond(() => new Promise(() => undefined));
|
||||
const view = await h.service.submitOrder('t1', USER, order.id);
|
||||
const view = await h.service.submitOrder('t1', USER, order.id, versionOf(order));
|
||||
expect(view.status).toBe('UNKNOWN');
|
||||
expect(h.calls).toHaveLength(1);
|
||||
expect(h.state.orders[0].status).toBe('UNKNOWN');
|
||||
@@ -619,7 +691,7 @@ describe('submitOrder — Ausgang', () => {
|
||||
h.respond(() => {
|
||||
throw Object.assign(new Error('reset'), { code: 'ECONNRESET' });
|
||||
});
|
||||
const view = await h.service.submitOrder('t1', USER, order.id);
|
||||
const view = await h.service.submitOrder('t1', USER, order.id, versionOf(order));
|
||||
expect(view.status).toBe('UNKNOWN');
|
||||
expect(h.calls).toHaveLength(1);
|
||||
});
|
||||
@@ -628,14 +700,66 @@ describe('submitOrder — Ausgang', () => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder();
|
||||
h.respond(() => reply(200, '<html>Wartung</html>'));
|
||||
expect((await h.service.submitOrder('t1', USER, order.id)).status).toBe('UNKNOWN');
|
||||
expect((await h.service.submitOrder('t1', USER, order.id, versionOf(order))).status).toBe(
|
||||
'UNKNOWN',
|
||||
);
|
||||
});
|
||||
|
||||
it('HTTP 502 ohne Umschlag (Gateway) -> UNKNOWN, nicht FAILED', async () => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder();
|
||||
h.respond(() => reply(502, 'Bad Gateway'));
|
||||
expect((await h.service.submitOrder('t1', USER, order.id)).status).toBe('UNKNOWN');
|
||||
expect((await h.service.submitOrder('t1', USER, order.id, versionOf(order))).status).toBe(
|
||||
'UNKNOWN',
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
408, 425, 429, 500, 502, 503, 504,
|
||||
])('HTTP %i mit AutoDNS-Huelle -> UNKNOWN, nicht FAILED, openKey bleibt', async (status) => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder();
|
||||
h.respond(() =>
|
||||
reply(
|
||||
status,
|
||||
envelope([], {
|
||||
type: 'ERROR',
|
||||
messages: [{ code: 'E9', text: 'Gateway timeout', status: 'ERROR' }],
|
||||
}),
|
||||
),
|
||||
);
|
||||
const view = await h.service.submitOrder('t1', USER, order.id, versionOf(order));
|
||||
expect(view.status).toBe('UNKNOWN');
|
||||
expect(view.errorText).toBeNull();
|
||||
expect(h.calls).toHaveLength(1);
|
||||
expect(h.state.orders[0]).toMatchObject({ status: 'UNKNOWN', openKey: 'beispiel.de' });
|
||||
expect(h.state.orders[0].lastCheckedAt).toBeNull();
|
||||
// Die Domain bleibt gesperrt: keine zweite Bestellung moeglich.
|
||||
h.respond(() => studioReply('beispiel.de', 'FREE'));
|
||||
await expect(
|
||||
h.service.createOrder('t1', 'u1', {
|
||||
domain: 'beispiel.de',
|
||||
ownerContactId: 11,
|
||||
adminContactId: 11,
|
||||
techContactId: 22,
|
||||
zoneContactId: 22,
|
||||
nameServers: ['ns1.example.com', 'ns2.example.com'],
|
||||
} as any),
|
||||
).rejects.toMatchObject({ response: { code: 'orderOpen' } });
|
||||
});
|
||||
|
||||
it('HTTP 400 mit AutoDNS-Huelle -> FAILED (ausdrueckliche Ablehnung)', async () => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder();
|
||||
h.respond(() =>
|
||||
reply(
|
||||
400,
|
||||
envelope([], { type: 'ERROR', messages: [{ text: 'Invalid contact', status: 'ERROR' }] }),
|
||||
),
|
||||
);
|
||||
const view = await h.service.submitOrder('t1', USER, order.id, versionOf(order));
|
||||
expect(view.status).toBe('FAILED');
|
||||
expect(h.state.orders[0].openKey).toBeNull();
|
||||
});
|
||||
|
||||
it('HTTP 200 mit status.type ERROR -> FAILED mit Text, openKey leer', async () => {
|
||||
@@ -650,7 +774,7 @@ describe('submitOrder — Ausgang', () => {
|
||||
}),
|
||||
),
|
||||
);
|
||||
const view = await h.service.submitOrder('t1', USER, order.id);
|
||||
const view = await h.service.submitOrder('t1', USER, order.id, versionOf(order));
|
||||
expect(view.status).toBe('FAILED');
|
||||
expect(view.errorText).toContain('Domain not available');
|
||||
expect(h.state.orders[0].openKey).toBeNull();
|
||||
@@ -669,7 +793,7 @@ describe('submitOrder — Ausgang', () => {
|
||||
}),
|
||||
),
|
||||
);
|
||||
const view = await h.service.submitOrder('t1', USER, order.id);
|
||||
const view = await h.service.submitOrder('t1', USER, order.id, versionOf(order));
|
||||
expect(view.status).toBe('FAILED');
|
||||
expect(view.errorText).toContain('Benutzername, Passwort und Kontext');
|
||||
expect(h.state.orders[0].openKey).toBeNull();
|
||||
@@ -681,7 +805,7 @@ describe('submitOrder — Ausgang', () => {
|
||||
h.respond(() =>
|
||||
reply(200, envelope([{}], { object: { type: 'domain', value: 'beispiel.de', summary: 1 } })),
|
||||
);
|
||||
const view = await h.service.submitOrder('t1', USER, order.id);
|
||||
const view = await h.service.submitOrder('t1', USER, order.id, versionOf(order));
|
||||
expect(view.status).toBe('SUBMITTED');
|
||||
expect(h.state.orders[0].jobId).toBeNull();
|
||||
});
|
||||
@@ -690,14 +814,52 @@ describe('submitOrder — Ausgang', () => {
|
||||
const h = makeHarness({ timeoutMs: 20 });
|
||||
const order = h.addOrder();
|
||||
h.respond(() => new Promise(() => undefined));
|
||||
await h.service.submitOrder('t1', USER, order.id);
|
||||
await expect(h.service.submitOrder('t1', USER, order.id)).rejects.toMatchObject({
|
||||
await h.service.submitOrder('t1', USER, order.id, versionOf(order));
|
||||
await expect(
|
||||
h.service.submitOrder('t1', USER, order.id, versionOf(order)),
|
||||
).rejects.toMatchObject({
|
||||
response: { code: 'alreadySubmitted' },
|
||||
});
|
||||
expect(h.calls).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitOrder — Speichern des Ergebnisses scheitert (IN-03)', () => {
|
||||
function failWritesFrom(h: ReturnType<typeof makeHarness>, failing: number[]) {
|
||||
const original = h.orderApi.updateMany.getMockImplementation() as (a: any) => Promise<any>;
|
||||
let call = 0;
|
||||
h.orderApi.updateMany.mockImplementation(async (a: any) => {
|
||||
call += 1;
|
||||
if (failing.includes(call)) throw new Error('Datenbank nicht erreichbar');
|
||||
return original(a);
|
||||
});
|
||||
}
|
||||
|
||||
it('Ergebnis-Schreibvorgang scheitert -> kein 500, Auftrag wird UNKNOWN, nie DRAFT', async () => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder();
|
||||
h.respond(() => reply(200, envelope([{ id: 987, status: 'RUNNING' }])));
|
||||
failWritesFrom(h, [2]); // 1 = Anspruch, 2 = Ergebnis
|
||||
const view = await h.service.submitOrder('t1', USER, order.id, versionOf(order));
|
||||
expect(view.status).toBe('UNKNOWN');
|
||||
expect(h.state.orders[0]).toMatchObject({ status: 'UNKNOWN', openKey: 'beispiel.de' });
|
||||
expect(h.calls).toHaveLength(1);
|
||||
expect(h.directory.invalidate).toHaveBeenCalledWith('t1');
|
||||
});
|
||||
|
||||
it('auch UNKNOWN laesst sich nicht speichern -> Ansicht UNKNOWN, Zeile bleibt SUBMITTING (nie DRAFT)', async () => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder();
|
||||
h.respond(() => reply(200, envelope([{ id: 987, status: 'RUNNING' }])));
|
||||
failWritesFrom(h, [2, 3]);
|
||||
const view = await h.service.submitOrder('t1', USER, order.id, versionOf(order));
|
||||
expect(view.status).toBe('UNKNOWN');
|
||||
expect(h.state.orders[0].status).toBe('SUBMITTING');
|
||||
expect(h.state.orders[0].openKey).toBe('beispiel.de');
|
||||
expect(h.calls).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshOpenOrders', () => {
|
||||
it('SUBMITTED mit Auftrag: SUCCESS -> SUCCESS, openKey bleibt, Zwischenspeicher geleert', async () => {
|
||||
const h = makeHarness();
|
||||
@@ -861,7 +1023,7 @@ describe('refreshOpenOrders', () => {
|
||||
expect(h.state.orders[0].lastCheckedAt).toBeNull();
|
||||
});
|
||||
|
||||
it('ein alter Auftrag ohne lesbare Anlagezeit zaehlt nicht', async () => {
|
||||
it('ein Auftrag der Domain ohne lesbare Anlagezeit ist unklar: lastCheckedAt bleibt leer', async () => {
|
||||
const h = makeHarness();
|
||||
h.addOrder({ status: 'UNKNOWN', confirmedAt });
|
||||
h.respond((call) =>
|
||||
@@ -871,6 +1033,93 @@ describe('refreshOpenOrders', () => {
|
||||
);
|
||||
await h.service.refreshOpenOrders('t1');
|
||||
expect(h.state.orders[0]).toMatchObject({ status: 'UNKNOWN', jobId: null });
|
||||
expect(h.state.orders[0].lastCheckedAt).toBeNull();
|
||||
});
|
||||
|
||||
it('ein Auftrag ohne lesbares Objekt ist unklar: lastCheckedAt bleibt leer', async () => {
|
||||
const h = makeHarness();
|
||||
h.addOrder({ status: 'UNKNOWN', confirmedAt });
|
||||
h.respond((call) =>
|
||||
call.url.endsWith('/domain/beispiel.de')
|
||||
? reply(404, envelope([], { type: 'ERROR', messages: [{ text: 'nf', status: 'ERROR' }] }))
|
||||
: reply(
|
||||
200,
|
||||
envelope([{ job: { id: 7, status: 'RUNNING', created: new Date().toISOString() } }]),
|
||||
),
|
||||
);
|
||||
await h.service.refreshOpenOrders('t1');
|
||||
expect(h.state.orders[0].lastCheckedAt).toBeNull();
|
||||
});
|
||||
|
||||
it('ein alter, lesbar datierter Auftrag zaehlt nicht und belegt "nichts gefunden"', async () => {
|
||||
const h = makeHarness();
|
||||
h.addOrder({ status: 'UNKNOWN', confirmedAt });
|
||||
h.respond((call) =>
|
||||
call.url.endsWith('/domain/beispiel.de')
|
||||
? reply(404, envelope([], { type: 'ERROR', messages: [{ text: 'nf', status: 'ERROR' }] }))
|
||||
: reply(
|
||||
200,
|
||||
envelope([
|
||||
{
|
||||
job: { id: 7, status: 'FAILED', created: '2020-01-01T00:00:00Z' },
|
||||
object: 'beispiel.de',
|
||||
},
|
||||
]),
|
||||
),
|
||||
);
|
||||
await h.service.refreshOpenOrders('t1');
|
||||
expect(h.state.orders[0]).toMatchObject({ status: 'UNKNOWN', jobId: null });
|
||||
expect(h.state.orders[0].lastCheckedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('eine volle Trefferliste ohne passenden Auftrag ist unklar (koennte abgeschnitten sein)', async () => {
|
||||
const h = makeHarness();
|
||||
h.addOrder({ status: 'UNKNOWN', confirmedAt });
|
||||
const jobs = Array.from({ length: 10 }, (_, i) => ({
|
||||
job: { id: 100 + i, status: 'SUCCESS', created: new Date().toISOString() },
|
||||
object: `andere${i}.de`,
|
||||
}));
|
||||
h.respond((call) =>
|
||||
call.url.endsWith('/domain/beispiel.de')
|
||||
? reply(404, envelope([], { type: 'ERROR', messages: [{ text: 'nf', status: 'ERROR' }] }))
|
||||
: reply(200, envelope(jobs)),
|
||||
);
|
||||
await h.service.refreshOpenOrders('t1');
|
||||
expect(h.state.orders[0].lastCheckedAt).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
408, 425, 429, 500, 502, 504,
|
||||
])('GET /domain/{name} mit HTTP %i und Huelle ist KEIN "gibt es nicht": nichts geschrieben, keine Auftragssuche', async (status) => {
|
||||
const h = makeHarness();
|
||||
h.addOrder({ status: 'UNKNOWN', confirmedAt });
|
||||
h.respond(() =>
|
||||
reply(
|
||||
status,
|
||||
envelope([], { type: 'ERROR', messages: [{ text: 'Timeout', status: 'ERROR' }] }),
|
||||
),
|
||||
);
|
||||
await h.service.refreshOpenOrders('t1');
|
||||
expect(h.calls.map((c) => c.url)).toEqual([
|
||||
'https://api.demo.autodns.com/v1/domain/beispiel.de',
|
||||
]);
|
||||
expect(h.state.orders[0].status).toBe('UNKNOWN');
|
||||
expect(h.state.orders[0].lastCheckedAt).toBeNull();
|
||||
});
|
||||
|
||||
it('die Auftragssuche mit HTTP 500 und Huelle belegt "nichts gefunden" nicht', async () => {
|
||||
const h = makeHarness();
|
||||
h.addOrder({ status: 'UNKNOWN', confirmedAt });
|
||||
h.respond((call) =>
|
||||
call.url.endsWith('/domain/beispiel.de')
|
||||
? reply(404, envelope([], { type: 'ERROR', messages: [{ text: 'nf', status: 'ERROR' }] }))
|
||||
: reply(
|
||||
500,
|
||||
envelope([], { type: 'ERROR', messages: [{ text: 'boom', status: 'ERROR' }] }),
|
||||
),
|
||||
);
|
||||
await h.service.refreshOpenOrders('t1');
|
||||
expect(h.state.orders[0].lastCheckedAt).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -926,22 +1175,116 @@ describe('cancelOrder', () => {
|
||||
expect(h.state.orders[0]).toMatchObject({ status: 'CANCELED', openKey: null });
|
||||
});
|
||||
|
||||
it('UNKNOWN mit gesetztem lastCheckedAt -> CANCELED', async () => {
|
||||
const notFound = (call: Call) =>
|
||||
call.url.endsWith('/domain/beispiel.de')
|
||||
? reply(404, envelope([], { type: 'ERROR', messages: [{ text: 'nf', status: 'ERROR' }] }))
|
||||
: reply(200, envelope([]));
|
||||
|
||||
it('UNKNOWN: Verwerfen fragt im Augenblick des Verwerfens erneut bei AutoDNS nach', async () => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder({ status: 'UNKNOWN', lastCheckedAt: new Date() });
|
||||
const order = h.addOrder({
|
||||
status: 'UNKNOWN',
|
||||
confirmedAt: new Date(Date.now() - 60 * 60_000),
|
||||
// Der fruehere Abgleich ist Stunden alt und zaehlt nicht.
|
||||
lastCheckedAt: new Date(Date.now() - 3 * 60 * 60_000),
|
||||
});
|
||||
h.respond(notFound);
|
||||
const view = await h.service.cancelOrder('t1', order.id);
|
||||
expect(view.status).toBe('CANCELED');
|
||||
expect(h.calls.map((c) => c.url)).toEqual([
|
||||
'https://api.demo.autodns.com/v1/domain/beispiel.de',
|
||||
'https://api.demo.autodns.com/v1/job/_search',
|
||||
]);
|
||||
expect(h.state.orders[0]).toMatchObject({ status: 'CANCELED', openKey: null });
|
||||
});
|
||||
|
||||
it('UNKNOWN ohne frueheren Abgleich wird bei ausdruecklichem "nicht gefunden" verworfen', async () => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder({ status: 'UNKNOWN', confirmedAt: new Date(Date.now() - 600_000) });
|
||||
h.respond(notFound);
|
||||
await h.service.cancelOrder('t1', order.id);
|
||||
expect(h.state.orders[0]).toMatchObject({ status: 'CANCELED', openKey: null });
|
||||
});
|
||||
|
||||
it('UNKNOWN ohne Abgleich -> 409 checkFirst', async () => {
|
||||
it('UNKNOWN: Domain inzwischen bei AutoDNS vorhanden -> 409 orderFound, Auftrag wird SUCCESS', async () => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder({ status: 'UNKNOWN' });
|
||||
const order = h.addOrder({ status: 'UNKNOWN', lastCheckedAt: new Date(Date.now() - 600_000) });
|
||||
h.respond(() => reply(200, envelope([{ name: 'beispiel.de' }])));
|
||||
await expect(h.service.cancelOrder('t1', order.id)).rejects.toMatchObject({
|
||||
status: 409,
|
||||
response: { code: 'orderFound' },
|
||||
});
|
||||
expect(h.state.orders[0]).toMatchObject({ status: 'SUCCESS', openKey: 'beispiel.de' });
|
||||
});
|
||||
|
||||
it('UNKNOWN: Auftrag inzwischen bei AutoDNS vorhanden -> 409 orderFound, openKey bleibt', async () => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder({
|
||||
status: 'UNKNOWN',
|
||||
confirmedAt: new Date(Date.now() - 600_000),
|
||||
lastCheckedAt: new Date(Date.now() - 600_000),
|
||||
});
|
||||
h.respond((call) =>
|
||||
call.url.endsWith('/domain/beispiel.de')
|
||||
? notFound(call)
|
||||
: reply(
|
||||
200,
|
||||
envelope([
|
||||
{
|
||||
job: { id: 555, status: 'RUNNING', created: new Date().toISOString() },
|
||||
object: 'beispiel.de',
|
||||
},
|
||||
]),
|
||||
),
|
||||
);
|
||||
await expect(h.service.cancelOrder('t1', order.id)).rejects.toMatchObject({
|
||||
response: { code: 'orderFound' },
|
||||
});
|
||||
expect(h.state.orders[0]).toMatchObject({
|
||||
status: 'SUBMITTED',
|
||||
jobId: '555',
|
||||
openKey: 'beispiel.de',
|
||||
});
|
||||
});
|
||||
|
||||
it('UNKNOWN: AutoDNS antwortet mit 500 + Huelle -> 409 checkFirst, nichts verworfen', async () => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder({ status: 'UNKNOWN', lastCheckedAt: new Date(Date.now() - 600_000) });
|
||||
h.respond(() =>
|
||||
reply(500, envelope([], { type: 'ERROR', messages: [{ text: 'boom', status: 'ERROR' }] })),
|
||||
);
|
||||
await expect(h.service.cancelOrder('t1', order.id)).rejects.toMatchObject({
|
||||
response: { code: 'checkFirst' },
|
||||
});
|
||||
expect(h.state.orders[0]).toMatchObject({ status: 'UNKNOWN', openKey: 'beispiel.de' });
|
||||
});
|
||||
|
||||
it('UNKNOWN: Zeitablauf beim Nachsehen -> 409 checkFirst, nichts verworfen', async () => {
|
||||
const h = makeHarness({ timeoutMs: 20 });
|
||||
const order = h.addOrder({ status: 'UNKNOWN', lastCheckedAt: new Date(Date.now() - 600_000) });
|
||||
h.respond(() => new Promise(() => undefined));
|
||||
await expect(h.service.cancelOrder('t1', order.id)).rejects.toMatchObject({
|
||||
response: { code: 'checkFirst' },
|
||||
});
|
||||
expect(h.state.orders[0].status).toBe('UNKNOWN');
|
||||
});
|
||||
|
||||
it('UNKNOWN eines anderen Systems -> 409 environmentChanged, kein Netz', async () => {
|
||||
const h = makeHarness({ environment: 'LIVE' });
|
||||
const order = h.addOrder({ status: 'UNKNOWN', environment: 'DEMO' });
|
||||
await expect(h.service.cancelOrder('t1', order.id)).rejects.toMatchObject({
|
||||
response: { code: 'environmentChanged' },
|
||||
});
|
||||
expect(h.fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('DRAFT wird ohne Netz verworfen', async () => {
|
||||
const h = makeHarness();
|
||||
const order = h.addOrder();
|
||||
await h.service.cancelOrder('t1', order.id);
|
||||
expect(h.fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
'SUBMITTING',
|
||||
'SUBMITTED',
|
||||
|
||||
Reference in New Issue
Block a user