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); }); });