import 'reflect-metadata'; import { BadRequestException } from '@nestjs/common'; import { Role } from '@prisma/client'; import { describe, expect, it, vi } from 'vitest'; import { IS_PUBLIC_KEY } from './decorators/public.decorator'; import { ROLES_KEY } from './decorators/roles.decorator'; import { AuthController } from './auth.controller'; /** * auth.controller.spec.ts — NEU (260911-fh9, Aufgabe 3). Nagelt die * Mandantenquelle je Handler fest: `me`/`changePassword` reichen * ausschliesslich `user.tenantId` aus dem Sitzungsnachweis (`@CurrentUser()`) * durch; `adminResetPassword` verzweigt nach Rolle des AUFRUFERS — ADMIN * bindet an den eigenen Mandanten, SUPER_ADMIN loest den Mandanten des * ZIELS ueber den gebundenen Fan-out `UserService.findByIdForPlatformAdmin` * auf. Form: `tenant.controller.spec.ts` (Dienst-Attrappen, Rollen-Metadaten * ueber `Reflect.getMetadata`, `reflect-metadata`). */ function makeFakeAuthService() { return { getMe: vi.fn(), changePassword: vi.fn(), adminResetPassword: vi.fn(), } as any; } function makeFakeUserService() { return { findByIdForPlatformAdmin: vi.fn(), } as any; } describe('AuthController.me', () => { it('reicht den Mandanten des Aufrufers und dessen Kennung GENAU durch (das Claim, nicht die Guard-Kennung)', async () => { const authService = makeFakeAuthService(); authService.getMe.mockResolvedValue({ id: 'u1' }); const controller = new AuthController(authService, makeFakeUserService()); await controller.me({ id: 'u1', tenantId: 't1', role: 'USER' }); expect(authService.getMe).toHaveBeenCalledWith('t1', 'u1'); }); it('SUPER_ADMIN, dessen Claim t1 traegt: ebenfalls (\'t1\', \'u1\') — keine Kopfzeile und keine Guard-Kennung koennten das aendern, weil der Handler nur @CurrentUser() liest', async () => { const authService = makeFakeAuthService(); authService.getMe.mockResolvedValue({ id: 'u1' }); const controller = new AuthController(authService, makeFakeUserService()); await controller.me({ id: 'u1', tenantId: 't1', role: Role.SUPER_ADMIN }); expect(authService.getMe).toHaveBeenCalledWith('t1', 'u1'); }); it('liefert null, wenn der Dienst null liefert — wirft NICHT (das ist der Beginn des leeren Rumpfs, (h3))', async () => { const authService = makeFakeAuthService(); authService.getMe.mockResolvedValue(null); const controller = new AuthController(authService, makeFakeUserService()); const result = await controller.me({ id: 'u1', tenantId: 't1', role: 'USER' }); expect(result).toBeNull(); }); }); describe('AuthController.changePassword', () => { it('reicht Mandant, Kennung, beide Kennwoerter und die Antwort durch, liefert die Erfolgsmeldung', async () => { const authService = makeFakeAuthService(); const controller = new AuthController(authService, makeFakeUserService()); const res = {} as any; const result = await controller.changePassword( { id: 'u1', tenantId: 't1', role: 'USER' }, { currentPassword: 'old', newPassword: 'new' } as any, res, ); expect(authService.changePassword).toHaveBeenCalledWith('t1', 'u1', 'old', 'new', res); expect(result).toEqual({ message: 'Password changed successfully.' }); }); }); describe('AuthController.adminResetPassword', () => { it('Aufrufer ADMIN (tenantId t1), Ziel "target": Dienst mit (\'t1\', \'ADMIN\', \'target\', \'new-password\', true) aufgerufen; findByIdForPlatformAdmin NICHT aufgerufen; Erfolgsmeldung', async () => { const authService = makeFakeAuthService(); const userService = makeFakeUserService(); const controller = new AuthController(authService, userService); const result = await controller.adminResetPassword( 'target', { newPassword: 'new-password' } as any, { id: 'admin-1', tenantId: 't1', role: Role.ADMIN }, ); expect(authService.adminResetPassword).toHaveBeenCalledWith( 't1', Role.ADMIN, 'target', 'new-password', true, ); expect(userService.findByIdForPlatformAdmin).not.toHaveBeenCalled(); expect(result).toEqual({ message: 'User password has been reset.' }); }); it('Aufrufer ADMIN, mustChangePassword: false im Rumpf: false wird durchgereicht', async () => { const authService = makeFakeAuthService(); const controller = new AuthController(authService, makeFakeUserService()); await controller.adminResetPassword( 'target', { newPassword: 'new-password', mustChangePassword: false } as any, { id: 'admin-1', tenantId: 't1', role: Role.ADMIN }, ); expect(authService.adminResetPassword).toHaveBeenCalledWith( 't1', Role.ADMIN, 'target', 'new-password', false, ); }); it('Aufrufer SUPER_ADMIN (tenantId t1), Fan-out liefert { id: "target", tenantId: "t9", role: "USER" }: Dienst mit (\'t9\', \'SUPER_ADMIN\', \'target\', ...) — der Mandant des ZIELS, nicht der des Aufrufers; findByIdForPlatformAdmin genau einmal mit "target"', async () => { const authService = makeFakeAuthService(); const userService = makeFakeUserService(); userService.findByIdForPlatformAdmin.mockResolvedValue({ id: 'target', tenantId: 't9', role: 'USER', }); const controller = new AuthController(authService, userService); await controller.adminResetPassword( 'target', { newPassword: 'new-password' } as any, { id: 'super-1', tenantId: 't1', role: Role.SUPER_ADMIN }, ); expect(userService.findByIdForPlatformAdmin).toHaveBeenCalledTimes(1); expect(userService.findByIdForPlatformAdmin).toHaveBeenCalledWith('target'); expect(authService.adminResetPassword).toHaveBeenCalledWith( 't9', Role.SUPER_ADMIN, 'target', 'new-password', true, ); }); it('Aufrufer SUPER_ADMIN, Fan-out liefert null: BadRequestException mit Meldung "User not found", Dienst NICHT aufgerufen', async () => { const authService = makeFakeAuthService(); const userService = makeFakeUserService(); userService.findByIdForPlatformAdmin.mockResolvedValue(null); const controller = new AuthController(authService, userService); await expect( controller.adminResetPassword( 'unknown', { newPassword: 'new-password' } as any, { id: 'super-1', tenantId: 't1', role: Role.SUPER_ADMIN }, ), ).rejects.toThrow(new BadRequestException('User not found')); expect(authService.adminResetPassword).not.toHaveBeenCalled(); }); }); describe('AuthController — Rollen-Metadaten (T-FH9)', () => { it('adminResetPassword traegt genau [Role.ADMIN, Role.SUPER_ADMIN]', () => { const roles = Reflect.getMetadata(ROLES_KEY, AuthController.prototype.adminResetPassword); expect(roles).toEqual([Role.ADMIN, Role.SUPER_ADMIN]); }); it.each(['me', 'changePassword', 'logout', 'login', 'requestReset', 'resetPassword'] as const)( 'Handler %s traegt KEINE Rollenmetadaten', (handlerName) => { const handlerRoles = Reflect.getMetadata( ROLES_KEY, (AuthController.prototype as any)[handlerName], ); expect(handlerRoles).toBeUndefined(); }, ); it('die Klasse selbst traegt KEINE Rollenmetadaten (die Grenze aus Befund B liegt je Handler, nicht klassenweit)', () => { const classRoles = Reflect.getMetadata(ROLES_KEY, AuthController); expect(classRoles).toBeUndefined(); }); }); describe('AuthController — Public-Metadaten (die Grenze aus Befund B als Metadaten-Test)', () => { it.each(['login', 'requestReset', 'resetPassword'] as const)( 'Handler %s (Anmeldeweg) ist @Public()', (handlerName) => { const isPublic = Reflect.getMetadata( IS_PUBLIC_KEY, (AuthController.prototype as any)[handlerName], ); expect(isPublic).toBe(true); }, ); it.each(['me', 'changePassword', 'adminResetPassword', 'logout'] as const)( 'Handler %s (Nach-Anmeldung) ist NICHT @Public() — waere er es, liefe die Bindung an das Claim ins Leere', (handlerName) => { const isPublic = Reflect.getMetadata( IS_PUBLIC_KEY, (AuthController.prototype as any)[handlerName], ); expect(isPublic).toBeUndefined(); }, ); });