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:
@@ -6,6 +6,7 @@ import {
|
|||||||
parseDomains,
|
parseDomains,
|
||||||
parseJob,
|
parseJob,
|
||||||
parseJobList,
|
parseJobList,
|
||||||
|
parseProfileNameServers,
|
||||||
} from './autodns-parse';
|
} from './autodns-parse';
|
||||||
import { TtlCache } from './domains-cache';
|
import { TtlCache } from './domains-cache';
|
||||||
|
|
||||||
@@ -282,3 +283,133 @@ describe('parseJobList', () => {
|
|||||||
expect(list[1]).toMatchObject({ id: '10', object: 'x.de', created: null });
|
expect(list[1]).toMatchObject({ id: '10', object: 'x.de', created: null });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('parseProfileNameServers (h3t, [ASSUMED] Schluesselnamen)', () => {
|
||||||
|
const profile = (...pairs: Array<[string, string]>) => [
|
||||||
|
{ profiles: pairs.map(([key, value]) => ({ key, value })) },
|
||||||
|
];
|
||||||
|
|
||||||
|
it('ordnet nach der Nummer im Schluessel und ignoriert fremde Eintraege', () => {
|
||||||
|
const result = parseProfileNameServers(
|
||||||
|
profile(
|
||||||
|
['ns3', 'c.example.net'],
|
||||||
|
['ns1', 'z.example.org'],
|
||||||
|
['techc', 'Max Muster'],
|
||||||
|
['ns4', 'd.example.net'],
|
||||||
|
['ns2', 'b.example.net'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(result.nameServers).toEqual([
|
||||||
|
'z.example.org',
|
||||||
|
'b.example.net',
|
||||||
|
'c.example.net',
|
||||||
|
'd.example.net',
|
||||||
|
]);
|
||||||
|
expect(result.keys).toEqual(['ns3', 'ns1', 'techc', 'ns4', 'ns2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sortiert numerisch: ns10 nach ns9', () => {
|
||||||
|
const result = parseProfileNameServers(
|
||||||
|
profile(['ns10', 'j.example.net'], ['ns9', 'i.example.net'], ['ns1', 'a.example.net']),
|
||||||
|
);
|
||||||
|
expect(result.nameServers).toEqual(['a.example.net', 'i.example.net', 'j.example.net']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalisiert Werte und verwirft Nicht-Rechnernamen', () => {
|
||||||
|
const result = parseProfileNameServers(
|
||||||
|
profile(
|
||||||
|
['ns1', ' Z.Example.NET. '],
|
||||||
|
['ns2', '192.0.2.1'],
|
||||||
|
['ns3', ''],
|
||||||
|
['ns4', 'kein rechner'],
|
||||||
|
['ns5', 'a.example.org 192.0.2.1'],
|
||||||
|
['ns6', 'b.example.org'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(result.nameServers).toEqual(['z.example.net', 'b.example.org']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['nameserver1', 'nameserver2'],
|
||||||
|
['NS_1', 'NS_2'],
|
||||||
|
['default_ns1', 'default_ns2'],
|
||||||
|
['nserver1', 'nserver2'],
|
||||||
|
])('erkennt die Schluesselform %s', (k1, k2) => {
|
||||||
|
const result = parseProfileNameServers(profile([k2, 'b.example.net'], [k1, 'a.example.net']));
|
||||||
|
expect(result.nameServers).toEqual(['a.example.net', 'b.example.net']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('akzeptiert flache Eintraege und ein einzelnes Objekt mit profiles', () => {
|
||||||
|
expect(
|
||||||
|
parseProfileNameServers([
|
||||||
|
{ key: 'ns2', value: 'b.example.net' },
|
||||||
|
{ key: 'ns1', value: 'a.example.net' },
|
||||||
|
]).nameServers,
|
||||||
|
).toEqual(['a.example.net', 'b.example.net']);
|
||||||
|
expect(
|
||||||
|
parseProfileNameServers({
|
||||||
|
profiles: [
|
||||||
|
{ key: 'ns1', value: 'a.example.net' },
|
||||||
|
{ key: 'ns2', value: 'b.example.net' },
|
||||||
|
],
|
||||||
|
}).nameServers,
|
||||||
|
).toEqual(['a.example.net', 'b.example.net']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gleiche Nummer mit zwei verschiedenen Rechnern: leer, nichts wird geraten', () => {
|
||||||
|
const result = parseProfileNameServers(
|
||||||
|
profile(['ns1', 'a.example.net'], ['default_ns1', 'x.example.net'], ['ns2', 'b.example.net']),
|
||||||
|
);
|
||||||
|
expect(result.nameServers).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gleiche Nummer mit gleichem Rechner zaehlt einmal, derselbe Rechner unter zwei Nummern bleibt einmal', () => {
|
||||||
|
expect(
|
||||||
|
parseProfileNameServers(
|
||||||
|
profile(
|
||||||
|
['ns1', 'a.example.net'],
|
||||||
|
['default_ns1', 'A.example.net'],
|
||||||
|
['ns2', 'b.example.net'],
|
||||||
|
),
|
||||||
|
).nameServers,
|
||||||
|
).toEqual(['a.example.net', 'b.example.net']);
|
||||||
|
expect(
|
||||||
|
parseProfileNameServers(
|
||||||
|
profile(['ns1', 'a.example.net'], ['ns2', 'a.example.net'], ['ns3', 'b.example.net']),
|
||||||
|
).nameServers,
|
||||||
|
).toEqual(['a.example.net', 'b.example.net']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Listenschluessel nur als Ersatz, in der geschriebenen Reihenfolge', () => {
|
||||||
|
expect(
|
||||||
|
parseProfileNameServers(profile(['nameservers', 'b.example.org, a.example.org'])).nameServers,
|
||||||
|
).toEqual(['b.example.org', 'a.example.org']);
|
||||||
|
// Nummerierte Schluessel haben Vorrang vor der Liste.
|
||||||
|
expect(
|
||||||
|
parseProfileNameServers(
|
||||||
|
profile(['nameservers', 'x.example.org, y.example.org'], ['ns1', 'a.example.net']),
|
||||||
|
).nameServers,
|
||||||
|
).toEqual(['a.example.net']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('zwei Listenschluessel mit verschiedenen Listen: leer', () => {
|
||||||
|
expect(
|
||||||
|
parseProfileNameServers(
|
||||||
|
profile(
|
||||||
|
['nameservers', 'a.example.org, b.example.org'],
|
||||||
|
['default_nameservers', 'c.example.org, d.example.org'],
|
||||||
|
),
|
||||||
|
).nameServers,
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[null],
|
||||||
|
['x'],
|
||||||
|
[{}],
|
||||||
|
[[1, 2]],
|
||||||
|
[undefined],
|
||||||
|
])('Muell %j ergibt eine leere Liste', (input) => {
|
||||||
|
expect(parseProfileNameServers(input)).toEqual({ nameServers: [], keys: [] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { domainToASCII } from 'node:url';
|
import { domainToASCII } from 'node:url';
|
||||||
|
import { HOSTNAME_PATTERN } from './domain-name';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reine Auswertung der AutoDNS-Antwortdaten (quick-261008-dts) — kein Nest,
|
* Reine Auswertung der AutoDNS-Antwortdaten (quick-261008-dts) — kein Nest,
|
||||||
@@ -307,3 +308,96 @@ export function parseJobList(data: unknown): ParsedJobListEntry[] {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Standard-Nameserver aus dem AutoDNS-Benutzerprofil (quick-261008-h3t) --------
|
||||||
|
|
||||||
|
export interface ParsedProfileNameServers {
|
||||||
|
/** Rechnernamen in der Reihenfolge der Positionsnummer im Profilschluessel. */
|
||||||
|
nameServers: string[];
|
||||||
|
/** Die gesehenen Profilschluessel (nur Namen, nie Werte) fuer die Protokollzeile. */
|
||||||
|
keys: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_PROFILE_KEYS = 50;
|
||||||
|
const MAX_PROFILE_KEY_LENGTH = 60;
|
||||||
|
const NUMBERED_KEY = /(?:nameserver|name[_-]server|nserver|ns)[_.-]?(\d{1,2})(?!\d)/;
|
||||||
|
const LIST_KEY = /^(?:default[_.-]?)?(?:nameservers?|name[_-]servers?|nservers?|ns)$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erkennt die Standard-Nameserver in der Antwort von
|
||||||
|
* `GET /user/{name}/{context}/profile` (UserProfileViews mit `profiles[]` aus
|
||||||
|
* `key`/`value`).
|
||||||
|
*
|
||||||
|
* [ASSUMED] Die Schluesselnamen der Standard-Nameserver im AutoDNS-Benutzerprofil
|
||||||
|
* sind nicht dokumentiert. Deshalb wird tolerant erkannt: ein Schluessel, der
|
||||||
|
* `ns`, `nserver` oder `nameserver` samt Nummer enthaelt (zum Beispiel
|
||||||
|
* `nameserver1`, `NS_2`, `default_ns3`); die Nummer bestimmt die Reihenfolge.
|
||||||
|
* Nur wenn so nichts erkannt wird, gilt ein Listenschluessel (`nameservers`) mit
|
||||||
|
* durch Komma/Leerzeichen getrennten Rechnernamen. Mehrdeutiges (dieselbe Nummer
|
||||||
|
* mit zwei verschiedenen Rechnern) ergibt eine leere Liste — es wird nie
|
||||||
|
* geraten. Pruefpunkt H-1 der Demo-Checkliste von quick-261008-h3t.
|
||||||
|
* Die Mindestanzahl prueft der Aufrufer.
|
||||||
|
*/
|
||||||
|
export function parseProfileNameServers(data: unknown): ParsedProfileNameServers {
|
||||||
|
const entries: Array<{ key: string; value: string }> = [];
|
||||||
|
const collect = (item: unknown): void => {
|
||||||
|
if (!isRecord(item)) return;
|
||||||
|
if (typeof item.key === 'string' && typeof item.value === 'string') {
|
||||||
|
entries.push({ key: item.key, value: item.value });
|
||||||
|
}
|
||||||
|
if (Array.isArray(item.profiles)) {
|
||||||
|
for (const p of item.profiles) {
|
||||||
|
if (isRecord(p) && typeof p.key === 'string' && typeof p.value === 'string') {
|
||||||
|
entries.push({ key: p.key, value: p.value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (Array.isArray(data)) for (const item of data) collect(item);
|
||||||
|
else collect(data);
|
||||||
|
|
||||||
|
const keys: string[] = [];
|
||||||
|
for (const { key } of entries) {
|
||||||
|
const shown = key.trim().slice(0, MAX_PROFILE_KEY_LENGTH);
|
||||||
|
if (shown !== '' && !keys.includes(shown) && keys.length < MAX_PROFILE_KEYS) keys.push(shown);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostOf = (value: string): string | null => {
|
||||||
|
const host = value.trim().toLowerCase().replace(/\.$/, '');
|
||||||
|
return HOSTNAME_PATTERN.test(host) ? host : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Nummerierte Schluessel.
|
||||||
|
const byPosition = new Map<number, string>();
|
||||||
|
let ambiguous = false;
|
||||||
|
for (const { key, value } of entries) {
|
||||||
|
const match = NUMBERED_KEY.exec(key.trim().toLowerCase());
|
||||||
|
if (!match) continue;
|
||||||
|
const host = hostOf(value);
|
||||||
|
if (host === null) continue;
|
||||||
|
const position = Number.parseInt(match[1] as string, 10);
|
||||||
|
const known = byPosition.get(position);
|
||||||
|
if (known !== undefined && known !== host) ambiguous = true;
|
||||||
|
else byPosition.set(position, host);
|
||||||
|
}
|
||||||
|
if (ambiguous) return { nameServers: [], keys };
|
||||||
|
if (byPosition.size > 0) {
|
||||||
|
const ordered = [...byPosition.entries()].sort((a, b) => a[0] - b[0]).map(([, host]) => host);
|
||||||
|
return { nameServers: [...new Set(ordered)], keys };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ersatz: ein Listenschluessel.
|
||||||
|
const lists: string[][] = [];
|
||||||
|
for (const { key, value } of entries) {
|
||||||
|
if (!LIST_KEY.test(key.trim().toLowerCase())) continue;
|
||||||
|
const tokens = value.split(/[\s,;]+/).filter((t) => t !== '');
|
||||||
|
if (tokens.length === 0) continue;
|
||||||
|
const hosts = tokens.map(hostOf);
|
||||||
|
if (hosts.some((h) => h === null)) continue;
|
||||||
|
lists.push([...new Set(hosts as string[])]);
|
||||||
|
}
|
||||||
|
const first = lists[0];
|
||||||
|
if (first === undefined) return { nameServers: [], keys };
|
||||||
|
if (lists.some((l) => l.join(',') !== first.join(','))) return { nameServers: [], keys };
|
||||||
|
return { nameServers: first, keys };
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ const LABEL = '[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?';
|
|||||||
const DOMAIN_PATTERN = new RegExp(`^(?:${LABEL}\\.)+${LABEL}$`);
|
const DOMAIN_PATTERN = new RegExp(`^(?:${LABEL}\\.)+${LABEL}$`);
|
||||||
const MAX_TOTAL_LENGTH = 253;
|
const MAX_TOTAL_LENGTH = 253;
|
||||||
|
|
||||||
|
/** Rechnername: Labels aus Buchstaben, Ziffern, Bindestrich; mindestens zwei Labels. */
|
||||||
|
export const HOSTNAME_PATTERN =
|
||||||
|
/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])$/;
|
||||||
|
|
||||||
export interface NormalizedDomain {
|
export interface NormalizedDomain {
|
||||||
/** ASCII-/Punycode-Form — das geht an AutoDNS. */
|
/** ASCII-/Punycode-Form — das geht an AutoDNS. */
|
||||||
ascii: string;
|
ascii: string;
|
||||||
|
|||||||
@@ -89,6 +89,31 @@ const PAYLOAD = {
|
|||||||
availabilityCheckedAt: '2026-10-08T10:00:00.000Z',
|
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 } = {}) {
|
function makeHarness(opts: { environment?: 'DEMO' | 'LIVE'; timeoutMs?: number } = {}) {
|
||||||
const state = {
|
const state = {
|
||||||
orders: [] as OrderRow[],
|
orders: [] as OrderRow[],
|
||||||
@@ -102,9 +127,15 @@ function makeHarness(opts: { environment?: 'DEMO' | 'LIVE'; timeoutMs?: number }
|
|||||||
throw new Error('kein Netz erwartet');
|
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 fetchImpl = vi.fn(async (url: string, init: any) => {
|
||||||
const call: Call = { url, init };
|
const call: Call = { url, init };
|
||||||
calls.push(call);
|
calls.push(call);
|
||||||
|
if (init.method === 'GET' && url.endsWith('/profile')) {
|
||||||
|
return (await profileResponder(call)) as never;
|
||||||
|
}
|
||||||
return (await responder(call)) as never;
|
return (await responder(call)) as never;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -223,6 +254,9 @@ function makeHarness(opts: { environment?: 'DEMO' | 'LIVE'; timeoutMs?: number }
|
|||||||
respond: (r: Responder) => {
|
respond: (r: Responder) => {
|
||||||
responder = r;
|
responder = r;
|
||||||
},
|
},
|
||||||
|
respondProfile: (r: Responder) => {
|
||||||
|
profileResponder = r;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +371,6 @@ describe('createOrder', () => {
|
|||||||
adminContactId: 11,
|
adminContactId: 11,
|
||||||
techContactId: 22,
|
techContactId: 22,
|
||||||
zoneContactId: 22,
|
zoneContactId: 22,
|
||||||
nameServers: ['NS1.example.com ', 'ns2.example.com'],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function free() {
|
function free() {
|
||||||
@@ -364,14 +397,14 @@ describe('createOrder', () => {
|
|||||||
adminContactId: 11,
|
adminContactId: 11,
|
||||||
techContactId: 22,
|
techContactId: 22,
|
||||||
zoneContactId: 22,
|
zoneContactId: 22,
|
||||||
nameServers: ['ns1.example.com', 'ns2.example.com'],
|
nameServers: PROFILE_NAMESERVERS,
|
||||||
periodYears: 1,
|
periodYears: 1,
|
||||||
price: { amount: 4.9, currency: 'EUR' },
|
price: { amount: 4.9, currency: 'EUR' },
|
||||||
});
|
});
|
||||||
expect(typeof h.state.orders[0].payload.availabilityCheckedAt).toBe('string');
|
expect(typeof h.state.orders[0].payload.availabilityCheckedAt).toBe('string');
|
||||||
expect(result.contacts.owner).toEqual({ id: 11, displayName: 'Muster GmbH' });
|
expect(result.contacts.owner).toEqual({ id: 11, displayName: 'Muster GmbH' });
|
||||||
expect(result.contacts.zone).toEqual({ id: 22, displayName: 'Erika Muster' });
|
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.order).toMatchObject({ domainName: 'beispiel.de', status: 'DRAFT' });
|
||||||
expect(result.version).toBe(h.state.orders[0].updatedAt.toISOString());
|
expect(result.version).toBe(h.state.orders[0].updatedAt.toISOString());
|
||||||
// Der Entwurf selbst bestellt nichts.
|
// Der Entwurf selbst bestellt nichts.
|
||||||
@@ -388,28 +421,60 @@ describe('createOrder', () => {
|
|||||||
expect(h.state.orders).toHaveLength(0);
|
expect(h.state.orders).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it('ein vom Browser gesendeter Nameserver-Satz wird ignoriert (h3t, D-03)', async () => {
|
||||||
[['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) => {
|
|
||||||
const h = makeHarness();
|
const h = makeHarness();
|
||||||
h.respond(free);
|
h.respond(free);
|
||||||
await expect(
|
const result = await h.service.createOrder('t1', 'u1', {
|
||||||
h.service.createOrder('t1', 'u1', { ...dto, nameServers } as any),
|
...dto,
|
||||||
).rejects.toMatchObject({ response: { code } });
|
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.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 () => {
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
BadGatewayException,
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
ConflictException,
|
ConflictException,
|
||||||
Injectable,
|
Injectable,
|
||||||
@@ -20,11 +21,12 @@ import {
|
|||||||
parseDomainStudio,
|
parseDomainStudio,
|
||||||
parseJob,
|
parseJob,
|
||||||
parseJobList,
|
parseJobList,
|
||||||
|
parseProfileNameServers,
|
||||||
} from './autodns-parse';
|
} from './autodns-parse';
|
||||||
import { normalizeDomainName, splitDomain } from './domain-name';
|
import { normalizeDomainName, splitDomain } from './domain-name';
|
||||||
import { autodnsFailureToHttp, type DomainsEnvironment } from './domains.types';
|
import { autodnsFailureToHttp, type DomainsEnvironment } from './domains.types';
|
||||||
import { DomainsDirectoryService } from './domains-directory.service';
|
import { DomainsDirectoryService } from './domains-directory.service';
|
||||||
import { DomainsSettingsService, HOSTNAME_PATTERN } from './domains-settings.service';
|
import { DomainsSettingsService } from './domains-settings.service';
|
||||||
import type { CreateDomainsOrderDto } from './dto/domains-order.dto';
|
import type { CreateDomainsOrderDto } from './dto/domains-order.dto';
|
||||||
|
|
||||||
export type DomainOrderStatus =
|
export type DomainOrderStatus =
|
||||||
@@ -45,8 +47,8 @@ const JOB_SEARCH_TOLERANCE_MS = 5 * 60_000;
|
|||||||
const LIST_MAX_ORDERS = 200;
|
const LIST_MAX_ORDERS = 200;
|
||||||
/** Trefferzahl der Auftragssuche beim Abgleich; eine volle Liste kann abgeschnitten sein. */
|
/** Trefferzahl der Auftragssuche beim Abgleich; eine volle Liste kann abgeschnitten sein. */
|
||||||
const JOB_SEARCH_LIMIT = 10;
|
const JOB_SEARCH_LIMIT = 10;
|
||||||
|
/** .de verlangt mindestens zwei Nameserver; weniger gilt als "nicht hinterlegt". */
|
||||||
const MIN_NAMESERVERS = 2;
|
const MIN_NAMESERVERS = 2;
|
||||||
const MAX_NAMESERVERS = 6;
|
|
||||||
|
|
||||||
type ReconcileOutcome = 'found' | 'not-found' | 'inconclusive';
|
type ReconcileOutcome = 'found' | 'not-found' | 'inconclusive';
|
||||||
|
|
||||||
@@ -165,6 +167,11 @@ const ERR = {
|
|||||||
code: 'notCancelable',
|
code: 'notCancelable',
|
||||||
message: 'Dieser Auftrag kann nicht mehr verworfen werden.',
|
message: 'Dieser Auftrag kann nicht mehr verworfen werden.',
|
||||||
},
|
},
|
||||||
|
noDefaultNameServers: {
|
||||||
|
code: 'noDefaultNameServers',
|
||||||
|
message:
|
||||||
|
'In AutoDNS sind keine Standard-Nameserver hinterlegt. Bitte hinterlegen Sie mindestens zwei Nameserver als Standard in AutoDNS; bis dahin ist keine Registrierung möglich.',
|
||||||
|
},
|
||||||
orderNotFound: { code: 'orderNotFound', message: 'Der Auftrag wurde nicht gefunden.' },
|
orderNotFound: { code: 'orderNotFound', message: 'Der Auftrag wurde nicht gefunden.' },
|
||||||
invalidDomain: {
|
invalidDomain: {
|
||||||
code: 'invalidDomain',
|
code: 'invalidDomain',
|
||||||
@@ -323,31 +330,42 @@ export class DomainsOrdersService {
|
|||||||
|
|
||||||
// --- Entwurf -----------------------------------------------------------------
|
// --- Entwurf -----------------------------------------------------------------
|
||||||
|
|
||||||
private normalizeNameServers(raw: string[]): string[] {
|
/**
|
||||||
const list = [...new Set((raw ?? []).map((n) => String(n).trim().toLowerCase()))].filter(
|
* Liest die Standard-Nameserver aus dem AutoDNS-Benutzerprofil des
|
||||||
(n) => n.length > 0,
|
* eingetragenen Benutzers (genau EIN GET, keine Wiederholung). Die Liste ist
|
||||||
);
|
* die einzige Quelle der Nameserver einer Registrierung: nichts wird geraten,
|
||||||
if (list.length < MIN_NAMESERVERS) {
|
* ergaenzt oder gekuerzt. Weniger als zwei erkannte Eintraege gelten als
|
||||||
throw new BadRequestException({
|
* "nicht hinterlegt" (409), ein Lesefehler wird als AutoDNS-Fehler gemeldet.
|
||||||
code: 'tooFewNameServers',
|
*/
|
||||||
message: 'Bitte geben Sie mindestens zwei Nameserver an.',
|
private async readProfileNameServers(credentials: AutodnsCredentials): Promise<string[]> {
|
||||||
|
const path = `/user/${encodeURIComponent(credentials.user)}/${credentials.context}/profile`;
|
||||||
|
let result: AutodnsResult;
|
||||||
|
try {
|
||||||
|
result = await this.callRaw(credentials, 'GET', path);
|
||||||
|
} catch {
|
||||||
|
throw new BadGatewayException({
|
||||||
|
code: 'autodnsError',
|
||||||
|
message: 'Tessera konnte die Standard-Nameserver nicht aus AutoDNS lesen.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (list.length > MAX_NAMESERVERS) {
|
if (!result.ok) throw autodnsFailureToHttp(result);
|
||||||
throw new BadRequestException({
|
const parsed = parseProfileNameServers(result.data);
|
||||||
code: 'tooManyNameServers',
|
if (parsed.nameServers.length < MIN_NAMESERVERS) {
|
||||||
message: 'Höchstens sechs Nameserver sind möglich.',
|
// Nur Schluesselnamen, nie Werte oder Zugangsdaten.
|
||||||
});
|
this.logger.warn(
|
||||||
|
`Keine Standard-Nameserver im AutoDNS-Profil erkannt (${credentials.environment}); Schluessel im Profil: ${parsed.keys.join(', ') || '(keine)'}`,
|
||||||
|
);
|
||||||
|
throw new ConflictException(ERR.noDefaultNameServers);
|
||||||
}
|
}
|
||||||
for (const name of list) {
|
return parsed.nameServers;
|
||||||
if (!HOSTNAME_PATTERN.test(name)) {
|
}
|
||||||
throw new BadRequestException({
|
|
||||||
code: 'invalidNameServer',
|
/** Standard-Nameserver des aktiven Systems zur Anzeige in "Registrieren". */
|
||||||
message: `Der Nameserver „${name}“ ist kein gültiger Rechnername.`,
|
async getProfileNameServers(
|
||||||
});
|
tenantId: string,
|
||||||
}
|
): Promise<{ environment: DomainsEnvironment; nameServers: string[] }> {
|
||||||
}
|
const { environment, credentials } = await this.settings.getActiveCredentials(tenantId);
|
||||||
return list;
|
return { environment, nameServers: await this.readProfileNameServers(credentials) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(
|
async createOrder(
|
||||||
@@ -357,7 +375,6 @@ export class DomainsOrdersService {
|
|||||||
): Promise<OrderSummaryView> {
|
): Promise<OrderSummaryView> {
|
||||||
const normalized = normalizeDomainName(dto.domain);
|
const normalized = normalizeDomainName(dto.domain);
|
||||||
if (!normalized) throw new BadRequestException(ERR.invalidDomain);
|
if (!normalized) throw new BadRequestException(ERR.invalidDomain);
|
||||||
const nameServers = this.normalizeNameServers(dto.nameServers);
|
|
||||||
const contactIds = [
|
const contactIds = [
|
||||||
dto.ownerContactId,
|
dto.ownerContactId,
|
||||||
dto.adminContactId,
|
dto.adminContactId,
|
||||||
@@ -381,6 +398,10 @@ export class DomainsOrdersService {
|
|||||||
})) as OrderRow | null;
|
})) as OrderRow | null;
|
||||||
if (existing && existing.status !== 'DRAFT') throw new ConflictException(ERR.orderOpen);
|
if (existing && existing.status !== 'DRAFT') throw new ConflictException(ERR.orderOpen);
|
||||||
|
|
||||||
|
// Die Nameserver liest der Server selbst aus dem AutoDNS-Profil; was der
|
||||||
|
// Browser anzeigt, ist nur zur Kontrolle (D-03). Fehlen sie, entsteht kein Entwurf.
|
||||||
|
const nameServers = await this.readProfileNameServers(credentials);
|
||||||
|
|
||||||
// Die Verfuegbarkeit wird serverseitig neu geprueft — dem Browser traut hier niemand.
|
// Die Verfuegbarkeit wird serverseitig neu geprueft — dem Browser traut hier niemand.
|
||||||
const availability = await this.availabilityFor(credentials, domain, normalized.unicode);
|
const availability = await this.availabilityFor(credentials, domain, normalized.unicode);
|
||||||
if (!availability.available) throw new ConflictException(ERR.notAvailable);
|
if (!availability.available) throw new ConflictException(ERR.notAvailable);
|
||||||
@@ -503,7 +524,7 @@ export class DomainsOrdersService {
|
|||||||
if (before.status !== 'DRAFT') throw new ConflictException(ERR.alreadySubmitted);
|
if (before.status !== 'DRAFT') throw new ConflictException(ERR.alreadySubmitted);
|
||||||
if (!this.sameVersion(before.updatedAt, version)) throw new ConflictException(ERR.orderChanged);
|
if (!this.sameVersion(before.updatedAt, version)) throw new ConflictException(ERR.orderChanged);
|
||||||
const payload = readPayload(before.payload);
|
const payload = readPayload(before.payload);
|
||||||
if (!payload) {
|
if (!payload || payload.nameServers.length < MIN_NAMESERVERS) {
|
||||||
throw new BadRequestException({
|
throw new BadRequestException({
|
||||||
code: 'orderInvalid',
|
code: 'orderInvalid',
|
||||||
message: 'Der Auftrag ist unvollständig. Bitte legen Sie ihn neu an.',
|
message: 'Der Auftrag ist unvollständig. Bitte legen Sie ihn neu an.',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
type AutodnsRateLimiter,
|
type AutodnsRateLimiter,
|
||||||
autodnsRequest,
|
autodnsRequest,
|
||||||
} from './autodns-client';
|
} from './autodns-client';
|
||||||
|
import { HOSTNAME_PATTERN } from './domain-name';
|
||||||
import {
|
import {
|
||||||
AUTODNS_AUTH_MESSAGE,
|
AUTODNS_AUTH_MESSAGE,
|
||||||
type DomainsConnectionTestResult,
|
type DomainsConnectionTestResult,
|
||||||
@@ -40,10 +41,6 @@ const DECRYPT_FAILED = {
|
|||||||
const MIN_NAMESERVERS = 2;
|
const MIN_NAMESERVERS = 2;
|
||||||
const MAX_NAMESERVERS = 6;
|
const MAX_NAMESERVERS = 6;
|
||||||
|
|
||||||
/** Rechnername: Labels aus Buchstaben, Ziffern, Bindestrich; mindestens zwei Labels. */
|
|
||||||
export const HOSTNAME_PATTERN =
|
|
||||||
/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])$/;
|
|
||||||
|
|
||||||
interface ConfigRow {
|
interface ConfigRow {
|
||||||
environment: DomainsEnvironment;
|
environment: DomainsEnvironment;
|
||||||
demoUser: string | null;
|
demoUser: string | null;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const MANAGE_HANDLERS = [
|
|||||||
'createContact',
|
'createContact',
|
||||||
'assignContacts',
|
'assignContacts',
|
||||||
'checkAvailability',
|
'checkAvailability',
|
||||||
|
'getNameServers',
|
||||||
'createOrder',
|
'createOrder',
|
||||||
'submitOrder',
|
'submitOrder',
|
||||||
'cancelOrder',
|
'cancelOrder',
|
||||||
@@ -50,6 +51,7 @@ function makeDirectory() {
|
|||||||
function makeOrders() {
|
function makeOrders() {
|
||||||
return {
|
return {
|
||||||
checkAvailability: vi.fn(async (..._a: unknown[]) => ({})),
|
checkAvailability: vi.fn(async (..._a: unknown[]) => ({})),
|
||||||
|
getProfileNameServers: vi.fn(async (..._a: unknown[]) => ({})),
|
||||||
listOrders: vi.fn(async (..._a: unknown[]) => []),
|
listOrders: vi.fn(async (..._a: unknown[]) => []),
|
||||||
createOrder: vi.fn(async (..._a: unknown[]) => ({})),
|
createOrder: vi.fn(async (..._a: unknown[]) => ({})),
|
||||||
refreshOpenOrders: vi.fn(async (..._a: unknown[]) => []),
|
refreshOpenOrders: vi.fn(async (..._a: unknown[]) => []),
|
||||||
@@ -106,6 +108,7 @@ describe('DomainsController — Metadaten', () => {
|
|||||||
expect(route('updateCustomer')).toEqual([2, 'customers/:id']);
|
expect(route('updateCustomer')).toEqual([2, 'customers/:id']);
|
||||||
expect(route('deleteCustomer')).toEqual([3, 'customers/:id']);
|
expect(route('deleteCustomer')).toEqual([3, 'customers/:id']);
|
||||||
expect(route('checkAvailability')).toEqual([1, 'availability']);
|
expect(route('checkAvailability')).toEqual([1, 'availability']);
|
||||||
|
expect(route('getNameServers')).toEqual([0, 'name-servers']);
|
||||||
expect(route('listOrders')).toEqual([0, 'orders']);
|
expect(route('listOrders')).toEqual([0, 'orders']);
|
||||||
expect(route('createOrder')).toEqual([1, 'orders']);
|
expect(route('createOrder')).toEqual([1, 'orders']);
|
||||||
expect(route('refreshOrders')).toEqual([1, 'orders/refresh']);
|
expect(route('refreshOrders')).toEqual([1, 'orders/refresh']);
|
||||||
@@ -223,12 +226,14 @@ describe('DomainsController — Auftraege: Verhalten', () => {
|
|||||||
const orders = makeOrders();
|
const orders = makeOrders();
|
||||||
const c = new DomainsController(makeSettings() as any, makeDirectory() as any, orders as any);
|
const c = new DomainsController(makeSettings() as any, makeDirectory() as any, orders as any);
|
||||||
await c.checkAvailability(req('t1'), { domain: 'beispiel.de' });
|
await c.checkAvailability(req('t1'), { domain: 'beispiel.de' });
|
||||||
|
await c.getNameServers(req('t1'));
|
||||||
await c.listOrders(req('t1'));
|
await c.listOrders(req('t1'));
|
||||||
await c.createOrder(req('t1'), user, { domain: 'beispiel.de' } as any);
|
await c.createOrder(req('t1'), user, { domain: 'beispiel.de' } as any);
|
||||||
await c.refreshOrders(req('t1'));
|
await c.refreshOrders(req('t1'));
|
||||||
await c.submitOrder(req('t1'), user, id, { version: '2026-10-08T10:00:00.000Z' });
|
await c.submitOrder(req('t1'), user, id, { version: '2026-10-08T10:00:00.000Z' });
|
||||||
await c.cancelOrder(req('t1'), id);
|
await c.cancelOrder(req('t1'), id);
|
||||||
expect(orders.checkAvailability).toHaveBeenCalledWith('t1', 'beispiel.de');
|
expect(orders.checkAvailability).toHaveBeenCalledWith('t1', 'beispiel.de');
|
||||||
|
expect(orders.getProfileNameServers).toHaveBeenCalledWith('t1');
|
||||||
expect(orders.listOrders).toHaveBeenCalledWith('t1');
|
expect(orders.listOrders).toHaveBeenCalledWith('t1');
|
||||||
expect(orders.createOrder).toHaveBeenCalledWith('t1', 'u1', { domain: 'beispiel.de' });
|
expect(orders.createOrder).toHaveBeenCalledWith('t1', 'u1', { domain: 'beispiel.de' });
|
||||||
expect(orders.refreshOpenOrders).toHaveBeenCalledWith('t1');
|
expect(orders.refreshOpenOrders).toHaveBeenCalledWith('t1');
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function mayRefresh(req: AuthenticatedRequest, value: string | undefined): boole
|
|||||||
* Verwalten (`@ModuleManage('domains')`, Administratoren und Benutzer mit
|
* Verwalten (`@ModuleManage('domains')`, Administratoren und Benutzer mit
|
||||||
* der Freigabestufe Verwalten): GET/PUT settings, POST
|
* der Freigabestufe Verwalten): GET/PUT settings, POST
|
||||||
* connection-test, Kunden und Kontakte anlegen/aendern/zuordnen,
|
* connection-test, Kunden und Kontakte anlegen/aendern/zuordnen,
|
||||||
* Verfuegbarkeit pruefen, Bestellungen anlegen, abschicken,
|
* Verfuegbarkeit pruefen, Standard-Nameserver aus AutoDNS lesen, Bestellungen anlegen, abschicken,
|
||||||
* abbrechen.
|
* abbrechen.
|
||||||
* Auf Verwalten-Handlern steht NIE ein Rollen-Decorator — der globale
|
* Auf Verwalten-Handlern steht NIE ein Rollen-Decorator — der globale
|
||||||
* RolesGuard wuerde Verwalter sonst aussperren.
|
* RolesGuard wuerde Verwalter sonst aussperren.
|
||||||
@@ -154,6 +154,12 @@ export class DomainsController {
|
|||||||
return this.orders.checkAvailability(this.requireTenantId(req), dto.domain);
|
return this.orders.checkAvailability(this.requireTenantId(req), dto.domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('name-servers')
|
||||||
|
@ModuleManage('domains')
|
||||||
|
async getNameServers(@Req() req: AuthenticatedRequest) {
|
||||||
|
return this.orders.getProfileNameServers(this.requireTenantId(req));
|
||||||
|
}
|
||||||
|
|
||||||
@Get('orders')
|
@Get('orders')
|
||||||
async listOrders(@Req() req: AuthenticatedRequest) {
|
async listOrders(@Req() req: AuthenticatedRequest) {
|
||||||
return this.orders.listOrders(this.requireTenantId(req));
|
return this.orders.listOrders(this.requireTenantId(req));
|
||||||
|
|||||||
@@ -1,14 +1,4 @@
|
|||||||
import {
|
import { IsInt, IsISO8601, IsNotEmpty, IsString, MaxLength, Min } from 'class-validator';
|
||||||
ArrayMaxSize,
|
|
||||||
ArrayMinSize,
|
|
||||||
IsArray,
|
|
||||||
IsInt,
|
|
||||||
IsISO8601,
|
|
||||||
IsNotEmpty,
|
|
||||||
IsString,
|
|
||||||
MaxLength,
|
|
||||||
Min,
|
|
||||||
} from 'class-validator';
|
|
||||||
|
|
||||||
/** Verfuegbarkeitspruefung einer Domain (quick-261008-dts, D-K). */
|
/** Verfuegbarkeitspruefung einer Domain (quick-261008-dts, D-K). */
|
||||||
export class CheckAvailabilityDto {
|
export class CheckAvailabilityDto {
|
||||||
@@ -20,7 +10,9 @@ export class CheckAvailabilityDto {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Entwurf einer Registrierung (D-O): alle vier Kontakte sind Pflicht, die
|
* Entwurf einer Registrierung (D-O): alle vier Kontakte sind Pflicht, die
|
||||||
* Laufzeit steht fest bei einem Jahr und ist deshalb kein Feld.
|
* Laufzeit steht fest bei einem Jahr und ist deshalb kein Feld. Die Nameserver
|
||||||
|
* kommen aus AutoDNS (Benutzerprofil), nie vom Browser; ein mitgeschicktes Feld
|
||||||
|
* `nameServers` entfernt die Whitelist der ValidationPipe.
|
||||||
*/
|
*/
|
||||||
export class CreateDomainsOrderDto {
|
export class CreateDomainsOrderDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -43,13 +35,6 @@ export class CreateDomainsOrderDto {
|
|||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
zoneContactId!: number;
|
zoneContactId!: number;
|
||||||
|
|
||||||
@IsArray()
|
|
||||||
@ArrayMinSize(2)
|
|
||||||
@ArrayMaxSize(6)
|
|
||||||
@IsString({ each: true })
|
|
||||||
@MaxLength(253, { each: true })
|
|
||||||
nameServers!: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ describe('Umgestellte Handler (Verwalten)', () => {
|
|||||||
'createContact',
|
'createContact',
|
||||||
'assignContacts',
|
'assignContacts',
|
||||||
'checkAvailability',
|
'checkAvailability',
|
||||||
|
'getNameServers',
|
||||||
'createOrder',
|
'createOrder',
|
||||||
'submitOrder',
|
'submitOrder',
|
||||||
'cancelOrder',
|
'cancelOrder',
|
||||||
|
|||||||
Reference in New Issue
Block a user