ff4938fa97
AutoDNS loescht abgeschlossene Auftraege; GET /job/{id} liefert dann 404
(EF300101). Gibt es die Domain, ist die Registrierung durch.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1559 lines
56 KiB
TypeScript
1559 lines
56 KiB
TypeScript
import { BadRequestException, ConflictException, NotFoundException } from '@nestjs/common';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
vi.mock('../prisma/prisma-tenant.extension', () => ({
|
|
forTenant: vi.fn((db: any, tenantId: string) => db.__bound(tenantId)),
|
|
}));
|
|
|
|
import { AutodnsRateLimiter } from './autodns-client';
|
|
import { DomainsOrdersService } from './domains-orders.service';
|
|
|
|
/**
|
|
* Das Netz ist hier NUR `fetchImpl`: die Tests pruefen die tatsaechlich
|
|
* gesendete Anfrage (Methode, genaue Adresse, Kopfzeilen, Rumpf) mit Literalen
|
|
* — nie mit Werten, die der Produktivcode selbst baut.
|
|
*/
|
|
|
|
interface Call {
|
|
url: string;
|
|
init: { method: string; headers: Record<string, string>; body?: string; redirect: string };
|
|
}
|
|
|
|
type Responder = (call: Call) => Promise<unknown> | unknown;
|
|
|
|
function reply(status: number, body: unknown) {
|
|
return {
|
|
status,
|
|
headers: { get: () => null },
|
|
text: async () => (typeof body === 'string' ? body : JSON.stringify(body)),
|
|
};
|
|
}
|
|
|
|
function envelope(
|
|
data: unknown[],
|
|
extra: { type?: string; object?: unknown; messages?: unknown[] } = {},
|
|
) {
|
|
return {
|
|
status: { code: 'S0000', text: 'ok', type: extra.type ?? 'SUCCESS' },
|
|
stid: 'stid-1',
|
|
object: extra.object ?? { type: 'x', value: null, summary: data.length },
|
|
messages: extra.messages ?? [],
|
|
data,
|
|
};
|
|
}
|
|
|
|
const CREDENTIALS_DEMO = { environment: 'DEMO', user: 'api-user', password: 'geheim', context: 4 };
|
|
const CREDENTIALS_LIVE = { environment: 'LIVE', user: 'api-user', password: 'geheim', context: 4 };
|
|
|
|
interface OrderRow {
|
|
id: string;
|
|
tenantId: string;
|
|
environment: 'DEMO' | 'LIVE';
|
|
domainName: string;
|
|
openKey: string | null;
|
|
status: string;
|
|
payload: any;
|
|
jobId: string | null;
|
|
jobStatus: string | null;
|
|
errorText: string | null;
|
|
createdByUserId: string;
|
|
confirmedByUserId: string | null;
|
|
confirmedByUsername: string | null;
|
|
confirmedAt: Date | null;
|
|
lastCheckedAt: Date | null;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
function matches(row: Record<string, any>, where: Record<string, any> = {}): boolean {
|
|
return Object.entries(where).every(([key, expected]) => {
|
|
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;
|
|
}
|
|
return row[key] === expected;
|
|
});
|
|
}
|
|
|
|
const PAYLOAD = {
|
|
ownerContactId: 11,
|
|
adminContactId: 11,
|
|
techContactId: 22,
|
|
zoneContactId: 22,
|
|
nameServers: ['ns1.example.com', 'ns2.example.com'],
|
|
periodYears: 1,
|
|
price: { amount: 4.9, currency: 'EUR' },
|
|
availabilityCheckedAt: '2026-10-08T10:00:00.000Z',
|
|
};
|
|
|
|
/** Standard-Nameserver im Profil, bewusst nicht alphabetisch und nicht in Schluesselreihenfolge geliefert. */
|
|
const PROFILE_NAMESERVERS = [
|
|
'zeta.example.net',
|
|
'alpha.example.org',
|
|
'mid.example.com',
|
|
'beta.example.net',
|
|
];
|
|
|
|
function profileReply() {
|
|
return reply(
|
|
200,
|
|
envelope([
|
|
{
|
|
profiles: [
|
|
{ key: 'ns3', value: 'mid.example.com' },
|
|
{ key: 'ns1', value: 'Zeta.example.net.' },
|
|
{ key: 'techc', value: 'Max Muster' },
|
|
{ key: 'ns4', value: 'beta.example.net' },
|
|
{ key: 'ns2', value: 'alpha.example.org' },
|
|
],
|
|
},
|
|
]),
|
|
);
|
|
}
|
|
|
|
function makeHarness(opts: { environment?: 'DEMO' | 'LIVE'; timeoutMs?: number } = {}) {
|
|
const state = {
|
|
orders: [] as OrderRow[],
|
|
environment: opts.environment ?? ('DEMO' as 'DEMO' | 'LIVE'),
|
|
nextId: 1,
|
|
createConflict: false,
|
|
readBarrier: null as null | { remaining: number; release: () => void; gate: Promise<void> },
|
|
};
|
|
const calls: Call[] = [];
|
|
let responder: Responder = () => {
|
|
throw new Error('kein Netz erwartet');
|
|
};
|
|
|
|
// Das Benutzerprofil (Standard-Nameserver) hat einen eigenen, ueberschreibbaren Antwortgeber.
|
|
let profileResponder: Responder = () => profileReply();
|
|
|
|
const fetchImpl = vi.fn(async (url: string, init: any) => {
|
|
const call: Call = { url, init };
|
|
calls.push(call);
|
|
if (init.method === 'GET' && url.endsWith('/profile')) {
|
|
return (await profileResponder(call)) as never;
|
|
}
|
|
return (await responder(call)) as never;
|
|
});
|
|
|
|
const orderApi = {
|
|
findFirst: vi.fn(async ({ where }: any) => {
|
|
const found = state.orders.find((o) => matches(o, where)) ?? null;
|
|
const snapshot = found ? { ...found } : null;
|
|
if (state.readBarrier) {
|
|
const barrier = state.readBarrier;
|
|
barrier.remaining -= 1;
|
|
if (barrier.remaining <= 0) barrier.release();
|
|
await barrier.gate;
|
|
}
|
|
return snapshot;
|
|
}),
|
|
findMany: vi.fn(async ({ where, orderBy, take }: any) => {
|
|
let rows = state.orders.filter((o) => matches(o, where)).map((o) => ({ ...o }));
|
|
if (orderBy?.createdAt === 'desc') {
|
|
rows = rows.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
}
|
|
if (orderBy?.lastCheckedAt) {
|
|
rows = rows.sort(
|
|
(a, b) => (a.lastCheckedAt?.getTime() ?? -1) - (b.lastCheckedAt?.getTime() ?? -1),
|
|
);
|
|
}
|
|
return typeof take === 'number' ? rows.slice(0, take) : rows;
|
|
}),
|
|
create: vi.fn(async ({ data }: any) => {
|
|
if (state.createConflict) throw Object.assign(new Error('unique'), { code: 'P2002' });
|
|
const row: OrderRow = {
|
|
id: `o${state.nextId++}`,
|
|
jobId: null,
|
|
jobStatus: null,
|
|
errorText: null,
|
|
confirmedByUserId: null,
|
|
confirmedByUsername: null,
|
|
confirmedAt: null,
|
|
lastCheckedAt: null,
|
|
createdAt: new Date(Date.now() + state.nextId),
|
|
updatedAt: new Date(),
|
|
...data,
|
|
};
|
|
state.orders.push(row);
|
|
return { ...row };
|
|
}),
|
|
// 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));
|
|
// 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 };
|
|
}),
|
|
};
|
|
const db = { __bound: () => ({ domainsOrder: orderApi }) };
|
|
|
|
const limiter = new AutodnsRateLimiter(0);
|
|
const settings = {
|
|
transport: { fetchImpl, limiter, timeoutMs: opts.timeoutMs },
|
|
getStatus: vi.fn(async () => ({ environment: state.environment })),
|
|
getActiveCredentials: vi.fn(async () => ({
|
|
environment: state.environment,
|
|
credentials: state.environment === 'DEMO' ? CREDENTIALS_DEMO : CREDENTIALS_LIVE,
|
|
configVersion: 1,
|
|
})),
|
|
};
|
|
const contacts = [
|
|
{ id: 11, displayName: 'Muster GmbH' },
|
|
{ id: 22, displayName: 'Erika Muster' },
|
|
];
|
|
const directory = {
|
|
findContactsByIds: vi.fn(async (_t: string, ids: number[]) =>
|
|
contacts.filter((c) => ids.includes(c.id)),
|
|
),
|
|
invalidate: vi.fn(),
|
|
};
|
|
const service = new DomainsOrdersService(db as any, settings as any, directory as any);
|
|
|
|
const addOrder = (patch: Partial<OrderRow> = {}): OrderRow => {
|
|
const row: OrderRow = {
|
|
id: `o${state.nextId++}`,
|
|
tenantId: 't1',
|
|
environment: 'DEMO',
|
|
domainName: 'beispiel.de',
|
|
openKey: 'beispiel.de',
|
|
status: 'DRAFT',
|
|
payload: { ...PAYLOAD },
|
|
jobId: null,
|
|
jobStatus: null,
|
|
errorText: null,
|
|
createdByUserId: 'u1',
|
|
confirmedByUserId: null,
|
|
confirmedByUsername: null,
|
|
confirmedAt: null,
|
|
lastCheckedAt: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
...patch,
|
|
};
|
|
state.orders.push(row);
|
|
return row;
|
|
};
|
|
|
|
return {
|
|
service,
|
|
state,
|
|
calls,
|
|
fetchImpl,
|
|
orderApi,
|
|
settings,
|
|
directory,
|
|
addOrder,
|
|
respond: (r: Responder) => {
|
|
responder = r;
|
|
},
|
|
respondProfile: (r: Responder) => {
|
|
profileResponder = r;
|
|
},
|
|
};
|
|
}
|
|
|
|
function studioReply(domain: string, status: string, prices?: unknown[]) {
|
|
return reply(
|
|
200,
|
|
envelope([
|
|
{
|
|
domain,
|
|
services: {
|
|
whois: { data: { status } },
|
|
...(prices ? { price: { data: { prices } } } : {}),
|
|
},
|
|
},
|
|
]),
|
|
);
|
|
}
|
|
|
|
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 },
|
|
ownerc: { id: 11 },
|
|
adminc: { id: 11 },
|
|
techc: { id: 22 },
|
|
zonec: { id: 22 },
|
|
nameServers: [{ name: 'ns1.example.com' }, { name: 'ns2.example.com' }],
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe('checkAvailability', () => {
|
|
it('lehnt einen ungueltigen Namen ab, ohne das Netz zu beruehren', async () => {
|
|
const h = makeHarness();
|
|
await expect(h.service.checkAvailability('t1', 'bei spiel')).rejects.toMatchObject({
|
|
response: { code: 'invalidDomain' },
|
|
});
|
|
expect(h.fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('sendet genau einen POST /domainstudio mit dem erwarteten Rumpf', async () => {
|
|
const h = makeHarness();
|
|
h.respond(() =>
|
|
studioReply('beispiel.de', 'FREE', [
|
|
{ amount: 4.9, currency: 'EUR', period: { unit: 'YEAR', period: 1 } },
|
|
]),
|
|
);
|
|
const result = await h.service.checkAvailability('t1', ' https://Beispiel.DE/x ');
|
|
expect(h.calls).toHaveLength(1);
|
|
expect(h.calls[0].url).toBe('https://api.demo.autodns.com/v1/domainstudio');
|
|
expect(h.calls[0].init.method).toBe('POST');
|
|
expect(JSON.parse(h.calls[0].init.body as string)).toEqual({
|
|
searchToken: 'beispiel.de',
|
|
currency: 'EUR',
|
|
sources: { custom: { domains: ['beispiel.de'], services: ['WHOIS', 'PRICE'] } },
|
|
});
|
|
expect(result).toMatchObject({
|
|
domain: 'beispiel.de',
|
|
available: true,
|
|
whoisStatus: 'FREE',
|
|
price: { amount: 4.9, currency: 'EUR' },
|
|
});
|
|
});
|
|
|
|
it('ASSIGNED ist nicht bestellbar', async () => {
|
|
const h = makeHarness();
|
|
h.respond(() => studioReply('beispiel.de', 'ASSIGNED'));
|
|
const result = await h.service.checkAvailability('t1', 'beispiel.de');
|
|
expect(result).toMatchObject({ available: false, whoisStatus: 'ASSIGNED', price: null });
|
|
});
|
|
|
|
it.each([
|
|
'ERROR',
|
|
'TIMEOUT',
|
|
'PREMIUM',
|
|
'RESERVED',
|
|
])('%s ist nicht bestellbar', async (status) => {
|
|
const h = makeHarness();
|
|
h.respond(() => studioReply('beispiel.de', status));
|
|
const result = await h.service.checkAvailability('t1', 'beispiel.de');
|
|
expect(result).toMatchObject({ available: false, whoisStatus: status });
|
|
});
|
|
|
|
it('der Umschlag einer anderen Domain zaehlt nicht', async () => {
|
|
const h = makeHarness();
|
|
h.respond(() => studioReply('beispiel.com', 'FREE'));
|
|
const result = await h.service.checkAvailability('t1', 'beispiel.de');
|
|
expect(result).toMatchObject({ available: false, whoisStatus: 'NO_RESULT' });
|
|
});
|
|
|
|
it('ein AutoDNS-Fehler wird zu 502', async () => {
|
|
const h = makeHarness();
|
|
h.respond(() =>
|
|
reply(401, envelope([], { type: 'ERROR', messages: [{ text: 'x', status: 'ERROR' }] })),
|
|
);
|
|
await expect(h.service.checkAvailability('t1', 'beispiel.de')).rejects.toMatchObject({
|
|
status: 502,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('createOrder', () => {
|
|
const dto = {
|
|
domain: 'Beispiel.de',
|
|
ownerContactId: 11,
|
|
adminContactId: 11,
|
|
techContactId: 22,
|
|
zoneContactId: 22,
|
|
};
|
|
|
|
function free() {
|
|
return studioReply('beispiel.de', 'FREE', [
|
|
{ amount: 4.9, currency: 'EUR', period: { unit: 'YEAR', period: 1 } },
|
|
]);
|
|
}
|
|
|
|
it('legt einen Entwurf im aktiven System an und liefert die Zusammenfassung', async () => {
|
|
const h = makeHarness();
|
|
h.respond(free);
|
|
const result = await h.service.createOrder('t1', 'u1', dto as any);
|
|
expect(h.state.orders).toHaveLength(1);
|
|
expect(h.state.orders[0]).toMatchObject({
|
|
tenantId: 't1',
|
|
environment: 'DEMO',
|
|
domainName: 'beispiel.de',
|
|
openKey: 'beispiel.de',
|
|
status: 'DRAFT',
|
|
createdByUserId: 'u1',
|
|
});
|
|
expect(h.state.orders[0].payload).toMatchObject({
|
|
ownerContactId: 11,
|
|
adminContactId: 11,
|
|
techContactId: 22,
|
|
zoneContactId: 22,
|
|
nameServers: PROFILE_NAMESERVERS,
|
|
periodYears: 1,
|
|
price: { amount: 4.9, currency: 'EUR' },
|
|
});
|
|
expect(typeof h.state.orders[0].payload.availabilityCheckedAt).toBe('string');
|
|
expect(result.contacts.owner).toEqual({ id: 11, displayName: 'Muster GmbH' });
|
|
expect(result.contacts.zone).toEqual({ id: 22, displayName: 'Erika Muster' });
|
|
expect(result.nameServers).toEqual(PROFILE_NAMESERVERS);
|
|
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);
|
|
});
|
|
|
|
it('nicht freie Domain -> 409 notAvailable, keine Zeile', async () => {
|
|
const h = makeHarness();
|
|
h.respond(() => studioReply('beispiel.de', 'ASSIGNED'));
|
|
await expect(h.service.createOrder('t1', 'u1', dto as any)).rejects.toMatchObject({
|
|
status: 409,
|
|
response: { code: 'notAvailable' },
|
|
});
|
|
expect(h.state.orders).toHaveLength(0);
|
|
});
|
|
|
|
it('ein vom Browser gesendeter Nameserver-Satz wird ignoriert (h3t, D-03)', async () => {
|
|
const h = makeHarness();
|
|
h.respond(free);
|
|
const result = await h.service.createOrder('t1', 'u1', {
|
|
...dto,
|
|
nameServers: ['evil.example.com', 'x.example.com'],
|
|
} as any);
|
|
expect(result.nameServers).toEqual(PROFILE_NAMESERVERS);
|
|
expect(h.state.orders[0].payload.nameServers).toEqual(PROFILE_NAMESERVERS);
|
|
});
|
|
|
|
it('Profil ohne Nameserver -> 409 noDefaultNameServers, keine Zeile, keine Verfuegbarkeitsabfrage', async () => {
|
|
const h = makeHarness();
|
|
h.respond(free);
|
|
h.respondProfile(() =>
|
|
reply(200, envelope([{ profiles: [{ key: 'techc', value: 'Max Muster' }] }])),
|
|
);
|
|
await expect(h.service.createOrder('t1', 'u1', dto as any)).rejects.toMatchObject({
|
|
status: 409,
|
|
response: { code: 'noDefaultNameServers' },
|
|
});
|
|
expect(h.state.orders).toHaveLength(0);
|
|
expect(h.calls.some((c) => c.url.endsWith('/domainstudio'))).toBe(false);
|
|
});
|
|
|
|
it('Profil mit nur einem Nameserver -> 409 noDefaultNameServers', async () => {
|
|
const h = makeHarness();
|
|
h.respond(free);
|
|
h.respondProfile(() =>
|
|
reply(200, envelope([{ profiles: [{ key: 'ns1', value: 'a.example.net' }] }])),
|
|
);
|
|
await expect(h.service.createOrder('t1', 'u1', dto as any)).rejects.toMatchObject({
|
|
response: { code: 'noDefaultNameServers' },
|
|
});
|
|
expect(h.state.orders).toHaveLength(0);
|
|
});
|
|
|
|
it('Profilabfrage mit HTTP 500 -> abgelehnt, keine Zeile', async () => {
|
|
const h = makeHarness();
|
|
h.respond(free);
|
|
h.respondProfile(() => reply(500, 'kaputt'));
|
|
await expect(h.service.createOrder('t1', 'u1', dto as any)).rejects.toBeDefined();
|
|
expect(h.state.orders).toHaveLength(0);
|
|
});
|
|
|
|
it('ein vorhandener Entwurf liest das Profil neu und bekommt einen neuen Stand (WR-02)', async () => {
|
|
const h = makeHarness();
|
|
const existing = h.addOrder();
|
|
const before = existing.updatedAt.toISOString();
|
|
h.respond(free);
|
|
const result = await h.service.createOrder('t1', 'u1', dto as any);
|
|
expect(h.calls.filter((c) => c.url.endsWith('/profile'))).toHaveLength(1);
|
|
expect(result.nameServers).toEqual(PROFILE_NAMESERVERS);
|
|
expect(result.version).not.toBe(before);
|
|
});
|
|
|
|
it('fehlender Kontakt -> BadRequest', async () => {
|
|
const h = makeHarness();
|
|
h.respond(free);
|
|
await expect(
|
|
h.service.createOrder('t1', 'u1', { ...dto, techContactId: undefined } as any),
|
|
).rejects.toBeInstanceOf(BadRequestException);
|
|
expect(h.state.orders).toHaveLength(0);
|
|
});
|
|
|
|
it('ein Kontakt, den AutoDNS nicht kennt -> BadRequest contactNotFound', async () => {
|
|
const h = makeHarness();
|
|
h.respond(free);
|
|
await expect(
|
|
h.service.createOrder('t1', 'u1', { ...dto, zoneContactId: 999 } as any),
|
|
).rejects.toMatchObject({ response: { code: 'contactNotFound' } });
|
|
});
|
|
|
|
it('ein vorhandener Entwurf derselben Domain wird aktualisiert (gleiche Nummer)', async () => {
|
|
const h = makeHarness();
|
|
const existing = h.addOrder({ payload: { ...PAYLOAD, ownerContactId: 22 } });
|
|
h.respond(free);
|
|
const result = await h.service.createOrder('t1', 'u1', dto as any);
|
|
expect(result.order.id).toBe(existing.id);
|
|
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([
|
|
'SUBMITTING',
|
|
'SUBMITTED',
|
|
'UNKNOWN',
|
|
'SUCCESS',
|
|
])('bestehender Auftrag im Zustand %s -> 409 orderOpen, ohne Netz', async (status) => {
|
|
const h = makeHarness();
|
|
h.addOrder({ status });
|
|
await expect(h.service.createOrder('t1', 'u1', dto as any)).rejects.toMatchObject({
|
|
status: 409,
|
|
response: { code: 'orderOpen' },
|
|
});
|
|
expect(h.fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('Wettlauf (P2002) -> 409 orderOpen', async () => {
|
|
const h = makeHarness();
|
|
h.respond(free);
|
|
h.state.createConflict = true;
|
|
await expect(h.service.createOrder('t1', 'u1', dto as any)).rejects.toMatchObject({
|
|
response: { code: 'orderOpen' },
|
|
});
|
|
});
|
|
|
|
it('ein FAILED/CANCELED-Auftrag (openKey null) blockiert nicht', async () => {
|
|
const h = makeHarness();
|
|
h.addOrder({ status: 'FAILED', openKey: null });
|
|
h.respond(free);
|
|
const result = await h.service.createOrder('t1', 'u1', dto as any);
|
|
expect(result.order.status).toBe('DRAFT');
|
|
expect(h.state.orders).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
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,
|
|
envelope([{ id: 987, status: 'RUNNING' }], {
|
|
object: { type: 'job', value: '987', summary: 1 },
|
|
}),
|
|
),
|
|
);
|
|
|
|
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];
|
|
expect(claim.where).toMatchObject({
|
|
id: order.id,
|
|
tenantId: 't1',
|
|
status: 'DRAFT',
|
|
environment: 'DEMO',
|
|
updatedAt: seenVersion,
|
|
});
|
|
expect(claim.data).toMatchObject({
|
|
status: 'SUBMITTING',
|
|
confirmedByUserId: 'u1',
|
|
confirmedByUsername: 'anna',
|
|
});
|
|
expect(claim.data.confirmedAt).toBeInstanceOf(Date);
|
|
|
|
expect(h.calls).toHaveLength(1);
|
|
expect(h.calls[0].url).toBe('https://api.demo.autodns.com/v1/domain');
|
|
expect(h.calls[0].init.method).toBe('POST');
|
|
expect(h.calls[0].init.redirect).toBe('error');
|
|
expect(h.calls[0].init.headers).toEqual({
|
|
Authorization: 'Basic YXBpLXVzZXI6Z2VoZWlt',
|
|
'X-Domainrobot-Context': '4',
|
|
Accept: 'application/json',
|
|
'Content-Type': 'application/json',
|
|
'User-Agent': expect.stringMatching(/^Tessera\//),
|
|
});
|
|
expect(JSON.parse(h.calls[0].init.body as string)).toEqual(EXPECTED_SUBMIT_BODY);
|
|
|
|
expect(view).toMatchObject({ status: 'SUBMITTED', jobStatus: 'RUNNING' });
|
|
expect(h.state.orders[0]).toMatchObject({
|
|
status: 'SUBMITTED',
|
|
jobId: '987',
|
|
jobStatus: 'RUNNING',
|
|
confirmedByUsername: 'anna',
|
|
openKey: 'beispiel.de',
|
|
});
|
|
expect(h.directory.invalidate).toHaveBeenCalledWith('t1');
|
|
});
|
|
|
|
it('zwei gleichzeitige Bestaetigungen: genau ein POST, eine 409 alreadySubmitted', async () => {
|
|
const h = makeHarness();
|
|
const order = h.addOrder();
|
|
// Beide Aufrufe lesen noch den Entwurf, bevor einer den Anspruch stellt:
|
|
// so beweist der Test den Zaehler (count === 1), nicht die Vorabpruefung.
|
|
let release!: () => void;
|
|
const gate = new Promise<void>((resolve) => {
|
|
release = resolve;
|
|
});
|
|
h.state.readBarrier = { remaining: 2, release, gate };
|
|
h.respond(async () => {
|
|
await new Promise((r) => setTimeout(r, 5));
|
|
return reply(200, envelope([{ id: 987, status: 'RUNNING' }]));
|
|
});
|
|
|
|
const results = await Promise.allSettled([
|
|
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');
|
|
const rejected = results.filter((r) => r.status === 'rejected') as PromiseRejectedResult[];
|
|
expect(ok).toHaveLength(1);
|
|
expect(rejected).toHaveLength(1);
|
|
expect(rejected[0].reason).toBeInstanceOf(ConflictException);
|
|
expect(rejected[0].reason.getResponse()).toMatchObject({ code: 'alreadySubmitted' });
|
|
expect(h.calls.filter((c) => c.url.endsWith('/domain'))).toHaveLength(1);
|
|
});
|
|
|
|
it('Systemwechsel nach dem Entwurf -> 409 environmentChanged, kein Netz, Entwurf bleibt', async () => {
|
|
const h = makeHarness({ environment: 'DEMO' });
|
|
const order = h.addOrder({ environment: 'DEMO' });
|
|
h.state.environment = 'LIVE';
|
|
await expect(
|
|
h.service.submitOrder('t1', USER, order.id, versionOf(order)),
|
|
).rejects.toMatchObject({
|
|
status: 409,
|
|
response: { code: 'environmentChanged' },
|
|
});
|
|
expect(h.fetchImpl).not.toHaveBeenCalled();
|
|
expect(h.state.orders[0].status).toBe('DRAFT');
|
|
});
|
|
|
|
it('Systemwechsel zwischen Lesen und Anspruch -> der Anspruch greift nicht, kein Netz', async () => {
|
|
const h = makeHarness({ environment: 'DEMO' });
|
|
const order = h.addOrder({ environment: 'LIVE' }); // gehoert zu LIVE, aktiv ist DEMO
|
|
// Vorabpruefung umgehen: die Zeile gibt sich beim Lesen als DEMO aus.
|
|
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;
|
|
return reads === 1 && row ? { ...row, environment: 'DEMO' } : row;
|
|
});
|
|
await expect(
|
|
h.service.submitOrder('t1', USER, order.id, versionOf(order)),
|
|
).rejects.toMatchObject({
|
|
response: { code: 'environmentChanged' },
|
|
});
|
|
expect(h.fetchImpl).not.toHaveBeenCalled();
|
|
expect(h.state.orders[0].status).toBe('DRAFT');
|
|
});
|
|
|
|
it.each([
|
|
'SUBMITTING',
|
|
'SUBMITTED',
|
|
'UNKNOWN',
|
|
'SUCCESS',
|
|
'FAILED',
|
|
'CANCELED',
|
|
])('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, versionOf(order)),
|
|
).rejects.toMatchObject({
|
|
status: 409,
|
|
response: { code: 'alreadySubmitted' },
|
|
});
|
|
expect(h.fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('fremde Nummer -> 404, kein Netz', async () => {
|
|
const h = makeHarness();
|
|
h.addOrder({ tenantId: 'andere' });
|
|
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();
|
|
});
|
|
});
|
|
|
|
describe('submitOrder — Ausgang', () => {
|
|
it('Zeitablauf -> UNKNOWN, genau ein Aufruf, lastCheckedAt bleibt leer', async () => {
|
|
const h = makeHarness({ timeoutMs: 20 });
|
|
const order = h.addOrder();
|
|
h.respond(() => new Promise(() => undefined));
|
|
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');
|
|
expect(h.state.orders[0].lastCheckedAt).toBeNull();
|
|
expect(h.state.orders[0].openKey).toBe('beispiel.de');
|
|
});
|
|
|
|
it('Verbindungsabbruch (ECONNRESET) -> UNKNOWN', async () => {
|
|
const h = makeHarness();
|
|
const order = h.addOrder();
|
|
h.respond(() => {
|
|
throw Object.assign(new Error('reset'), { code: 'ECONNRESET' });
|
|
});
|
|
const view = await h.service.submitOrder('t1', USER, order.id, versionOf(order));
|
|
expect(view.status).toBe('UNKNOWN');
|
|
expect(h.calls).toHaveLength(1);
|
|
});
|
|
|
|
it('HTTP 200 mit HTML -> UNKNOWN', async () => {
|
|
const h = makeHarness();
|
|
const order = h.addOrder();
|
|
h.respond(() => reply(200, '<html>Wartung</html>'));
|
|
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, 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 () => {
|
|
const h = makeHarness();
|
|
const order = h.addOrder();
|
|
h.respond(() =>
|
|
reply(
|
|
200,
|
|
envelope([], {
|
|
type: 'ERROR',
|
|
messages: [{ code: 'E1', text: 'Domain not available', status: 'ERROR' }],
|
|
}),
|
|
),
|
|
);
|
|
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();
|
|
expect(h.directory.invalidate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('HTTP 401 -> FAILED mit dem Anmeldetext, openKey leer', async () => {
|
|
const h = makeHarness();
|
|
const order = h.addOrder();
|
|
h.respond(() =>
|
|
reply(
|
|
401,
|
|
envelope([], {
|
|
type: 'ERROR',
|
|
messages: [{ code: 'EF00202', text: 'bad login', status: 'ERROR' }],
|
|
}),
|
|
),
|
|
);
|
|
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();
|
|
});
|
|
|
|
it('Erfolg ohne Auftragsnummer -> SUBMITTED mit jobId null', async () => {
|
|
const h = makeHarness();
|
|
const order = h.addOrder();
|
|
h.respond(() =>
|
|
reply(200, envelope([{}], { object: { type: 'domain', value: 'beispiel.de', summary: 1 } })),
|
|
);
|
|
const view = await h.service.submitOrder('t1', USER, order.id, versionOf(order));
|
|
expect(view.status).toBe('SUBMITTED');
|
|
expect(h.state.orders[0].jobId).toBeNull();
|
|
});
|
|
|
|
it('nach UNKNOWN kann derselbe Auftrag nie erneut abgeschickt werden', async () => {
|
|
const h = makeHarness({ timeoutMs: 20 });
|
|
const order = h.addOrder();
|
|
h.respond(() => new Promise(() => undefined));
|
|
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();
|
|
h.addOrder({ status: 'SUBMITTED', jobId: '987', jobStatus: 'RUNNING' });
|
|
h.respond(() => reply(200, envelope([{ job: { id: 987, status: 'SUCCESS' } }])));
|
|
await h.service.refreshOpenOrders('t1');
|
|
expect(h.calls).toHaveLength(1);
|
|
expect(h.calls[0].url).toBe('https://api.demo.autodns.com/v1/job/987');
|
|
expect(h.calls[0].init.method).toBe('GET');
|
|
expect(h.state.orders[0]).toMatchObject({ status: 'SUCCESS', openKey: 'beispiel.de' });
|
|
expect(h.directory.invalidate).toHaveBeenCalledWith('t1');
|
|
});
|
|
|
|
it('abgeschlossener Auftrag ist bei AutoDNS geloescht (404): Domain vorhanden -> SUCCESS', async () => {
|
|
const h = makeHarness();
|
|
h.addOrder({ status: 'SUBMITTED', jobId: '987', jobStatus: 'WAIT' });
|
|
h.respond((call) =>
|
|
call.url.endsWith('/job/987')
|
|
? reply(
|
|
404,
|
|
envelope([], {
|
|
type: 'ERROR',
|
|
messages: [{ code: 'EF300101', text: 'Auftrag konnte nicht gefunden werden.' }],
|
|
}),
|
|
)
|
|
: reply(200, envelope([{ name: 'beispiel.de' }])),
|
|
);
|
|
await h.service.refreshOpenOrders('t1');
|
|
expect(h.calls.map((c) => c.url)).toEqual([
|
|
'https://api.demo.autodns.com/v1/job/987',
|
|
'https://api.demo.autodns.com/v1/domain/beispiel.de',
|
|
]);
|
|
expect(h.state.orders[0]).toMatchObject({ status: 'SUCCESS', jobStatus: 'SUCCESS' });
|
|
});
|
|
|
|
it('Auftrag geloescht und Domain unbekannt: bleibt offen', async () => {
|
|
const h = makeHarness();
|
|
h.addOrder({ status: 'SUBMITTED', jobId: '987', jobStatus: 'WAIT' });
|
|
h.respond(() => reply(404, envelope([], { type: 'ERROR' })));
|
|
await h.service.refreshOpenOrders('t1');
|
|
expect(h.state.orders[0]).toMatchObject({ status: 'SUBMITTED', jobStatus: 'WAIT' });
|
|
});
|
|
|
|
it('Auftrag nicht abrufbar wegen Serverfehler: keine Domainabfrage, bleibt offen', async () => {
|
|
const h = makeHarness();
|
|
h.addOrder({ status: 'SUBMITTED', jobId: '987', jobStatus: 'WAIT' });
|
|
h.respond(() => reply(502, 'Bad Gateway'));
|
|
await h.service.refreshOpenOrders('t1');
|
|
expect(h.calls).toHaveLength(1);
|
|
expect(h.state.orders[0]).toMatchObject({ status: 'SUBMITTED' });
|
|
});
|
|
|
|
it('FAILED -> FAILED mit Text, openKey leer', async () => {
|
|
const h = makeHarness();
|
|
h.addOrder({ status: 'SUBMITTED', jobId: '987' });
|
|
h.respond(() =>
|
|
reply(
|
|
200,
|
|
envelope([
|
|
{
|
|
job: {
|
|
id: 987,
|
|
status: 'FAILED',
|
|
events: [{ status: 'ERROR', message: 'Nameserver nicht erreichbar' }],
|
|
},
|
|
},
|
|
]),
|
|
),
|
|
);
|
|
await h.service.refreshOpenOrders('t1');
|
|
expect(h.state.orders[0]).toMatchObject({ status: 'FAILED', openKey: null });
|
|
expect(h.state.orders[0].errorText).toContain('Nameserver nicht erreichbar');
|
|
});
|
|
|
|
it('CANCELED -> CANCELED, openKey leer', async () => {
|
|
const h = makeHarness();
|
|
h.addOrder({ status: 'SUBMITTED', jobId: '987' });
|
|
h.respond(() => reply(200, envelope([{ job: { id: 987, status: 'CANCELED' } }])));
|
|
await h.service.refreshOpenOrders('t1');
|
|
expect(h.state.orders[0]).toMatchObject({ status: 'CANCELED', openKey: null });
|
|
});
|
|
|
|
it('RUNNING bleibt SUBMITTED, lastCheckedAt wird gesetzt', async () => {
|
|
const h = makeHarness();
|
|
h.addOrder({ status: 'SUBMITTED', jobId: '987' });
|
|
h.respond(() => reply(200, envelope([{ job: { id: 987, status: 'RUNNING' } }])));
|
|
await h.service.refreshOpenOrders('t1');
|
|
expect(h.state.orders[0]).toMatchObject({ status: 'SUBMITTED', jobStatus: 'RUNNING' });
|
|
expect(h.state.orders[0].lastCheckedAt).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it('SUPPORT bleibt SUBMITTED mit jobStatus SUPPORT', async () => {
|
|
const h = makeHarness();
|
|
h.addOrder({ status: 'SUBMITTED', jobId: '987' });
|
|
h.respond(() => reply(200, envelope([{ job: { id: 987, status: 'SUPPORT' } }])));
|
|
await h.service.refreshOpenOrders('t1');
|
|
expect(h.state.orders[0]).toMatchObject({ status: 'SUBMITTED', jobStatus: 'SUPPORT' });
|
|
});
|
|
|
|
it('SUBMITTING seit 3 Minuten -> UNKNOWN, seit 30 s -> unveraendert', async () => {
|
|
const h = makeHarness();
|
|
h.addOrder({ id: 'alt', status: 'SUBMITTING', confirmedAt: new Date(Date.now() - 3 * 60_000) });
|
|
h.addOrder({
|
|
id: 'neu',
|
|
domainName: 'neu.de',
|
|
openKey: 'neu.de',
|
|
status: 'SUBMITTING',
|
|
confirmedAt: new Date(Date.now() - 30_000),
|
|
});
|
|
await h.service.refreshOpenOrders('t1');
|
|
expect(h.state.orders.find((o) => o.id === 'alt')?.status).toBe('UNKNOWN');
|
|
expect(h.state.orders.find((o) => o.id === 'neu')?.status).toBe('SUBMITTING');
|
|
// Der gerade unklar gewordene Auftrag wird sofort abgeglichen; der junge nicht.
|
|
expect(h.calls.map((c) => c.url)).toEqual([
|
|
'https://api.demo.autodns.com/v1/domain/beispiel.de',
|
|
]);
|
|
});
|
|
|
|
describe('UNKNOWN-Abgleich', () => {
|
|
const confirmedAt = new Date(Date.now() - 10 * 60_000);
|
|
|
|
it('GET /domain/{name} gelingt -> SUCCESS', async () => {
|
|
const h = makeHarness();
|
|
h.addOrder({ status: 'UNKNOWN', confirmedAt });
|
|
h.respond(() => reply(200, envelope([{ name: 'beispiel.de' }])));
|
|
await h.service.refreshOpenOrders('t1');
|
|
expect(h.calls).toHaveLength(1);
|
|
expect(h.calls[0].url).toBe('https://api.demo.autodns.com/v1/domain/beispiel.de');
|
|
expect(h.state.orders[0].status).toBe('SUCCESS');
|
|
expect(h.directory.invalidate).toHaveBeenCalledWith('t1');
|
|
});
|
|
|
|
it('nicht gefunden, aber Auftrag nach der Bestaetigung -> SUBMITTED mit dieser Nummer', async () => {
|
|
const h = makeHarness();
|
|
h.addOrder({ status: 'UNKNOWN', confirmedAt });
|
|
h.respond((call) => {
|
|
if (call.url.endsWith('/domain/beispiel.de')) {
|
|
return reply(
|
|
404,
|
|
envelope([], { type: 'ERROR', messages: [{ text: 'not found', status: 'ERROR' }] }),
|
|
);
|
|
}
|
|
return reply(
|
|
200,
|
|
envelope([
|
|
{
|
|
job: {
|
|
id: 555,
|
|
status: 'RUNNING',
|
|
created: new Date(Date.now() - 9 * 60_000).toISOString(),
|
|
},
|
|
object: 'beispiel.de',
|
|
},
|
|
{
|
|
job: { id: 1, status: 'FAILED', created: '2020-01-01T00:00:00Z' },
|
|
object: 'beispiel.de',
|
|
},
|
|
]),
|
|
);
|
|
});
|
|
await h.service.refreshOpenOrders('t1');
|
|
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(JSON.parse(h.calls[1].init.body as string)).toEqual({
|
|
filters: [{ key: 'object', operator: 'EQUAL', value: 'beispiel.de' }],
|
|
view: { limit: 10, offset: 0 },
|
|
orders: [{ key: 'created', type: 'DESC' }],
|
|
});
|
|
expect(h.state.orders[0]).toMatchObject({
|
|
status: 'SUBMITTED',
|
|
jobId: '555',
|
|
jobStatus: 'RUNNING',
|
|
});
|
|
});
|
|
|
|
it('nichts gefunden -> bleibt UNKNOWN, lastCheckedAt gesetzt', 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: 'not found', status: 'ERROR' }] }),
|
|
)
|
|
: reply(200, envelope([])),
|
|
);
|
|
await h.service.refreshOpenOrders('t1');
|
|
expect(h.state.orders[0].status).toBe('UNKNOWN');
|
|
expect(h.state.orders[0].lastCheckedAt).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it('ein Fehler beim Nachsehen (Zeitablauf) setzt lastCheckedAt NICHT', async () => {
|
|
const h = makeHarness({ timeoutMs: 20 });
|
|
h.addOrder({ status: 'UNKNOWN', confirmedAt });
|
|
h.respond(() => new Promise(() => undefined));
|
|
await h.service.refreshOpenOrders('t1');
|
|
expect(h.state.orders[0].status).toBe('UNKNOWN');
|
|
expect(h.state.orders[0].lastCheckedAt).toBeNull();
|
|
});
|
|
|
|
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) =>
|
|
call.url.endsWith('/domain/beispiel.de')
|
|
? reply(404, envelope([], { type: 'ERROR', messages: [{ text: 'nf', status: 'ERROR' }] }))
|
|
: reply(200, envelope([{ job: { id: 7, status: 'FAILED' }, object: 'beispiel.de' }])),
|
|
);
|
|
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();
|
|
});
|
|
});
|
|
|
|
it('prueft hoechstens 20 Auftraege, die am laengsten ungeprueften zuerst', async () => {
|
|
const h = makeHarness();
|
|
for (let i = 0; i < 25; i += 1) {
|
|
h.addOrder({
|
|
id: `x${i}`,
|
|
domainName: `d${i}.de`,
|
|
openKey: `d${i}.de`,
|
|
status: 'SUBMITTED',
|
|
jobId: String(100 + i),
|
|
lastCheckedAt: new Date(Date.now() - (25 - i) * 1000),
|
|
});
|
|
}
|
|
h.respond(() => reply(200, envelope([{ job: { status: 'RUNNING' } }])));
|
|
await h.service.refreshOpenOrders('t1');
|
|
expect(h.calls).toHaveLength(20);
|
|
// x0 ist am aeltesten geprueft, x24 am juengsten -> x20..x24 bleiben liegen.
|
|
const urls = h.calls.map((c) => c.url);
|
|
expect(urls[0]).toBe('https://api.demo.autodns.com/v1/job/100');
|
|
expect(urls).not.toContain('https://api.demo.autodns.com/v1/job/124');
|
|
});
|
|
|
|
it('ein AutoDNS-Fehler bei einem Auftrag haelt die anderen nicht auf', async () => {
|
|
const h = makeHarness();
|
|
h.addOrder({ id: 'a', domainName: 'a.de', openKey: 'a.de', status: 'SUBMITTED', jobId: '1' });
|
|
h.addOrder({ id: 'b', domainName: 'b.de', openKey: 'b.de', status: 'SUBMITTED', jobId: '2' });
|
|
h.respond((call) =>
|
|
call.url.endsWith('/job/1')
|
|
? reply(500, 'kaputt')
|
|
: reply(200, envelope([{ job: { id: 2, status: 'SUCCESS' } }])),
|
|
);
|
|
await h.service.refreshOpenOrders('t1');
|
|
expect(h.state.orders.find((o) => o.id === 'b')?.status).toBe('SUCCESS');
|
|
expect(h.state.orders.find((o) => o.id === 'a')?.status).toBe('SUBMITTED');
|
|
});
|
|
|
|
it('Auftraege des anderen Systems werden nicht angefasst', async () => {
|
|
const h = makeHarness({ environment: 'LIVE' });
|
|
h.addOrder({ environment: 'DEMO', status: 'SUBMITTED', jobId: '9' });
|
|
await h.service.refreshOpenOrders('t1');
|
|
expect(h.fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('cancelOrder', () => {
|
|
it('DRAFT -> CANCELED, openKey leer', async () => {
|
|
const h = makeHarness();
|
|
const order = h.addOrder();
|
|
const view = await h.service.cancelOrder('t1', order.id);
|
|
expect(view.status).toBe('CANCELED');
|
|
expect(h.state.orders[0]).toMatchObject({ status: 'CANCELED', openKey: null });
|
|
});
|
|
|
|
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',
|
|
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: Domain inzwischen bei AutoDNS vorhanden -> 409 orderFound, Auftrag wird SUCCESS', async () => {
|
|
const h = makeHarness();
|
|
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',
|
|
'SUCCESS',
|
|
'FAILED',
|
|
'CANCELED',
|
|
])('%s -> 409 notCancelable', async (status) => {
|
|
const h = makeHarness();
|
|
const order = h.addOrder({ status });
|
|
await expect(h.service.cancelOrder('t1', order.id)).rejects.toMatchObject({
|
|
response: { code: 'notCancelable' },
|
|
});
|
|
});
|
|
|
|
it('fremde Nummer -> 404', async () => {
|
|
const h = makeHarness();
|
|
h.addOrder({ tenantId: 'andere' });
|
|
await expect(h.service.cancelOrder('t1', 'o1')).rejects.toBeInstanceOf(NotFoundException);
|
|
});
|
|
});
|
|
|
|
describe('listOrders', () => {
|
|
it('liefert die neuesten zuerst mit den Anzeigefeldern', async () => {
|
|
const h = makeHarness();
|
|
h.addOrder({
|
|
id: 'alt',
|
|
domainName: 'alt.de',
|
|
openKey: null,
|
|
status: 'FAILED',
|
|
errorText: 'Fehler',
|
|
createdAt: new Date('2026-10-01T10:00:00Z'),
|
|
});
|
|
h.addOrder({
|
|
id: 'neu',
|
|
domainName: 'xn--mller-kva.de',
|
|
openKey: 'xn--mller-kva.de',
|
|
status: 'SUCCESS',
|
|
confirmedByUsername: 'anna',
|
|
confirmedAt: new Date('2026-10-08T09:00:00Z'),
|
|
createdAt: new Date('2026-10-08T08:00:00Z'),
|
|
});
|
|
const list = await h.service.listOrders('t1');
|
|
expect(list.map((o) => o.id)).toEqual(['neu', 'alt']);
|
|
expect(list[0]).toEqual({
|
|
id: 'neu',
|
|
environment: 'DEMO',
|
|
domainName: 'xn--mller-kva.de',
|
|
domainNameUnicode: 'müller.de',
|
|
status: 'SUCCESS',
|
|
jobStatus: null,
|
|
errorText: null,
|
|
confirmedByUsername: 'anna',
|
|
confirmedAt: '2026-10-08T09:00:00.000Z',
|
|
createdAt: '2026-10-08T08:00:00.000Z',
|
|
lastCheckedAt: null,
|
|
price: { amount: 4.9, currency: 'EUR' },
|
|
});
|
|
expect(h.orderApi.findMany.mock.calls[0][0]).toMatchObject({
|
|
where: { tenantId: 't1' },
|
|
take: 200,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getProfileNameServers (h3t)', () => {
|
|
it('liest das Profil mit genau einem GET und liefert die Nameserver in Schluesselreihenfolge', async () => {
|
|
const h = makeHarness();
|
|
const result = await h.service.getProfileNameServers('t1');
|
|
expect(result).toEqual({ environment: 'DEMO', nameServers: PROFILE_NAMESERVERS });
|
|
expect(h.calls).toHaveLength(1);
|
|
expect(h.calls[0].url).toBe('https://api.demo.autodns.com/v1/user/api-user/4/profile');
|
|
expect(h.calls[0].init.method).toBe('GET');
|
|
});
|
|
|
|
it('kodiert einen Benutzernamen mit Leerzeichen im Pfad', async () => {
|
|
const h = makeHarness();
|
|
h.settings.getActiveCredentials.mockResolvedValueOnce({
|
|
environment: 'DEMO',
|
|
credentials: { ...CREDENTIALS_DEMO, user: 'api user' },
|
|
configVersion: 1,
|
|
});
|
|
await h.service.getProfileNameServers('t1');
|
|
expect(h.calls[0].url).toBe('https://api.demo.autodns.com/v1/user/api%20user/4/profile');
|
|
});
|
|
|
|
it('Profil ohne Nameserver-Schluessel -> 409 noDefaultNameServers', async () => {
|
|
const h = makeHarness();
|
|
h.respondProfile(() =>
|
|
reply(200, envelope([{ profiles: [{ key: 'techc', value: 'Max Muster' }] }])),
|
|
);
|
|
await expect(h.service.getProfileNameServers('t1')).rejects.toMatchObject({
|
|
status: 409,
|
|
response: { code: 'noDefaultNameServers' },
|
|
});
|
|
});
|
|
|
|
it('AutoDNS 401 -> 502 autodnsAuth', async () => {
|
|
const h = makeHarness();
|
|
h.respondProfile(() => reply(401, ''));
|
|
await expect(h.service.getProfileNameServers('t1')).rejects.toMatchObject({
|
|
status: 502,
|
|
response: { code: 'autodnsAuth' },
|
|
});
|
|
});
|
|
|
|
it('Zeitablauf -> 504', async () => {
|
|
const h = makeHarness({ timeoutMs: 20 });
|
|
h.respondProfile(() => new Promise(() => {}));
|
|
await expect(h.service.getProfileNameServers('t1')).rejects.toMatchObject({ status: 504 });
|
|
});
|
|
});
|
|
|
|
describe('Nameserver aus AutoDNS (h3t)', () => {
|
|
const dto = {
|
|
domain: 'beispiel.de',
|
|
ownerContactId: 11,
|
|
adminContactId: 11,
|
|
techContactId: 22,
|
|
zoneContactId: 22,
|
|
};
|
|
|
|
it('Kette: Entwurf -> Bestaetigung -> genau ein POST /domain mit den Profil-Nameservern in Profil-Reihenfolge', async () => {
|
|
const h = makeHarness();
|
|
h.respond((call) => {
|
|
if (call.url.endsWith('/domainstudio')) {
|
|
return studioReply('beispiel.de', 'FREE', [
|
|
{ amount: 4.9, currency: 'EUR', period: { unit: 'YEAR', period: 1 } },
|
|
]);
|
|
}
|
|
return reply(200, envelope([{ id: 77, status: 'RUNNING' }]));
|
|
});
|
|
const summary = await h.service.createOrder('t1', 'u1', dto as any);
|
|
expect(summary.nameServers).toEqual(PROFILE_NAMESERVERS);
|
|
await h.service.submitOrder(
|
|
't1',
|
|
{ id: 'u1', username: 'admin' },
|
|
summary.order.id,
|
|
summary.version,
|
|
);
|
|
const posts = h.calls.filter((c) => c.init.method === 'POST' && c.url.endsWith('/domain'));
|
|
expect(posts).toHaveLength(1);
|
|
expect(JSON.parse(posts[0].init.body as string).nameServers).toEqual([
|
|
{ name: 'zeta.example.net' },
|
|
{ name: 'alpha.example.org' },
|
|
{ name: 'mid.example.com' },
|
|
{ name: 'beta.example.net' },
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
[[]],
|
|
[['ns1.example.com']],
|
|
])('Entwurf mit Nameservern %j -> 400 orderInvalid vor dem Anspruch, ohne Netz', async (nameServers) => {
|
|
const h = makeHarness();
|
|
const row = h.addOrder({ payload: { ...PAYLOAD, nameServers } });
|
|
await expect(
|
|
h.service.submitOrder(
|
|
't1',
|
|
{ id: 'u1', username: 'admin' },
|
|
row.id,
|
|
row.updatedAt.toISOString(),
|
|
),
|
|
).rejects.toMatchObject({ status: 400, response: { code: 'orderInvalid' } });
|
|
expect(h.orderApi.updateMany).not.toHaveBeenCalled();
|
|
expect(h.fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
});
|