docs(quick-260911-fh9): Mandantenquelle im auth-Controller festnageln, Klassifikation nachziehen, Ledger-Eintraege anlegen
- auth.controller.spec.ts: NEU. Mandantenquelle je Handler (das Claim fuer me/changePassword; fuer die oberste Rolle der Mandant des Ziels aus dem Fan-out), unbekanntes Ziel, null-Durchreichung von me, Rollen-Metadaten (ROLES_KEY) und Public-Metadaten (IS_PUBLIC_KEY) fuer alle sieben Handler — 23 Faelle; Falsifizierungsnachweis am Fan-out-Zweig durchgefuehrt und zurueckgenommen - docs/mandantentrennung-zugriffsklassifikation.md: Uebersichtszeile auth auf 3/10 (war 8/5), Summenzeile 78/167, Bestandsaufnahme-Zeile auth.service.ts/user auf gebunden (keine gemischt-Zeile mehr fuer diese Datei), Klassen-Verteilung unveraendert bei 64 Paaren mit Stand-Vermerk, Hintergrunddienst-Abschnitt ohne sechsten Fall, Etappe-3-Anmeldeweg-Punkt in "Was diese Etappe NICHT entscheidet"; beide Dokument-Falsifizierungen durchgefuehrt und zurueckgenommen - .planning/WINDOWS.md: zwei neue offene Eintraege (#28 verschluckte Leere im Frontend, Familie #23/#25/#26; #29 Rechteausweitung ADMIN->SUPER_ADMIN im Schwesterweg PATCH /users/:id, T-FH9-05) Baseline wiederhergestellt: 951/951 Tests gruen in 60 Dateien (927+23 neue Faelle plus der in Aufgabe 2 erwartungsgemaess rote Test), Werkzeug 120/120, Typpruefung sauber. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
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();
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user