feat(domains): Standard-Nameserver aus dem AutoDNS-Profil lesen (h3t)
- parseProfileNameServers erkennt die Nameserver tolerant ([ASSUMED] Schluesselnamen) - createOrder liest das Profil serverseitig, Browser-Liste wird ignoriert - GET modules/domains/name-servers (Verwalten), Submit-Sperre bei weniger als zwei Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,31 @@ const PAYLOAD = {
|
||||
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[],
|
||||
@@ -102,9 +127,15 @@ function makeHarness(opts: { environment?: 'DEMO' | 'LIVE'; timeoutMs?: number }
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -223,6 +254,9 @@ function makeHarness(opts: { environment?: 'DEMO' | 'LIVE'; timeoutMs?: number }
|
||||
respond: (r: Responder) => {
|
||||
responder = r;
|
||||
},
|
||||
respondProfile: (r: Responder) => {
|
||||
profileResponder = r;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -337,7 +371,6 @@ describe('createOrder', () => {
|
||||
adminContactId: 11,
|
||||
techContactId: 22,
|
||||
zoneContactId: 22,
|
||||
nameServers: ['NS1.example.com ', 'ns2.example.com'],
|
||||
};
|
||||
|
||||
function free() {
|
||||
@@ -364,14 +397,14 @@ describe('createOrder', () => {
|
||||
adminContactId: 11,
|
||||
techContactId: 22,
|
||||
zoneContactId: 22,
|
||||
nameServers: ['ns1.example.com', 'ns2.example.com'],
|
||||
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(['ns1.example.com', 'ns2.example.com']);
|
||||
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.
|
||||
@@ -388,28 +421,60 @@ describe('createOrder', () => {
|
||||
expect(h.state.orders).toHaveLength(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[['ns1.example.com'], 'tooFewNameServers'],
|
||||
[
|
||||
[
|
||||
'a.example.com',
|
||||
'b.example.com',
|
||||
'c.example.com',
|
||||
'd.example.com',
|
||||
'e.example.com',
|
||||
'f.example.com',
|
||||
'g.example.com',
|
||||
],
|
||||
'tooManyNameServers',
|
||||
],
|
||||
[['ns1.example.com', 'kein rechner'], 'invalidNameServer'],
|
||||
])('Nameserver %j -> BadRequest %s', async (nameServers, code) => {
|
||||
it('ein vom Browser gesendeter Nameserver-Satz wird ignoriert (h3t, D-03)', async () => {
|
||||
const h = makeHarness();
|
||||
h.respond(free);
|
||||
await expect(
|
||||
h.service.createOrder('t1', 'u1', { ...dto, nameServers } as any),
|
||||
).rejects.toMatchObject({ response: { code } });
|
||||
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 () => {
|
||||
@@ -1348,3 +1413,107 @@ describe('listOrders', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user