9eada2e082
Jeder Neuabruf sind bis zu 20 Aufrufe auf dem geteilten AutoDNS-Takt; Benutzer ohne Freigabestufe Verwalten koennten damit Bestellung und Abgleich verdraengen. Der ModuleGuard legt die wirksame Stufe auf den Request, das Flag wird fuer Nicht-Verwalter ignoriert (Zwischenspeicher bleibt). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
248 lines
10 KiB
TypeScript
248 lines
10 KiB
TypeScript
import 'reflect-metadata';
|
|
import { ForbiddenException } from '@nestjs/common';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import { ROLES_KEY } from '../auth/decorators/roles.decorator';
|
|
import { MODULE_MANAGE_KEY, MODULE_SLUG_KEY } from '../module-registry/module.guard';
|
|
import { DomainsController } from './domains.controller';
|
|
|
|
const proto = DomainsController.prototype as any;
|
|
const req = (tenantId?: string) => ({ tenantId }) as any;
|
|
/** Aufrufer mit der Freigabestufe Verwalten (setzt sonst der ModuleGuard). */
|
|
const managerReq = (tenantId?: string) => ({ tenantId, moduleAccessLevel: 'MANAGE' }) as any;
|
|
const userReq = (tenantId?: string) => ({ tenantId, moduleAccessLevel: 'USE' }) as any;
|
|
|
|
const MANAGE_HANDLERS = [
|
|
'getSettings',
|
|
'saveSettings',
|
|
'testConnection',
|
|
'createCustomer',
|
|
'updateCustomer',
|
|
'deleteCustomer',
|
|
'createContact',
|
|
'assignContacts',
|
|
'checkAvailability',
|
|
'createOrder',
|
|
'submitOrder',
|
|
'cancelOrder',
|
|
];
|
|
const USE_HANDLERS = [
|
|
'getStatus',
|
|
'listCustomers',
|
|
'listContacts',
|
|
'listDomains',
|
|
'listOrders',
|
|
'refreshOrders',
|
|
];
|
|
|
|
function makeDirectory() {
|
|
return {
|
|
listCustomers: vi.fn(async (..._a: unknown[]) => []),
|
|
createCustomer: vi.fn(async (..._a: unknown[]) => ({})),
|
|
updateCustomer: vi.fn(async (..._a: unknown[]) => ({})),
|
|
deleteCustomer: vi.fn(async (..._a: unknown[]) => ({})),
|
|
listContacts: vi.fn(async (..._a: unknown[]) => ({})),
|
|
createContact: vi.fn(async (..._a: unknown[]) => ({})),
|
|
assignContacts: vi.fn(async (..._a: unknown[]) => ({})),
|
|
listDomains: vi.fn(async (..._a: unknown[]) => ({})),
|
|
};
|
|
}
|
|
|
|
function makeOrders() {
|
|
return {
|
|
checkAvailability: vi.fn(async (..._a: unknown[]) => ({})),
|
|
listOrders: vi.fn(async (..._a: unknown[]) => []),
|
|
createOrder: vi.fn(async (..._a: unknown[]) => ({})),
|
|
refreshOpenOrders: vi.fn(async (..._a: unknown[]) => []),
|
|
submitOrder: vi.fn(async (..._a: unknown[]) => ({})),
|
|
cancelOrder: vi.fn(async (..._a: unknown[]) => ({})),
|
|
};
|
|
}
|
|
|
|
function makeSettings() {
|
|
return {
|
|
getStatus: vi.fn(async (..._a: unknown[]) => ({})),
|
|
getSettings: vi.fn(async (..._a: unknown[]) => ({})),
|
|
saveSettings: vi.fn(async (..._a: unknown[]) => ({})),
|
|
testConnection: vi.fn(async (..._a: unknown[]) => ({})),
|
|
};
|
|
}
|
|
|
|
describe('DomainsController — Metadaten', () => {
|
|
it('haengt an modules/domains und traegt @UseModule(domains)', () => {
|
|
expect(Reflect.getMetadata('path', DomainsController)).toBe('modules/domains');
|
|
expect(Reflect.getMetadata(MODULE_SLUG_KEY, DomainsController)).toBe('domains');
|
|
});
|
|
|
|
it('Einstellungen, Verbindungstest, Kunden-/Kontakt-Schreibwege verlangen Verwalten, ohne Rollen-Decorator', () => {
|
|
for (const name of MANAGE_HANDLERS) {
|
|
expect(Reflect.getMetadata(MODULE_MANAGE_KEY, proto[name]), name).toBe(true);
|
|
expect(Reflect.getMetadata(ROLES_KEY, proto[name]), name).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('Lesewege stehen auf Benutzen-Ebene', () => {
|
|
for (const name of USE_HANDLERS) {
|
|
expect(Reflect.getMetadata(MODULE_MANAGE_KEY, proto[name]), name).toBeUndefined();
|
|
expect(Reflect.getMetadata(ROLES_KEY, proto[name]), name).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('Pfade und Methoden', () => {
|
|
const route = (name: string) => [
|
|
Reflect.getMetadata('method', proto[name]),
|
|
Reflect.getMetadata('path', proto[name]),
|
|
];
|
|
// RequestMethod: GET 0, POST 1, PUT 2, DELETE 3
|
|
expect(route('getStatus')).toEqual([0, 'status']);
|
|
expect(route('getSettings')).toEqual([0, 'settings']);
|
|
expect(route('saveSettings')).toEqual([2, 'settings']);
|
|
expect(route('testConnection')).toEqual([1, 'connection-test']);
|
|
expect(route('listCustomers')).toEqual([0, 'customers']);
|
|
expect(route('createCustomer')).toEqual([1, 'customers']);
|
|
expect(route('listContacts')).toEqual([0, 'contacts']);
|
|
expect(route('createContact')).toEqual([1, 'contacts']);
|
|
expect(route('assignContacts')).toEqual([1, 'contacts/assign']);
|
|
expect(route('listDomains')).toEqual([0, 'domains']);
|
|
expect(route('updateCustomer')).toEqual([2, 'customers/:id']);
|
|
expect(route('deleteCustomer')).toEqual([3, 'customers/:id']);
|
|
expect(route('checkAvailability')).toEqual([1, 'availability']);
|
|
expect(route('listOrders')).toEqual([0, 'orders']);
|
|
expect(route('createOrder')).toEqual([1, 'orders']);
|
|
expect(route('refreshOrders')).toEqual([1, 'orders/refresh']);
|
|
expect(route('submitOrder')).toEqual([1, 'orders/:id/submit']);
|
|
expect(route('cancelOrder')).toEqual([1, 'orders/:id/cancel']);
|
|
});
|
|
});
|
|
|
|
describe('DomainsController — Routen-Reihenfolge (statisch vor :id)', () => {
|
|
it('deklariert jeden Handler mit :id-Pfad nach allen statischen Handlern', () => {
|
|
const names = Object.getOwnPropertyNames(DomainsController.prototype).filter(
|
|
(n) => n !== 'constructor' && typeof proto[n] === 'function' && n !== 'requireTenantId',
|
|
);
|
|
const isIdRoute = (n: string) => String(Reflect.getMetadata('path', proto[n])).includes(':id');
|
|
const firstId = names.findIndex(isIdRoute);
|
|
expect(firstId, 'es gibt :id-Routen').toBeGreaterThan(-1);
|
|
names.slice(firstId).forEach((n) => {
|
|
expect(isIdRoute(n), `${n} steht nach einer :id-Route, ist aber statisch`).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('DomainsController — Auftraege: Reihenfolge', () => {
|
|
it("'orders/refresh' steht vor jedem :id-Handler", () => {
|
|
const names = Object.getOwnPropertyNames(DomainsController.prototype);
|
|
const refresh = names.indexOf('refreshOrders');
|
|
expect(refresh).toBeGreaterThan(-1);
|
|
for (const n of ['updateCustomer', 'deleteCustomer', 'submitOrder', 'cancelOrder']) {
|
|
expect(refresh, n).toBeLessThan(names.indexOf(n));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('DomainsController — Verhalten', () => {
|
|
it('reicht req.tenantId weiter', async () => {
|
|
const settings = makeSettings();
|
|
const directory = makeDirectory();
|
|
const c = new DomainsController(settings as any, directory as any, makeOrders() as any);
|
|
await c.getStatus(req('t1'));
|
|
await c.getSettings(req('t1'));
|
|
await c.saveSettings(req('t1'), { demoUser: 'x' } as any);
|
|
await c.testConnection(req('t1'), { environment: 'DEMO' });
|
|
expect(settings.getStatus).toHaveBeenCalledWith('t1');
|
|
expect(settings.getSettings).toHaveBeenCalledWith('t1');
|
|
expect(settings.saveSettings).toHaveBeenCalledWith('t1', { demoUser: 'x' });
|
|
expect(settings.testConnection).toHaveBeenCalledWith('t1', 'DEMO');
|
|
});
|
|
|
|
it('Kunden-, Kontakt- und Domainwege reichen req.tenantId weiter; refresh nur bei 1/true', async () => {
|
|
const settings = makeSettings();
|
|
const directory = makeDirectory();
|
|
const c = new DomainsController(settings as any, directory as any, makeOrders() as any);
|
|
const id = '11111111-1111-4111-8111-111111111111';
|
|
await c.listCustomers(req('t1'));
|
|
await c.createCustomer(req('t1'), { name: 'A' } as any);
|
|
await c.updateCustomer(req('t1'), id, { name: 'B' } as any);
|
|
await c.deleteCustomer(req('t1'), id);
|
|
await c.listContacts(managerReq('t1'), '1');
|
|
await c.listContacts(managerReq('t1'), 'true');
|
|
await c.listContacts(managerReq('t1'), undefined);
|
|
await c.listContacts(managerReq('t1'), '0');
|
|
await c.createContact(req('t1'), { type: 'PERSON' } as any);
|
|
await c.assignContacts(req('t1'), { contactIds: [1], customerId: null });
|
|
await c.listDomains(managerReq('t1'), 'true');
|
|
expect(directory.listCustomers).toHaveBeenCalledWith('t1');
|
|
expect(directory.createCustomer).toHaveBeenCalledWith('t1', { name: 'A' });
|
|
expect(directory.updateCustomer).toHaveBeenCalledWith('t1', id, { name: 'B' });
|
|
expect(directory.deleteCustomer).toHaveBeenCalledWith('t1', id);
|
|
expect(directory.listContacts.mock.calls.map((x) => x[1])).toEqual([
|
|
{ refresh: true },
|
|
{ refresh: true },
|
|
{ refresh: false },
|
|
{ refresh: false },
|
|
]);
|
|
expect(directory.createContact).toHaveBeenCalledWith('t1', { type: 'PERSON' });
|
|
expect(directory.assignContacts).toHaveBeenCalledWith('t1', {
|
|
contactIds: [1],
|
|
customerId: null,
|
|
});
|
|
expect(directory.listDomains).toHaveBeenCalledWith('t1', { refresh: true });
|
|
});
|
|
|
|
it('refresh=1 wird ohne Freigabestufe Verwalten ignoriert (kein Neuabruf bei AutoDNS)', async () => {
|
|
const directory = makeDirectory();
|
|
const c = new DomainsController(makeSettings() as any, directory as any, makeOrders() as any);
|
|
await c.listContacts(userReq('t1'), '1');
|
|
await c.listContacts(req('t1'), 'true');
|
|
await c.listDomains(userReq('t1'), '1');
|
|
await c.listDomains(req('t1'), 'true');
|
|
expect(directory.listContacts.mock.calls.map((x) => x[1])).toEqual([
|
|
{ refresh: false },
|
|
{ refresh: false },
|
|
]);
|
|
expect(directory.listDomains.mock.calls.map((x) => x[1])).toEqual([
|
|
{ refresh: false },
|
|
{ refresh: false },
|
|
]);
|
|
});
|
|
|
|
it('ohne Mandantenkontext 403', async () => {
|
|
const c = new DomainsController(
|
|
makeSettings() as any,
|
|
makeDirectory() as any,
|
|
makeOrders() as any,
|
|
);
|
|
await expect(c.getStatus(req(undefined))).rejects.toBeInstanceOf(ForbiddenException);
|
|
});
|
|
});
|
|
|
|
describe('DomainsController — Auftraege: Verhalten', () => {
|
|
const user = { id: 'u1', username: 'anna' } as any;
|
|
const id = '22222222-2222-4222-8222-222222222222';
|
|
|
|
it('reicht Organisation, Benutzer und Auftragsnummer weiter', async () => {
|
|
const orders = makeOrders();
|
|
const c = new DomainsController(makeSettings() as any, makeDirectory() as any, orders as any);
|
|
await c.checkAvailability(req('t1'), { domain: 'beispiel.de' });
|
|
await c.listOrders(req('t1'));
|
|
await c.createOrder(req('t1'), user, { domain: 'beispiel.de' } as any);
|
|
await c.refreshOrders(req('t1'));
|
|
await c.submitOrder(req('t1'), user, id);
|
|
await c.cancelOrder(req('t1'), id);
|
|
expect(orders.checkAvailability).toHaveBeenCalledWith('t1', 'beispiel.de');
|
|
expect(orders.listOrders).toHaveBeenCalledWith('t1');
|
|
expect(orders.createOrder).toHaveBeenCalledWith('t1', 'u1', { domain: 'beispiel.de' });
|
|
expect(orders.refreshOpenOrders).toHaveBeenCalledWith('t1');
|
|
expect(orders.submitOrder).toHaveBeenCalledWith('t1', { id: 'u1', username: 'anna' }, id);
|
|
expect(orders.cancelOrder).toHaveBeenCalledWith('t1', id);
|
|
});
|
|
|
|
it('ohne Mandantenkontext 403', async () => {
|
|
const c = new DomainsController(
|
|
makeSettings() as any,
|
|
makeDirectory() as any,
|
|
makeOrders() as any,
|
|
);
|
|
await expect(c.submitOrder(req(undefined), user, id)).rejects.toBeInstanceOf(ForbiddenException);
|
|
});
|
|
});
|