187 lines
22 KiB
TypeScript
187 lines
22 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { ConnectorError, type ConnectorContext, type ProvisionContext } from '@kc/connector-sdk';
|
||
import { runContract } from '@kc/connector-sdk/dist/contract.js';
|
||
import { createKeyHelpConnector, usernameFor } from '../src/index.js';
|
||
import { createFake, type Fake } from './fake.js';
|
||
|
||
const ctx = (extra: Record<string, unknown> = {}): ConnectorContext => ({ config: { baseUrl: 'https://kh.test', ...extra }, secrets: { apiKey: 'test-key' }, correlationId: 'c' });
|
||
const mk = (fake: Fake) => createKeyHelpConnector({ fetchImpl: fake.fetch, sleep: async () => {}, now: () => new Date('2026-09-26T12:00:00Z') });
|
||
const PC: ProvisionContext = { source: 'kundencenter', customerNumber: 'K-10001', customerName: 'Muster GmbH', contactName: 'Max Muster', customerEmail: 'max@example.test', orderNumber: 'B-20001', contractNumber: 'V-30001' };
|
||
|
||
describe('KeyHelp-Connector: Lesen', () => {
|
||
it('erfüllt den Connector-Vertrag', async () => { const f = createFake(); await runContract(expect as never, mk(f), ctx()); });
|
||
it('normalisiert Konten mit Nutzung, Limits, Tarif und Zustand – ohne Geheimnisse', async () => {
|
||
const list = await mk(createFake()).listResources(ctx());
|
||
expect(list.map((r) => [r.externalRef, r.state, r.type])).toEqual([['1', 'active', 'hosting_account'], ['2', 'suspended', 'hosting_account']]);
|
||
const a = list[0]!;
|
||
expect(a.name).toBe('Alpha GmbH · alpha'); expect(a.details).toMatchObject({ username: 'alpha', plan: 'Starter', planId: 1, providerStatus: 'ok' });
|
||
expect(a.usage).toMatchObject({ disk_space: 123456789, domains: 2 }); expect(a.limits).toMatchObject({ disk_space: 10737418240, domains: 2 });
|
||
expect(list[1]!.limits!.disk_space).toBeNull(); expect(list[1]!.details.scheduledDeleteOn).toBe('2026-12-01 00:00:00'); // unbegrenzt / geplantes Löschen
|
||
expect(JSON.stringify(list)).not.toMatch(/GEHEIM|password|AUTO-GENERATED/i);
|
||
});
|
||
it('Statistik-Ausfall einzelner Konten bricht den Abgleich nicht ab', async () => {
|
||
const f = createFake(); const orig = f.fetch; (f as any).fetch = async (u: string, i: RequestInit) => (String(u).includes('/clients/2/stats') ? new Response('{}', { status: 500 }) : orig(u, i));
|
||
const list = await createKeyHelpConnector({ fetchImpl: f.fetch, sleep: async () => {}, retries: 0 }).listResources(ctx());
|
||
expect(list).toHaveLength(2); expect(list[1]!.usage).toEqual({});
|
||
});
|
||
it('liest Hosting-Tarife als Produktvorlagen (fest vorgegeben)', async () => {
|
||
const items = await mk(createFake()).listCatalog!(ctx());
|
||
expect(items.map((i) => i.name)).toEqual(['Starter', 'Business']);
|
||
expect(items[0]).toMatchObject({ externalRef: 'plan:1', category: 'hosting', fixed: true, provisioning: { hostingPlanId: 1, language: 'de', createSystemDomain: true, sendLoginCredentials: true } });
|
||
expect(items[0]!.features).toEqual(expect.arrayContaining(['Speicher: 10 GB', 'Domains: 2', 'FTP', 'Dateimanager'])); expect(items[1]!.features).toContain('Speicher: unbegrenzt');
|
||
});
|
||
it('Health: Version, Ausfall und falscher Schlüssel', async () => {
|
||
const f = createFake(); const h = await mk(f).healthCheck(ctx()); expect(h).toMatchObject({ ok: true, version: 'KeyHelp 26.0 (API 2.15)' });
|
||
const down = createKeyHelpConnector({ fetchImpl: (async () => { throw Object.assign(new TypeError('fetch failed'), { cause: { code: 'ECONNREFUSED' } }); }) as never, sleep: async () => {}, retries: 0 });
|
||
expect(await down.healthCheck(ctx())).toMatchObject({ ok: false, message: expect.stringMatching(/nicht erreichbar/) });
|
||
await expect(mk(f).listResources({ ...ctx(), secrets: { apiKey: 'falsch' } })).rejects.toMatchObject({ code: 'AUTH_FAILED' });
|
||
});
|
||
it('wiederholt Lesezugriffe beim Webserver-Neustart (503), nie das Anlegen', async () => {
|
||
const f = createFake(); f.opts.failGet503 = 2; expect((await mk(f).listResources(ctx())).length).toBe(2);
|
||
const g = createFake(); const c = mk(g); g.opts.failGet503 = 0;
|
||
const orig = g.fetch; let posts = 0; (g as any).fetch = async (u: string, i: RequestInit) => { if (i.method === 'POST') { posts++; return new Response('{}', { status: 503 }); } return orig(u, i); };
|
||
const c2 = createKeyHelpConnector({ fetchImpl: g.fetch, sleep: async () => {} }); void c;
|
||
await expect(c2.provision!(ctx(), { params: { hostingPlanId: 1 }, label: 'x', idempotencyKey: 'k', context: PC })).rejects.toMatchObject({ code: 'UPSTREAM_ERROR', ambiguous: true });
|
||
expect(posts).toBe(1); // Anlage wird nie automatisch wiederholt
|
||
});
|
||
it('prüft Konfiguration: nur https, Fingerabdruck-Format', async () => {
|
||
await expect(createKeyHelpConnector().healthCheck({ config: { baseUrl: 'http://kh.test' }, secrets: { apiKey: 'k' }, correlationId: 'c' }).then((h) => h.ok)).resolves.toBe(false);
|
||
await expect(createKeyHelpConnector().listResources({ config: { baseUrl: 'https://kh.test', tlsFingerprint: 'zzz' }, secrets: { apiKey: 'k' }, correlationId: 'c' })).rejects.toMatchObject({ code: 'BAD_CONFIG' });
|
||
await expect(createKeyHelpConnector({ fetchImpl: createFake().fetch }).listResources({ config: { baseUrl: 'https://kh.test' }, secrets: {}, correlationId: 'c' })).rejects.toMatchObject({ code: 'BAD_CONFIG' });
|
||
});
|
||
});
|
||
|
||
describe('KeyHelp-Connector: Konto anlegen und ändern', () => {
|
||
const params = { hostingPlanId: 2, language: 'de', createSystemDomain: true, sendLoginCredentials: true };
|
||
it('legt ein Konto an, vermerkt Kundencenter, liest kein Passwort und ist wiederholbar', async () => {
|
||
const f = createFake(); const c = mk(f);
|
||
expect(usernameFor(PC, 'x')).toBe('kc30001');
|
||
const r = await c.provision!(ctx(), { params, label: 'x', idempotencyKey: 'k1', context: PC });
|
||
expect(r.resource).toMatchObject({ type: 'hosting_account', state: 'active', details: { username: 'kc30001', planId: 2, plan: 'Business' } });
|
||
const post = f.calls.find((x) => x.method === 'POST' && x.path === '/clients')!.body;
|
||
expect(post).toMatchObject({ username: 'kc30001', email: 'max@example.test', id_hosting_plan: 2, create_system_domain: true, send_login_credentials: true, notes: 'Kundencenter K-10001 V-30001', contact_data: { company: 'Muster GmbH', first_name: 'Max', last_name: 'Muster' } });
|
||
expect(post).not.toHaveProperty('password'); expect(JSON.stringify(r)).not.toContain('AUTO-GENERATED');
|
||
// Wiederholung (z. B. nach unklarem Timeout): vorhandenes Konto wird übernommen, keine Doppelanlage
|
||
const again = await c.provision!(ctx(), { params, label: 'x', idempotencyKey: 'k2', context: PC });
|
||
expect(again.resource.externalRef).toBe(r.resource.externalRef); expect(f.calls.filter((x) => x.method === 'POST' && x.path === '/clients').length).toBe(1);
|
||
expect(f.state.clients.filter((x) => x.username === 'kc30001').length).toBe(1);
|
||
});
|
||
it('lehnt fremd belegten Benutzernamen und fehlende Angaben ab', async () => {
|
||
const f = createFake(); f.state.clients.push({ id: 9, username: 'kc30001', email: 'x@y.test', notes: 'Kundencenter K-99999 V-30001x', status: 1 });
|
||
await expect(mk(f).provision!(ctx(), { params, label: 'x', idempotencyKey: 'k', context: PC })).rejects.toMatchObject({ code: 'CONFLICT' });
|
||
await expect(mk(createFake()).provision!(ctx(), { params, label: 'x', idempotencyKey: 'k', context: { ...PC, customerEmail: null } })).rejects.toMatchObject({ code: 'BAD_CONFIG' });
|
||
await expect(mk(createFake()).provision!(ctx(), { params: { hostingPlanId: 0 }, label: 'x', idempotencyKey: 'k', context: PC })).rejects.toMatchObject({ code: 'BAD_CONFIG' });
|
||
expect(mk(createFake()).validateProvisioning!({ hostingPlanId: 1, language: 'DE!' })).toMatch(/language/);
|
||
});
|
||
it('meldet vom Panel abgelehnte Eingaben verständlich und wiederholt sie nicht', async () => {
|
||
const f = createFake(); const orig = f.fetch; let n = 0; (f as any).fetch = async (u: string, i: RequestInit) => { if (i.method === 'POST') { n++; return new Response(JSON.stringify({ code: '400', message: 'Invalid property data for: email' }), { status: 400 }); } return orig(u, i); };
|
||
const e = await createKeyHelpConnector({ fetchImpl: f.fetch, sleep: async () => {} }).provision!(ctx(), { params, label: 'x', idempotencyKey: 'k', context: PC }).catch((x) => x) as ConnectorError;
|
||
expect(e.code).toBe('INVALID_INPUT'); expect(e.notSent).toBe(true); expect(e.message).toContain('Invalid property data'); expect(n).toBe(1);
|
||
});
|
||
it('sperrt, entsperrt, wechselt den Tarif und löscht (idempotent)', async () => {
|
||
const f = createFake(); const c = mk(f);
|
||
expect((await c.execute!(ctx(), { action: 'suspend', externalRef: '1', idempotencyKey: 'k' })).resource?.state).toBe('suspended'); expect(f.state.clients[0]!.is_suspended).toBe(true);
|
||
expect((await c.execute!(ctx(), { action: 'suspend', externalRef: '1', idempotencyKey: 'k' })).resource?.state).toBe('suspended'); // Wiederholung wirkungsgleich
|
||
expect((await c.execute!(ctx(), { action: 'unsuspend', externalRef: '1', idempotencyKey: 'k' })).resource?.state).toBe('active');
|
||
const up = await c.execute!(ctx(), { action: 'change_plan', externalRef: '1', params: { planId: 2 }, idempotencyKey: 'k' }); expect(up.resource?.details).toMatchObject({ planId: 2, plan: 'Business' });
|
||
await expect(c.execute!(ctx(), { action: 'change_plan', externalRef: '1', params: {}, idempotencyKey: 'k' })).rejects.toMatchObject({ code: 'BAD_CONFIG' });
|
||
await expect(c.execute!(ctx(), { action: 'extend', externalRef: '1', params: { until: '2027-01-01' }, idempotencyKey: 'k' })).rejects.toMatchObject({ code: 'UNSUPPORTED' });
|
||
await c.execute!(ctx(), { action: 'terminate', externalRef: '2', idempotencyKey: 'k' }); expect(f.state.clients.map((x) => x.id)).toEqual([1]);
|
||
await c.execute!(ctx(), { action: 'terminate', externalRef: '2', idempotencyKey: 'k' }); // schon weg: kein Fehler
|
||
});
|
||
it('liefert den Panel-Login nur als https-Link, gültig 60 Minuten', async () => {
|
||
const r = await mk(createFake()).loginUrl!(ctx(), '1'); expect(r).toEqual({ url: 'https://kh.test/login?token=EINMALIG-1', validForSec: 3600 });
|
||
const f = createFake(); const orig = f.fetch; (f as any).fetch = async (u: string, i: RequestInit) => (String(u).includes('/login/') ? new Response(JSON.stringify({ url: 'http://unsicher.test' }), { status: 200 }) : orig(u, i));
|
||
await expect(createKeyHelpConnector({ fetchImpl: f.fetch }).loginUrl!(ctx(), '1')).rejects.toMatchObject({ code: 'INVALID_RESPONSE' });
|
||
await expect(mk(createFake()).loginUrl!(ctx(), '999')).rejects.toMatchObject({ code: 'NOT_FOUND' });
|
||
});
|
||
});
|
||
|
||
describe('KeyHelp-Connector: Unterobjekte und Mandantentrennung', () => {
|
||
it('listet alle Arten, liefert nie Passwörter und nie Objekte fremder Kunden', async () => {
|
||
const f = createFake(); f.opts.leakForeign = true; const ch = mk(f).children!;
|
||
expect((await ch.kinds(ctx(), '1')).map((k) => [k.kind, k.canWrite])).toEqual([['domain', true], ['email', true], ['database', true], ['ftp', true], ['certificate', false]]);
|
||
expect((await ch.kinds(ctx(), '2')).find((k) => k.kind === 'ftp')!.canWrite).toBe(false); // Recht "ftp" fehlt
|
||
const doms = await ch.list(ctx(), '1', 'domain'); expect(doms.map((d) => d.name)).toEqual(['alpha.example.test', 'alpha.sys.example.test']); // beta.example.test wurde trotz "Leck" gefiltert
|
||
expect(doms[0]!.details).toMatchObject({ letsEncrypt: true, forceHttps: true, emailDomain: true, system: false });
|
||
expect((await ch.list(ctx(), '1', 'email')).map((e) => e.name)).toEqual(['info@alpha.example.test']);
|
||
expect((await ch.list(ctx(), '1', 'database'))[0]!.details).toMatchObject({ user: 'alpha_db', sizeBytes: 2048 });
|
||
expect((await ch.list(ctx(), '1', 'ftp'))[0]!.details).toMatchObject({ home: '/www/' });
|
||
const certs = await ch.list(ctx(), '1', 'certificate'); expect(certs.map((c) => [c.name, c.state])).toEqual([['alpha-cert', 'active'], ['alt', 'expired']]); expect(certs[0]!.validUntil).toBe('2027-01-01T00:00:00.000Z');
|
||
expect(JSON.stringify([doms, await ch.list(ctx(), '1', 'email')])).not.toMatch(/GEHEIM|password/i);
|
||
});
|
||
it('legt Objekte für den Kunden an (Passwort getrennt, Übernahme bei Wiederholung)', async () => {
|
||
const f = createFake(); const ch = mk(f).children!;
|
||
const dom = await ch.act(ctx(), { parentRef: '1', kind: 'domain', op: 'create', data: { domain: 'Neu.Example.Test', phpVersion: '8.3', letsEncrypt: true }, idempotencyKey: 'k' });
|
||
expect(dom.child).toMatchObject({ kind: 'domain', name: 'neu.example.test' }); expect(f.calls.find((c) => c.method === 'POST' && c.path === '/domains')!.body).toMatchObject({ id_user: 1, domain: 'neu.example.test', php_version: '8.3' });
|
||
const mail = await ch.act(ctx(), { parentRef: '1', kind: 'email', op: 'create', data: { local: 'Kontakt', domain: 'alpha.example.test' }, secrets: { password: 'ein-sehr-langes-Passwort-1' }, idempotencyKey: 'k' });
|
||
expect(mail.child!.name).toBe('kontakt@alpha.example.test'); expect(f.calls.find((c) => c.path === '/emails' && c.method === 'POST')!.body).toMatchObject({ id_user: 1, password: 'ein-sehr-langes-Passwort-1' });
|
||
expect(JSON.stringify(mail)).not.toContain('ein-sehr-langes'); expect(JSON.stringify(mail)).not.toContain('AUTO-GENERATED');
|
||
const again = await ch.act(ctx(), { parentRef: '1', kind: 'email', op: 'create', data: { local: 'Kontakt', domain: 'alpha.example.test' }, secrets: { password: 'ein-sehr-langes-Passwort-1' }, idempotencyKey: 'k2' });
|
||
expect(again.child!.id).toBe(mail.child!.id); expect(f.calls.filter((c) => c.path === '/emails' && c.method === 'POST').length).toBe(1); // keine Doppelanlage
|
||
const db = await ch.act(ctx(), { parentRef: '1', kind: 'database', op: 'create', data: {}, secrets: { password: 'db-passwort-lang-123' }, idempotencyKey: 'k' }); expect(db.child!.name).toMatch(/^db\d+$/);
|
||
const ftp = await ch.act(ctx(), { parentRef: '1', kind: 'ftp', op: 'create', data: { username: 'neu_ftp', home: '/www/seite' }, secrets: { password: 'ftp-passwort-lang-123' }, idempotencyKey: 'k' }); expect(ftp.child!.details.home).toBe('/www/seite');
|
||
const up = await ch.act(ctx(), { parentRef: '1', kind: 'email', op: 'update', id: '30', data: { maxSizeBytes: 9999 }, secrets: { password: 'neues-passwort-lang-1' }, idempotencyKey: 'k' });
|
||
expect(f.state.emails.find((e) => e.id === 30)).toMatchObject({ max_size: 9999 }); expect(f.state.emails.find((e) => e.id === 30)).not.toHaveProperty('password'); void up;
|
||
const dsu = await ch.act(ctx(), { parentRef: '1', kind: 'domain', op: 'update', id: '10', data: { phpVersion: '8.2', forceHttps: false }, idempotencyKey: 'k' }); expect(dsu.child!.details).toMatchObject({ phpVersion: '8.2', forceHttps: false });
|
||
});
|
||
it('verhindert Zugriffe auf Objekte fremder Kunden (Administrator-Schlüssel!)', async () => {
|
||
const f = createFake(); const ch = mk(f).children!; const before = JSON.stringify(f.state);
|
||
for (const [kind, id] of [['domain', '20'], ['email', '31'], ['database', '41'], ['ftp', '51']] as const) {
|
||
await expect(ch.act(ctx(), { parentRef: '1', kind, op: 'delete', id, data: {}, idempotencyKey: 'k' })).rejects.toMatchObject({ code: 'NOT_FOUND' });
|
||
await expect(ch.act(ctx(), { parentRef: '1', kind, op: 'update', id, data: { description: 'x' }, secrets: { password: 'ein-langes-passwort-1' }, idempotencyKey: 'k' })).rejects.toMatchObject({ code: 'NOT_FOUND' });
|
||
}
|
||
// E-Mail auf fremder Domain anlegen, Unterdomain unter fremder Hauptdomain, Namenskonflikt mit fremdem Objekt
|
||
await expect(ch.act(ctx(), { parentRef: '1', kind: 'email', op: 'create', data: { local: 'x', domain: 'beta.example.test' }, secrets: { password: 'ein-langes-passwort-1' }, idempotencyKey: 'k' })).rejects.toMatchObject({ code: 'NOT_FOUND' });
|
||
await expect(ch.act(ctx(), { parentRef: '1', kind: 'domain', op: 'create', data: { domain: 'www.beta.example.test', parentDomainId: 20 }, idempotencyKey: 'k' })).rejects.toMatchObject({ code: 'NOT_FOUND' });
|
||
await expect(ch.act(ctx(), { parentRef: '1', kind: 'database', op: 'create', data: { name: 'beta_db' }, secrets: { password: 'ein-langes-passwort-1' }, idempotencyKey: 'k' })).rejects.toMatchObject({ code: 'CONFLICT' });
|
||
expect(JSON.stringify(f.state)).toBe(before); // nichts wurde verändert
|
||
expect(f.calls.filter((c) => c.method !== 'GET').length).toBe(0);
|
||
});
|
||
it('prüft Eingaben streng und schützt Systemobjekte', async () => {
|
||
const f = createFake(); const ch = mk(f).children!; const p = { parentRef: '1', idempotencyKey: 'k' };
|
||
await expect(ch.act(ctx(), { ...p, kind: 'domain', op: 'create', data: { domain: 'kein domain name' } })).rejects.toMatchObject({ code: 'INVALID_INPUT' });
|
||
await expect(ch.act(ctx(), { ...p, kind: 'email', op: 'create', data: { local: 'a b', domain: 'alpha.example.test' }, secrets: { password: 'ein-langes-passwort-1' } })).rejects.toMatchObject({ code: 'INVALID_INPUT' });
|
||
await expect(ch.act(ctx(), { ...p, kind: 'email', op: 'create', data: { local: 'ok', domain: 'alpha.example.test' } })).rejects.toMatchObject({ code: 'INVALID_INPUT' }); // Passwort fehlt
|
||
await expect(ch.act(ctx(), { ...p, kind: 'ftp', op: 'create', data: { home: '/etc' }, secrets: { password: 'ein-langes-passwort-1' } })).rejects.toMatchObject({ code: 'INVALID_INPUT' });
|
||
await expect(ch.act(ctx(), { ...p, kind: 'ftp', op: 'create', data: { home: '/www/../etc' }, secrets: { password: 'ein-langes-passwort-1' } })).rejects.toMatchObject({ code: 'INVALID_INPUT' });
|
||
await expect(ch.act(ctx(), { ...p, kind: 'database', op: 'create', data: { name: 'Bad Name!' }, secrets: { password: 'ein-langes-passwort-1' } })).rejects.toMatchObject({ code: 'INVALID_INPUT' });
|
||
await expect(ch.act(ctx(), { ...p, kind: 'domain', op: 'delete', id: '11', data: {} })).rejects.toMatchObject({ code: 'INVALID_INPUT' }); // System-Domain
|
||
await expect(ch.act(ctx(), { ...p, kind: 'certificate', op: 'delete', id: '60', data: {} })).rejects.toMatchObject({ code: 'UNSUPPORTED' });
|
||
await expect(ch.act(ctx(), { ...p, kind: 'email', op: 'update', id: '30', data: {} })).rejects.toMatchObject({ code: 'INVALID_INPUT' }); // keine Änderung
|
||
expect(f.calls.filter((c) => c.method !== 'GET').length).toBe(0);
|
||
});
|
||
it('Löschen eigener Objekte ist idempotent', async () => {
|
||
const f = createFake(); const ch = mk(f).children!;
|
||
await ch.act(ctx(), { parentRef: '1', kind: 'email', op: 'delete', id: '30', data: {}, idempotencyKey: 'k' }); expect(f.state.emails.map((e) => e.id)).toEqual([31]);
|
||
await ch.act(ctx(), { parentRef: '1', kind: 'email', op: 'delete', id: '30', data: {}, idempotencyKey: 'k' }); // bereits gelöscht
|
||
await ch.act(ctx(), { parentRef: '1', kind: 'domain', op: 'delete', id: '10', data: {}, idempotencyKey: 'k' }); expect(f.state.domains.some((d) => d.id === 10)).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('KeyHelp-Connector: Kunden übernehmen und Tarife anlegen', () => {
|
||
it('liest Kunden mit Kontaktdaten und Tarif (ohne Geheimnisse)', async () => {
|
||
const f = createFake(); f.state.clients[0]!.contact_data = { company: 'Alpha GmbH', first_name: 'Anna', last_name: 'Alpha', telephone: '0123 4567', address: 'Weg 1', zip: '10115', city: 'Berlin', country: 'DE', client_id: 'K-77' };
|
||
const list = await mk(f).listCustomers!(ctx());
|
||
expect(list[0]).toEqual({ externalRef: '1', displayName: 'Alpha GmbH', company: 'Alpha GmbH', firstName: 'Anna', lastName: 'Alpha', email: 'alpha@example.test', phone: '0123 4567',
|
||
address: { street: 'Weg 1', zip: '10115', city: 'Berlin', state: null, country: 'DE' }, legacyNumber: 'K-77', planRef: 'plan:1', planName: 'Starter', state: 'active', createdAt: '2026-01-05T10:00:00.000Z' });
|
||
expect(list[1]).toMatchObject({ displayName: 'beta', company: null, planName: 'Business', state: 'suspended' });
|
||
expect(JSON.stringify(list)).not.toMatch(/GEHEIM|password/i);
|
||
expect(await mk(f).capabilities(ctx())).toEqual(expect.arrayContaining(['customers.list', 'catalog.write']));
|
||
});
|
||
it('legt neue Tarife an: Größen in Byte, Eingaben geprüft, "unbegrenzt" nur wenn die Instanz es kennt', async () => {
|
||
const f = createFake(); const c = mk(f);
|
||
const item = await c.createCatalogItem!(ctx(), { name: 'Neu 50', limits: { diskSpaceGb: 50, trafficGb: 500, domains: 5, emailAccounts: 20, databases: 5, ftpUsers: 3 }, permissions: { ftp: true, ssh: false } });
|
||
const post = f.calls.find((x) => x.method === 'POST' && x.path === '/hosting-plans')!.body;
|
||
expect(post).toMatchObject({ name: 'Neu 50', resources: { disk_space: 50 * 1024 ** 3, traffic: 500 * 1024 ** 3, domains: 5, email_accounts: 20, databases: 5, ftp_users: 3 }, permissions: { ftp: true, ssh: false } });
|
||
expect(item).toMatchObject({ name: 'Neu 50', fixed: true, provisioning: { hostingPlanId: expect.any(Number) } }); expect(item.features).toEqual(expect.arrayContaining(['Speicher: 50 GB', 'Domains: 5', 'FTP']));
|
||
await expect(c.createCatalogItem!(ctx(), { name: 'starter', limits: {} })).rejects.toMatchObject({ code: 'CONFLICT' }); // Name existiert (Groß-/Kleinschreibung egal)
|
||
await expect(c.createCatalogItem!(ctx(), { name: 'X', limits: { domains: -3 } })).rejects.toMatchObject({ code: 'INVALID_INPUT' });
|
||
await expect(c.createCatalogItem!(ctx(), { name: '', limits: {} })).rejects.toMatchObject({ code: 'INVALID_INPUT' });
|
||
const u = await c.createCatalogItem!(ctx(), { name: 'Unbegrenzt', limits: { trafficGb: null, domains: 10 } }); // Instanz kennt -1 (Tarif "Business")
|
||
expect(f.calls.filter((x) => x.method === 'POST' && x.path === '/hosting-plans').pop()!.body.resources.traffic).toBe(-1); expect(u.features).toContain('Traffic: unbegrenzt');
|
||
const fresh = createFake(); fresh.state.plans.forEach((p) => Object.keys(p.resources).forEach((k) => ((p.resources as any)[k] = 5))); fresh.state.clients[1]!.resource_limits = {};
|
||
await expect(mk(fresh).createCatalogItem!(ctx(), { name: 'Ohne Unbegrenzt', limits: { trafficGb: null } })).rejects.toMatchObject({ code: 'INVALID_INPUT' }); // nicht raten
|
||
expect(fresh.calls.some((x) => x.method === 'POST')).toBe(false);
|
||
});
|
||
});
|