ceo-api/src/auth/tenant.guard.spec.ts
valentinbvro 42cefaddb2 feat: Platform Kernel v1 -- workspaces, ExecutionContext, /v1/navigation, event envelope
Specul de arhitectura cere Platform Kernel INAINTEA modulelor de domeniu.
Modulele A1/A2 au fost construite peste un kernel caruia ii lipseau exact
piesele astea. Le adaug acum, aditiv, fara sa rup ce merge.

- workspaces: tenant != workspace. Workspace-ul e contextul de lucru DIN
  tenant. Backfill: fiecare tenant existent primeste workspace implicit,
  altfel SessionGuard i-ar respinge toate requesturile.
- memberships.workspace_id + valid_from/valid_until: rol per workspace si
  acces delegat cu expirare (contabil pana la o data). SessionGuard respinge
  membership expirat si membership legat de alt workspace.
- ExecutionContext inlocuieste sesiunea subtire (userId+tenantId+role):
  requestId, correlationId, workspaceId, membershipId, roles, permissions,
  purpose, timezone, source. Tipul vechi ramane exportat sub acelasi nume,
  ca sa nu ating ~15 module de domeniu doar pentru o redenumire.
- GET /v1/navigation: menu registry mutat in backend. Filtreaza pe rol, tip
  de workspace, permisiuni si feature flags; intoarce doar itemii autorizati.
  Ramane UX, nu securitate -- fiecare endpoint verifica din nou.
- event envelope: workspace_id, occurred_at, actor_id, aggregate_type,
  causation_id, classification, provenance
- audit envelope: workspace_id, actor_type, purpose, changed_fields,
  before/after hash, session_id
2026-07-29 11:46:20 +02:00

88 lines
2.9 KiB
TypeScript

import { ForbiddenException } from '@nestjs/common';
import type { ExecutionContext } from '@nestjs/common';
import type { Reflector } from '@nestjs/core';
import { TenantGuard, type AuthenticatedSession } from './tenant.guard';
interface FakeRequest {
session?: AuthenticatedSession;
body?: Record<string, unknown>;
query?: Record<string, unknown>;
}
function contextFor(request: FakeRequest, meta: { isPublic?: boolean; isAuthOnly?: boolean } = {}) {
const reflector = {
getAllAndOverride: jest.fn((key: string) => {
if (key === 'isPublic') {
return meta.isPublic ?? false;
}
if (key === 'isAuthOnly') {
return meta.isAuthOnly ?? false;
}
return false;
}),
} as unknown as Reflector;
const context = {
getHandler: () => ({}),
getClass: () => ({}),
switchToHttp: () => ({ getRequest: () => request }),
} as unknown as ExecutionContext;
return { guard: new TenantGuard(reflector), context };
}
const session: AuthenticatedSession = {
requestId: '00000000-0000-0000-0000-0000000000r1',
correlationId: '00000000-0000-0000-0000-0000000000c1',
sessionId: 'sess-1',
userId: '11111111-1111-1111-1111-111111111111',
tenantId: '22222222-2222-2222-2222-222222222222',
workspaceId: '44444444-4444-4444-4444-444444444444',
membershipId: '55555555-5555-5555-5555-555555555555',
role: 'member',
roles: ['member'],
permissions: ['task:read'],
purpose: 'user_request',
timezone: 'Europe/Bucharest',
source: 'web',
};
describe('TenantGuard', () => {
it('rejects requests without an active tenant session (deny-by-default)', () => {
const { guard, context } = contextFor({ body: {}, query: {} });
expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
});
it('rejects client-supplied tenantId that differs from the session tenant', () => {
const { guard, context } = contextFor({
session,
body: { tenantId: '33333333-3333-3333-3333-333333333333' },
query: {},
});
expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
});
it('rejects tenantId smuggled through the query string', () => {
const { guard, context } = contextFor({
session,
body: {},
query: { tenantId: '33333333-3333-3333-3333-333333333333' },
});
expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
});
it('allows a request whose session tenant matches', () => {
const { guard, context } = contextFor({ session, body: {}, query: {} });
expect(guard.canActivate(context)).toBe(true);
});
it('allows public routes without a session', () => {
const { guard, context } = contextFor({}, { isPublic: true });
expect(guard.canActivate(context)).toBe(true);
});
it('allows auth-only bootstrap routes without a tenant', () => {
const { guard, context } = contextFor({}, { isAuthOnly: true });
expect(guard.canActivate(context)).toBe(true);
});
});