import { ForbiddenException, UnauthorizedException } from '@nestjs/common'; import { SessionValidationMiddleware, type SessionRequest } from './session-validation.middleware'; const record = { userId: 'user-1', portal: 'admin' as const, sessionVersion: 3, createdAt: 1, lastActivityAt: 1, lastAuthenticatedAt: 1, absoluteExpiresAt: Date.now() + 1000, }; function request(path = '/api/admin/users', cookie = 'cmpp_admin_session=opaque-token', tenantId?: string): SessionRequest { return { originalUrl: path, header: jest.fn((name: string) => name === 'cookie' ? cookie : name === 'x-tenant-id' ? tenantId : undefined), }; } describe('SessionValidationMiddleware', () => { const cookieName = jest.fn((portal: 'admin' | 'client') => `cmpp_${portal}_session`); it('accepts an active Redis session and exposes its user and record', async () => { const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', tenantId: null, status: 'active', deletedAt: null, sessionVersion: 3 }) } }; const sessions = { cookieName, validate: jest.fn().mockResolvedValue({ status: 'active', record }), remove: jest.fn() }; const middleware = new SessionValidationMiddleware(prisma as never, sessions as never); const currentRequest = request(); const next = jest.fn(); await middleware.use(currentRequest, {}, next); expect(next).toHaveBeenCalledTimes(1); expect(currentRequest.sessionUserId).toBe('user-1'); expect(currentRequest.sessionToken).toBe('opaque-token'); expect(currentRequest.authSession).toEqual(record); }); it('rejects a session after the user session version changes', async () => { const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', tenantId: null, status: 'active', deletedAt: null, sessionVersion: 4 }) } }; const sessions = { cookieName, validate: jest.fn().mockResolvedValue({ status: 'active', record }), remove: jest.fn() }; const middleware = new SessionValidationMiddleware(prisma as never, sessions as never); await expect(middleware.use(request(), {}, jest.fn())).rejects.toBeInstanceOf(UnauthorizedException); expect(sessions.remove).toHaveBeenCalledWith('opaque-token'); }); it('only lets a locked session reach unlock and logout endpoints', async () => { const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', tenantId: null, status: 'active', deletedAt: null, sessionVersion: 3 }) } }; const sessions = { cookieName, validate: jest.fn().mockResolvedValue({ status: 'locked', record }), remove: jest.fn() }; const middleware = new SessionValidationMiddleware(prisma as never, sessions as never); await expect(middleware.use(request(), {}, jest.fn())).rejects.toBeInstanceOf(UnauthorizedException); const next = jest.fn(); await middleware.use(request('/api/admin/auth/session/unlock'), {}, next); expect(next).toHaveBeenCalled(); }); it('selects only the cookie belonging to the requested portal', async () => { const clientRecord = { ...record, portal: 'client' as const }; const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', tenantId: 'tenant-a', status: 'active', deletedAt: null, sessionVersion: 3 }) } }; const sessions = { cookieName, validate: jest.fn().mockResolvedValue({ status: 'active', record: clientRecord }), remove: jest.fn() }; const middleware = new SessionValidationMiddleware(prisma as never, sessions as never); const currentRequest = request('/api/client/users', 'cmpp_admin_session=admin-token; cmpp_client_session=client-token'); await middleware.use(currentRequest, {}, jest.fn()); expect(sessions.validate).toHaveBeenCalledWith('client-token', false); expect(currentRequest.sessionToken).toBe('client-token'); expect(currentRequest.sessionTenantId).toBe('tenant-a'); }); it('rejects a client session whose user is not bound to a tenant', async () => { const clientRecord = { ...record, portal: 'client' as const }; const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', tenantId: null, status: 'active', deletedAt: null, sessionVersion: 3 }) } }; const sessions = { cookieName, validate: jest.fn().mockResolvedValue({ status: 'active', record: clientRecord }), remove: jest.fn() }; const middleware = new SessionValidationMiddleware(prisma as never, sessions as never); await expect(middleware.use(request('/api/client/users', 'cmpp_client_session=client-token'), {}, jest.fn())) .rejects.toBeInstanceOf(ForbiddenException); }); it('rejects and audits a client tenant header that disagrees with the authenticated user', async () => { const clientRecord = { ...record, portal: 'client' as const }; const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', tenantId: 'tenant-a', status: 'active', deletedAt: null, sessionVersion: 3 }) }, operationLog: { create: jest.fn().mockResolvedValue({ id: 'log-1' }) }, }; const sessions = { cookieName, validate: jest.fn().mockResolvedValue({ status: 'active', record: clientRecord }), remove: jest.fn() }; const middleware = new SessionValidationMiddleware(prisma as never, sessions as never); await expect(middleware.use(request('/api/client/users', 'cmpp_client_session=client-token', 'tenant-b'), {}, jest.fn())) .rejects.toMatchObject({ response: expect.objectContaining({ code: 'CLIENT_TENANT_MISMATCH' }) }); expect(prisma.operationLog.create).toHaveBeenCalledWith({ data: expect.objectContaining({ tenantId: 'tenant-a', userId: 'user-1', action: 'security.client_tenant_mismatch' }), }); }); it('does not accept an admin cookie for a client route', async () => { const sessions = { cookieName, validate: jest.fn(), remove: jest.fn() }; const middleware = new SessionValidationMiddleware({} as never, sessions as never); await expect(middleware.use(request('/api/client/users', 'cmpp_admin_session=admin-token'), {}, jest.fn())) .rejects.toBeInstanceOf(UnauthorizedException); expect(sessions.validate).not.toHaveBeenCalled(); }); });