import { describe, it, expect, afterEach } from 'vitest'; import { ForbiddenException, type ExecutionContext } from '@nestjs/common'; import jwt from 'jsonwebtoken'; import { RealtimeTokenRestGuard } from './realtime-token.guard'; // Realtime delegation, REST half: a socket-scoped token (aud=iios-message) opens the /message // socket and NOTHING else. The gateway enforces the mirror rule; this guard is the REST side. const SECRET = 'dev-crm-secret'; const token = (payload: Record) => jwt.sign(payload, SECRET, { algorithm: 'HS256', expiresIn: '5m' }); /** An HTTP ExecutionContext carrying the given authorization header. */ function httpCtx(authorization?: string): ExecutionContext { return { getType: () => 'http', switchToHttp: () => ({ getRequest: () => ({ headers: authorization ? { authorization } : {} }) }), } as unknown as ExecutionContext; } const wsCtx = () => ({ getType: () => 'ws' }) as unknown as ExecutionContext; const guard = new RealtimeTokenRestGuard(); describe('RealtimeTokenRestGuard (socket tokens must not drive REST)', () => { afterEach(() => { delete process.env.IIOS_REALTIME_AUDIENCE; }); it('rejects a socket-scoped token (aud=iios-message) on REST', () => { process.env.IIOS_REALTIME_AUDIENCE = 'iios-message'; const ctx = httpCtx(`Bearer ${token({ sub: 'pp_1', appId: 'crm-web', aud: 'iios-message' })}`); expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); }); it('allows a normal actor token (no aud)', () => { process.env.IIOS_REALTIME_AUDIENCE = 'iios-message'; expect(guard.canActivate(httpCtx(`Bearer ${token({ sub: 'pp_1', appId: 'crm-web' })}`))).toBe(true); }); it('allows a REST-audience token (aud=iios-core)', () => { process.env.IIOS_REALTIME_AUDIENCE = 'iios-message'; expect(guard.canActivate(httpCtx(`Bearer ${token({ sub: 'pp_1', aud: 'iios-core' })}`))).toBe(true); }); it('rejects when the socket audience is one of several in an aud array', () => { process.env.IIOS_REALTIME_AUDIENCE = 'iios-message'; const ctx = httpCtx(`Bearer ${token({ sub: 'pp_1', aud: ['iios-core', 'iios-message'] })}`); expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); }); it('is a no-op when IIOS_REALTIME_AUDIENCE is unset (same switch as the socket half)', () => { const ctx = httpCtx(`Bearer ${token({ sub: 'pp_1', aud: 'iios-message' })}`); expect(guard.canActivate(ctx)).toBe(true); }); it('passes through unauthenticated routes (health, metrics, HMAC adapter webhooks)', () => { process.env.IIOS_REALTIME_AUDIENCE = 'iios-message'; expect(guard.canActivate(httpCtx())).toBe(true); expect(guard.canActivate(httpCtx('Hmac abc123'))).toBe(true); // non-bearer scheme }); it('never applies to the socket itself (ws context)', () => { process.env.IIOS_REALTIME_AUDIENCE = 'iios-message'; expect(guard.canActivate(wsCtx())).toBe(true); }); it('passes a malformed bearer through (auth is the controller\'s job, not this filter\'s)', () => { process.env.IIOS_REALTIME_AUDIENCE = 'iios-message'; expect(guard.canActivate(httpCtx('Bearer not-a-jwt'))).toBe(true); }); });