Stand vor Einführung des Nacht-Agenten
This commit is contained in:
commit
4763548bfb
168 changed files with 12726 additions and 0 deletions
121
packages/connector-licensing/test/contract.test.ts
Normal file
121
packages/connector-licensing/test/contract.test.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { ConnectorError } from '@kc/connector-sdk';
|
||||
import { runContract } from '@kc/connector-sdk/dist/contract.js';
|
||||
import { createLicensingConnector, localToUtcIso } from '../src/index.js';
|
||||
|
||||
// Anonymisierte Beispielantworten des Lizenzsystems (Struktur wie /licenses/ und /programs/).
|
||||
const PROGRAMS = [{ id: 1, name: 'Beispiel-Tool', description: null, api_key: 'aaaaaaaa-0000-4000-8000-000000000001' }];
|
||||
const LICENSES = [
|
||||
{ id: 10, program_id: 1, license_key: '11111111-2222-4333-8444-555555555555', user_limit: 5, duration_type: 'YEAR', starts_at: '2026-01-01T10:00:00', expires_at: '2027-01-01T10:00:00', is_active: true, activation: { id: 1, license_id: 10, hardware_id: 'HW-ABCDEF123456', ip_address: '203.0.113.5', last_seen_ip: '203.0.113.5', activated_at: '2026-01-02T08:00:00', last_seen_at: '2026-09-25T07:00:00' } },
|
||||
{ id: 11, program_id: 1, license_key: '66666666-7777-4888-8999-000000000000', user_limit: null, duration_type: 'MONTH', starts_at: '2025-01-01T10:00:00', expires_at: '2025-01-31T10:00:00', is_active: true, activation: null },
|
||||
{ id: 12, program_id: 1, license_key: 'abcdefab-cdef-4abc-8def-abcdefabcdef', user_limit: 1, duration_type: 'UNLIMITED', starts_at: null, expires_at: null, is_active: false, activation: null },
|
||||
];
|
||||
const json = (b: unknown, status = 200) => new Response(JSON.stringify(b), { status, headers: { 'content-type': 'application/json' } });
|
||||
const CREATED = { id: 99, program_id: 1, license_key: 'deadbeef-dead-4bee-8fde-adbeefdeadbe', user_limit: 3, duration_type: 'YEAR', starts_at: '2026-09-26T12:00:00', expires_at: '2027-09-26T12:00:00', is_active: true, activation: null };
|
||||
let FEATURES: string[] | null = null;
|
||||
const fakeFetch = (log: { method: string; url: string; body?: string; auth?: string }[] = [], failFirst = 0) => {
|
||||
let fails = failFirst;
|
||||
return (async (url: string, init: RequestInit) => {
|
||||
const u = new URL(url); const h = init.headers as Record<string, string>;
|
||||
log.push({ method: init.method!, url: u.pathname, body: init.body as string, auth: h.authorization });
|
||||
if (fails-- > 0) return json({}, 503);
|
||||
if (u.pathname === '/') return json({ message: 'ok', ...(FEATURES ? { features: FEATURES } : {}) });
|
||||
if (u.pathname === '/token') return (init.body as string).includes('secret') ? json({ access_token: 'tok' }) : json({}, 401);
|
||||
if (u.pathname === '/programs/') return json(PROGRAMS);
|
||||
if (u.pathname === '/licenses/' && init.method === 'POST') { const b = JSON.parse(init.body as string); return json(b.key_prefix && !(FEATURES ?? []).includes('key_prefix') ? CREATED : { ...CREATED, license_key: b.key_prefix ? `${b.key_prefix}-deadbeef-dead-4bee-8fde-adbeefdeadbe` : CREATED.license_key, expires_at: b.expires_at ?? CREATED.expires_at }, 201); }
|
||||
if (u.pathname === '/licenses/') return json(LICENSES);
|
||||
if (u.pathname.startsWith('/licenses/') && init.method === 'PUT') return json({ ...LICENSES[0], ...JSON.parse(init.body as string) });
|
||||
return json({}, 404);
|
||||
}) as unknown as typeof fetch;
|
||||
};
|
||||
const ctx = (extra: Record<string, string> = {}) => ({ config: { baseUrl: 'http://lic.test' }, secrets: extra, correlationId: 'c' });
|
||||
const opts = (f: typeof fetch) => ({ fetchImpl: f, sleep: async () => {}, now: () => new Date('2026-09-26T12:00:00Z') });
|
||||
|
||||
describe('Lizenz-Connector', () => {
|
||||
it('erfüllt den Connector-Vertrag', async () => { await runContract(expect as never, createLicensingConnector(opts(fakeFetch())), ctx()); });
|
||||
it('normalisiert Status, Zeiten (Ortszeit → UTC) und maskiert Schlüssel', async () => {
|
||||
const list = await createLicensingConnector(opts(fakeFetch())).listResources(ctx());
|
||||
expect(list.map((r) => r.state)).toEqual(['active', 'expired', 'suspended']);
|
||||
expect(list[0]!.validUntil).toBe('2027-01-01T09:00:00.000Z'); // Berlin Winterzeit UTC+1
|
||||
expect(list[0]!.name).toBe('Beispiel-Tool · 1111…5555');
|
||||
expect(JSON.stringify(list)).not.toContain('11111111-2222');
|
||||
expect(JSON.stringify(list)).not.toContain('aaaaaaaa-0000'); // Program-API-Key gelangt nie in die Ausgabe
|
||||
});
|
||||
it('meldet Schreib-Fähigkeiten nur mit API-Benutzer', async () => {
|
||||
const c = createLicensingConnector(opts(fakeFetch()));
|
||||
expect(await c.capabilities(ctx())).not.toContain('lifecycle.suspend');
|
||||
expect(await c.capabilities(ctx({ username: 'u', password: 'secret' }))).toContain('lifecycle.suspend');
|
||||
});
|
||||
it('wiederholt GET bei 503 mit Backoff und gibt dann auf', async () => {
|
||||
const log: any[] = [];
|
||||
expect((await createLicensingConnector(opts(fakeFetch(log, 2))).listResources(ctx())).length).toBe(3);
|
||||
await expect(createLicensingConnector(opts(fakeFetch([], 99))).listResources(ctx())).rejects.toMatchObject({ code: 'UPSTREAM_ERROR', retryable: true });
|
||||
});
|
||||
it('führt Aktionen als absoluten Zielzustand aus (idempotent) und wiederholt Schreibzugriffe nicht', async () => {
|
||||
const log: any[] = []; const c = createLicensingConnector(opts(fakeFetch(log)));
|
||||
const r = await c.execute!(ctx({ username: 'u', password: 'secret' }), { action: 'suspend', externalRef: '10', idempotencyKey: 'k' });
|
||||
expect(r.resource?.state).toBe('suspended');
|
||||
expect(log.find((l) => l.method === 'PUT')!.body).toBe('{"is_active":false}');
|
||||
await expect(c.execute!(ctx(), { action: 'suspend', externalRef: '10', idempotencyKey: 'k' })).rejects.toMatchObject({ code: 'UNSUPPORTED' });
|
||||
const w: any[] = []; const failing = createLicensingConnector(opts(fakeFetch(w, 5)));
|
||||
await expect(failing.execute!(ctx({ username: 'u', password: 'secret' }), { action: 'unsuspend', externalRef: '10', idempotencyKey: 'k2' })).rejects.toBeInstanceOf(ConnectorError);
|
||||
expect(w.filter((l) => l.method === 'PUT').length).toBe(1);
|
||||
});
|
||||
it('legt Lizenzen an, validiert Parameter und wiederholt die Anlage nie', async () => {
|
||||
const log: any[] = []; const c = createLicensingConnector(opts(fakeFetch(log)));
|
||||
const auth = ctx({ username: 'u', password: 'secret' });
|
||||
expect(c.validateProvisioning!({ programId: 1, durationType: 'YEAR', userLimit: 3 })).toBeNull();
|
||||
expect(c.validateProvisioning!({ programId: 0, durationType: 'YEAR' })).toMatch(/programId/);
|
||||
expect(c.validateProvisioning!({ programId: 1, durationType: 'DAY' })).toMatch(/durationType/);
|
||||
expect(await c.capabilities(ctx())).not.toContain('lifecycle.create');
|
||||
const r = await c.provision!(auth, { params: { programId: 1, durationType: 'YEAR', userLimit: 3 }, label: 'X', idempotencyKey: 'k' });
|
||||
expect(r.resource.externalRef).toBe('99'); expect(r.resource.name).toBe('Beispiel-Tool · dead…dbe'.replace('…dbe', '…' + 'deadbeef-dead-4bee-8fde-adbeefdeadbe'.slice(-4)));
|
||||
expect(JSON.stringify(r)).not.toContain('deadbeef-dead');
|
||||
expect(log.find((l) => l.method === 'POST' && l.url === '/licenses/')!.body).toBe('{"program_id":1,"user_limit":3,"duration_type":"YEAR","is_active":true}');
|
||||
await expect(c.provision!(auth, { params: { programId: 5, durationType: 'YEAR' }, label: 'X', idempotencyKey: 'k' })).rejects.toMatchObject({ code: 'BAD_CONFIG' });
|
||||
// Anlage bei 503: genau ein POST, kein automatischer Retry
|
||||
const w: any[] = []; const f503 = (async (url: string, init: RequestInit) => { w.push(init.method + ' ' + new URL(url).pathname); const u = new URL(url); if (u.pathname === '/token') return json({ access_token: 't' }); if (u.pathname === '/programs/') return json(PROGRAMS); return json({}, 503); }) as unknown as typeof fetch;
|
||||
await expect(createLicensingConnector(opts(f503)).provision!(auth, { params: { programId: 1, durationType: 'YEAR' }, label: 'X', idempotencyKey: 'k' })).rejects.toMatchObject({ code: 'UPSTREAM_ERROR', ambiguous: true });
|
||||
expect(w.filter((x) => x === 'POST /licenses/').length).toBe(1);
|
||||
});
|
||||
it('liefert Programme als Produktvorlagen mit Vorschlägen, ohne Programm-API-Key', async () => {
|
||||
const items = await createLicensingConnector(opts(fakeFetch())).listCatalog!(ctx());
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0]).toMatchObject({ externalRef: '1', name: 'Beispiel-Tool', category: 'license', provisioning: { programId: 1, durationType: 'YEAR', userLimit: null } });
|
||||
expect(items[0]!.hints!.map((h) => h.label)).toEqual(expect.arrayContaining(['Jahr, 5 Benutzer', 'Monat, unbegrenzt viele Benutzer', 'Unbegrenzt, 1 Benutzer']));
|
||||
expect(JSON.stringify(items)).not.toContain('aaaaaaaa-0000'); // Program-API-Key
|
||||
expect(JSON.stringify(items)).not.toContain('11111111-2222'); // Lizenzschlüssel
|
||||
});
|
||||
it('Edition/Ablauf: ohne Erweiterung im Lizenzsystem wird NICHT angelegt; mit Erweiterung mit Präfix und Ablaufdatum', async () => {
|
||||
const auth = ctx({ username: 'u', password: 'secret' });
|
||||
const params = { programId: 1, durationType: 'WEEK', userLimit: null, keyPrefix: 'TRIAL', validityDays: 14 };
|
||||
// 1) altes Lizenzsystem (keine features): Anlage wird vor dem POST abgelehnt
|
||||
FEATURES = null; const log1: any[] = [];
|
||||
await expect(createLicensingConnector(opts(fakeFetch(log1))).provision!(auth, { params, label: 'X', idempotencyKey: 'k' })).rejects.toMatchObject({ code: 'BAD_CONFIG', notSent: true });
|
||||
expect(log1.some((l) => l.method === 'POST' && l.url === '/licenses/')).toBe(false);
|
||||
expect(await createLicensingConnector(opts(fakeFetch())).capabilities(auth)).not.toContain('license.key_prefix');
|
||||
// 2) erweitertes Lizenzsystem
|
||||
FEATURES = ['key_prefix', 'explicit_expiry']; const log2: any[] = [];
|
||||
const c = createLicensingConnector({ ...opts(fakeFetch(log2)), now: () => new Date('2026-09-26T12:00:00Z') });
|
||||
expect(await c.capabilities(auth)).toEqual(expect.arrayContaining(['license.key_prefix', 'license.expiry']));
|
||||
const r = await c.provision!(auth, { params, label: 'X', idempotencyKey: 'k2' });
|
||||
const post = JSON.parse(log2.find((l) => l.method === 'POST' && l.url === '/licenses/')!.body);
|
||||
expect(post).toMatchObject({ key_prefix: 'TRIAL', duration_type: 'WEEK' }); expect(post.expires_at).toBe('2026-10-10T14:00:00'); // 14 Tage, Berlin-Ortszeit
|
||||
expect(r.resource.details).toMatchObject({ edition: 'TRIAL' });
|
||||
// 3) Lizenzsystem ignoriert das Präfix (meldet features, liefert aber Schlüssel ohne Präfix): als unklar/fehlerhaft melden, nie still übergehen
|
||||
const ignoring = (async (url: string, init: RequestInit) => { const u = new URL(url); if (u.pathname === '/') return json({ features: ['key_prefix'] }); if (u.pathname === '/token') return json({ access_token: 't' }); if (u.pathname === '/programs/') return json(PROGRAMS); return json(CREATED, 201); }) as unknown as typeof fetch;
|
||||
await expect(createLicensingConnector(opts(ignoring)).provision!(auth, { params: { programId: 1, durationType: 'YEAR', keyPrefix: 'PREMIUM' }, label: 'X', idempotencyKey: 'k3' })).rejects.toMatchObject({ code: 'INVALID_RESPONSE', ambiguous: true });
|
||||
expect(c.validateProvisioning!({ programId: 1, durationType: 'YEAR', keyPrefix: 'GOLD' })).toMatch(/keyPrefix/);
|
||||
expect(c.validateProvisioning!({ programId: 1, durationType: 'YEAR', validityDays: 0 })).toMatch(/validityDays/);
|
||||
FEATURES = null;
|
||||
});
|
||||
it('erkennt falsche Zugangsdaten und Nichterreichbarkeit', async () => {
|
||||
await expect(createLicensingConnector(opts(fakeFetch())).listResources(ctx({ username: 'u', password: 'falsch' }))).rejects.toMatchObject({ code: 'AUTH_FAILED' });
|
||||
const down = createLicensingConnector({ ...opts(fakeFetch()), fetchImpl: (async () => { throw Object.assign(new TypeError('fetch failed'), { cause: { code: 'ECONNREFUSED' } }); }) as never });
|
||||
const h = await down.healthCheck(ctx()); expect(h.ok).toBe(false); expect(h.message).toContain('nicht erreichbar');
|
||||
});
|
||||
it('rechnet Ortszeit korrekt um (Sommer-/Winterzeit)', () => {
|
||||
expect(localToUtcIso('2026-07-01T12:00:00', 'Europe/Berlin')).toBe('2026-07-01T10:00:00.000Z');
|
||||
expect(localToUtcIso('2026-01-01T12:00:00', 'Europe/Berlin')).toBe('2026-01-01T11:00:00.000Z');
|
||||
});
|
||||
});
|
||||
113
packages/connector-licensing/test/modern.test.ts
Normal file
113
packages/connector-licensing/test/modern.test.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { runContract } from '@kc/connector-sdk/dist/contract.js';
|
||||
import { createLicensingConnector } from '../src/index.js';
|
||||
|
||||
// Anonymisierte Antworten eines neueren Lizenzsystems (Produktkatalog, Lifecycle-Endpunkte, Service-Tokens, UTC).
|
||||
let FEATURES_OVERRIDE: string[] | null = null;
|
||||
const FEATURES = ['key_prefix', 'explicit_expiry', 'service_tokens', 'lifecycle', 'multi_activation', 'audit', 'utc_timestamps'];
|
||||
const PROGRAMS = [{ id: 1, name: 'Familytool', description: null }, { id: 2, name: 'RP-Framework', description: null }];
|
||||
const GROUPS = [
|
||||
{ id: 2, name: 'Premium', program_id: 1, is_active: true, products: [
|
||||
{ id: 3, name: 'Monatlich', description: 'Premium-Funktionen', price: '2.99', currency: 'EUR', duration_type: 'MONTH', user_limit: null, is_active: true, features: 'Stundenplan\nAufräumplan', modules: 'a,b', badge: null, product_key: 'SECRET-PRODUCT-KEY', public_key: 'SECRET-PUBLIC' },
|
||||
{ id: 4, name: 'Jährlich', price: '29.90', currency: 'EUR', duration_type: 'YEAR', user_limit: null, is_active: true, features: '', modules: '' }] },
|
||||
{ id: 4, name: 'Testen', program_id: 1, is_active: true, products: [{ id: 2, name: 'Testen', price: '0.00', currency: 'EUR', duration_type: 'UNLIMITED', user_limit: 2, is_active: true, features: 'Kalender' }] },
|
||||
{ id: 6, name: 'Server-Lizenzen', program_id: 2, is_active: false, products: [{ id: 7, name: 'Starter', price: '0.00', currency: 'EUR', duration_type: 'MONTH', is_active: false }] },
|
||||
];
|
||||
const lic = (o: object = {}) => ({ id: 18, program_id: 1, product_id: 6, license_key: 'aaaaaaaa-1111-4222-8333-bbbbbbbbbbbb', user_limit: null, duration_type: 'MONTH', starts_at: '2026-09-01T10:00:00Z', expires_at: '2026-10-01T10:00:00Z', is_active: true, status: 'active', blocked: false, suspended_at: null, revoked_at: null, product: { name: 'Premium' }, activation: null, activations: [], ...o });
|
||||
const json = (b: unknown, status = 200) => new Response(JSON.stringify(b), { status, headers: { 'content-type': 'application/json' } });
|
||||
interface Call { method: string; path: string; auth?: string; body?: any }
|
||||
const api = (opts: { licenses?: object[]; features?: string[] | null; log?: Call[] } = {}) => {
|
||||
const log = opts.log ?? [];
|
||||
return (async (url: string, init: RequestInit) => {
|
||||
const u = new URL(url); const h = (init.headers ?? {}) as Record<string, string>; const body = init.body && !String(init.body).includes('=') ? JSON.parse(init.body as string) : init.body;
|
||||
log.push({ method: init.method!, path: u.pathname, auth: h.authorization, body });
|
||||
if (u.pathname === '/') return json({ message: 'ok', features: opts.features === undefined ? (FEATURES_OVERRIDE ?? FEATURES) : opts.features ?? undefined });
|
||||
if (h.authorization !== 'Bearer svc-token-1' && u.pathname !== '/token') return json({}, 401);
|
||||
if (u.pathname === '/programs/') return json(PROGRAMS);
|
||||
if (u.pathname === '/products/groups') return json(GROUPS);
|
||||
if (u.pathname === '/licenses/' && init.method === 'GET') return json(opts.licenses ?? [lic()]);
|
||||
if (/^\/licenses\/\d+$/.test(u.pathname) && init.method === 'GET') return json(lic({ id: Number(u.pathname.split('/')[2]) }));
|
||||
if (u.pathname === '/licenses/' && init.method === 'POST') return json(lic({ id: 99, product_id: body.product_id === 6 ? 6 : null, expires_at: body.expires_at ?? null }), 201);
|
||||
const m = /^\/licenses\/(\d+)\/(suspend|unsuspend|extend)$/.exec(u.pathname);
|
||||
if (m && init.method === 'POST') return json({ changed: true, action: m[2], license: lic({ id: Number(m[1]), is_active: m[2] !== 'suspend', suspended_at: m[2] === 'suspend' ? '2026-09-26T12:00:00Z' : null, status: m[2] === 'suspend' ? 'suspended' : 'active', expires_at: m[2] === 'extend' ? body.until : '2026-10-01T10:00:00Z' }) });
|
||||
return json({}, 404);
|
||||
}) as unknown as typeof fetch;
|
||||
};
|
||||
const ctx = { config: { baseUrl: 'http://lic.test', timezone: 'Europe/Berlin' }, secrets: { token: 'svc-token-1' }, correlationId: 'c' };
|
||||
const mk = (o: Parameters<typeof api>[0] = {}) => createLicensingConnector({ fetchImpl: api(o), sleep: async () => {}, now: () => new Date('2026-09-26T12:00:00Z') });
|
||||
|
||||
describe('Lizenz-Connector (neues Lizenzsystem)', () => {
|
||||
it('erfüllt den Vertrag mit Service-Token (kein Login)', async () => {
|
||||
const log: Call[] = []; await runContract(expect as never, mk({ log }), ctx);
|
||||
expect(log.some((l) => l.path === '/token')).toBe(false);
|
||||
expect(log.filter((l) => l.path !== '/').every((l) => l.auth === 'Bearer svc-token-1')).toBe(true);
|
||||
});
|
||||
it('liest den Produktkatalog des Lizenzsystems (Preis, Dauer, Features) ohne Schlüssel', async () => {
|
||||
const items = await mk().listCatalog!(ctx);
|
||||
expect(items.map((i) => i.name)).toEqual(['Premium – Monatlich', 'Premium – Jährlich', 'Testen', 'Server-Lizenzen – Starter']);
|
||||
const m = items[0]!;
|
||||
expect(m).toMatchObject({ externalRef: 'product:3', group: 'Premium', program: 'Familytool', interval: 'monthly', price: { cents: 299, currency: 'EUR' }, providerActive: true, features: ['Stundenplan', 'Aufräumplan'],
|
||||
provisioning: { programId: 1, productId: 3, durationType: 'MONTH', userLimit: null } });
|
||||
expect(items[1]).toMatchObject({ interval: 'yearly', price: { cents: 2990 } });
|
||||
expect(items[2]).toMatchObject({ interval: 'once', price: { cents: 0 }, provisioning: { userLimit: 2 } });
|
||||
expect(items[3]!.providerActive).toBe(false);
|
||||
expect(JSON.stringify(items)).not.toMatch(/SECRET|product_key|public_key/);
|
||||
});
|
||||
it('erkennt gesperrte, widerrufene und blockierte Lizenzen sowie Ablauf', async () => {
|
||||
const list = await mk({ licenses: [lic({ id: 1 }), lic({ id: 2, blocked: true }), lic({ id: 3, suspended_at: '2026-09-20T00:00:00Z', status: 'suspended', is_active: true }), lic({ id: 4, revoked_at: '2026-09-20T00:00:00Z' }), lic({ id: 5, expires_at: '2026-09-01T00:00:00Z' }), lic({ id: 6, status: 'revoked' })] }).listResources(ctx);
|
||||
expect(list.map((r) => r.state)).toEqual(['active', 'suspended', 'suspended', 'suspended', 'expired', 'suspended']);
|
||||
expect(list[0]!.validUntil).toBe('2026-10-01T10:00:00.000Z'); // UTC bleibt UTC (keine Ortszeit-Verschiebung)
|
||||
expect(list[0]!.details).toMatchObject({ product: 'Premium', edition: 'Premium' });
|
||||
expect(JSON.stringify(list)).not.toContain('aaaaaaaa-1111');
|
||||
});
|
||||
it('nutzt die Lebenszyklus-Endpunkte für Sperren, Entsperren und Verlängern', async () => {
|
||||
const log: Call[] = []; const c = mk({ log });
|
||||
expect((await c.execute!(ctx, { action: 'suspend', externalRef: '18', idempotencyKey: 'k' })).resource?.state).toBe('suspended');
|
||||
expect((await c.execute!(ctx, { action: 'unsuspend', externalRef: '18', idempotencyKey: 'k' })).resource?.state).toBe('active');
|
||||
const r = await c.execute!(ctx, { action: 'extend', externalRef: '18', params: { until: '2027-01-01T00:00:00.000Z' }, idempotencyKey: 'k' });
|
||||
expect(r.resource?.validUntil).toBe('2027-01-01T00:00:00.000Z');
|
||||
const posts = log.filter((l) => l.method === 'POST').map((l) => l.path); expect(posts).toEqual(['/licenses/18/suspend', '/licenses/18/unsuspend', '/licenses/18/extend']);
|
||||
expect(log.find((l) => l.path.endsWith('/extend'))!.body).toMatchObject({ until: '2027-01-01T00:00:00.000Z' });
|
||||
expect(log.some((l) => l.method === 'PUT')).toBe(false);
|
||||
});
|
||||
it('gibt den vollständigen Lizenzschlüssel nur über reveal heraus, sonst nie', async () => {
|
||||
const c = mk();
|
||||
expect(await c.capabilities(ctx)).toContain('secret.reveal');
|
||||
expect(await c.capabilities({ ...ctx, secrets: {} })).not.toContain('secret.reveal');
|
||||
expect(await c.reveal!(ctx, '18')).toEqual([{ label: 'Lizenzschlüssel', value: 'aaaaaaaa-1111-4222-8333-bbbbbbbbbbbb' }]);
|
||||
expect(JSON.stringify(await c.listResources(ctx))).not.toContain('aaaaaaaa-1111'); // Listen enthalten den Schlüssel weiterhin nie
|
||||
await expect(c.reveal!({ ...ctx, secrets: {} }, '18')).rejects.toMatchObject({ code: 'UNSUPPORTED' });
|
||||
});
|
||||
it('meldet Kunde und Herkunft (Kundencenter) nur, wenn das Lizenzsystem es unterstützt', async () => {
|
||||
const context = { source: 'kundencenter' as const, customerNumber: 'K-10001', customerName: 'Muster GmbH', contactName: 'Max Muster', customerEmail: 'max@example.test', orderNumber: 'B-20001', contractNumber: 'V-30001' };
|
||||
const params = { programId: 1, productId: 6, durationType: 'UNLIMITED', userLimit: null };
|
||||
// 1) ohne Unterstützung: Felder werden NICHT gesendet
|
||||
const log1: Call[] = []; await mk({ log: log1 }).provision!(ctx, { params, label: 'X', idempotencyKey: 'k', context });
|
||||
const b1 = log1.find((l) => l.method === 'POST' && l.path === '/licenses/')!.body; expect(b1).not.toHaveProperty('source'); expect(b1).not.toHaveProperty('customer_email');
|
||||
expect(await mk().capabilities(ctx)).not.toContain('license.customer_info');
|
||||
// 2) mit Unterstützung: alle Angaben werden gesendet
|
||||
FEATURES_OVERRIDE = [...FEATURES, 'customer_info'];
|
||||
try {
|
||||
const log2: Call[] = []; const c = mk({ log: log2 }); expect(await c.capabilities(ctx)).toContain('license.customer_info');
|
||||
await c.provision!(ctx, { params, label: 'X', idempotencyKey: 'k2', context });
|
||||
expect(log2.find((l) => l.method === 'POST' && l.path === '/licenses/')!.body).toMatchObject({ source: 'kundencenter', customer_name: 'Muster GmbH', customer_email: 'max@example.test', customer_contact: 'Max Muster', customer_reference: 'K-10001', order_ref: 'B-20001', external_ref: 'V-30001' });
|
||||
} finally { FEATURES_OVERRIDE = null; }
|
||||
});
|
||||
it('legt Lizenzen mit Produkt an und meldet, wenn das Produkt verloren ginge', async () => {
|
||||
const log: Call[] = []; const c = mk({ log });
|
||||
const params = { programId: 1, productId: 6, durationType: 'UNLIMITED', userLimit: null };
|
||||
expect((await c.provision!(ctx, { params, label: 'X', idempotencyKey: 'k' })).resource.externalRef).toBe('99');
|
||||
expect(log.find((l) => l.method === 'POST' && l.path === '/licenses/')!.body).toMatchObject({ program_id: 1, product_id: 6, duration_type: 'UNLIMITED' });
|
||||
// Lizenzsystem ignoriert das Produkt (liefert product_id null): nie still akzeptieren
|
||||
await expect(c.provision!(ctx, { params: { ...params, productId: 7 }, label: 'X', idempotencyKey: 'k2' })).rejects.toMatchObject({ code: 'INVALID_RESPONSE', ambiguous: true });
|
||||
expect(c.validateProvisioning!({ ...params, productId: 0 })).toMatch(/productId/);
|
||||
});
|
||||
it('Testphase mit festem Ablauf sendet UTC und meldet falschen Token als Anmeldefehler', async () => {
|
||||
const log: Call[] = []; const c = mk({ log });
|
||||
await c.provision!(ctx, { params: { programId: 1, durationType: 'WEEK', validityDays: 14, keyPrefix: 'TRIAL' }, label: 'X', idempotencyKey: 'k' }).catch(() => undefined);
|
||||
expect(log.find((l) => l.method === 'POST' && l.path === '/licenses/')!.body.expires_at).toBe('2026-10-10T12:00:00.000Z');
|
||||
await expect(mk().listResources({ ...ctx, secrets: { token: 'falsch' } })).rejects.toMatchObject({ code: 'AUTH_FAILED' });
|
||||
expect(await mk().capabilities(ctx)).toEqual(expect.arrayContaining(['catalog.list', 'lifecycle.suspend', 'lifecycle.create', 'license.key_prefix']));
|
||||
expect(await mk().capabilities({ ...ctx, secrets: {} })).not.toContain('lifecycle.suspend'); // ohne Zugang nur lesend
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue