Files
stack/apps/api/src/prisma/rls-context.provider.spec.ts
Jason Woltje 93d403807b
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
feat(#351): Implement RLS context interceptor (fix SEC-API-4)
Implements Row-Level Security (RLS) context propagation via NestJS interceptor and AsyncLocalStorage.

Core Implementation:
- RlsContextInterceptor sets PostgreSQL session variables (app.current_user_id, app.current_workspace_id) within transaction boundaries
- Uses SET LOCAL for transaction-scoped variables, preventing connection pool leakage
- AsyncLocalStorage propagates transaction-scoped Prisma client to services
- Graceful handling of unauthenticated routes
- 30-second transaction timeout with 10-second max wait

Security Features:
- Error sanitization prevents information disclosure to clients
- TransactionClient type provides compile-time safety, prevents invalid method calls
- Defense-in-depth security layer for RLS policy enforcement

Quality Rails Compliance:
- Fixed 154 lint errors in llm-usage module (package-level enforcement)
- Added proper TypeScript typing for Prisma operations
- Resolved all type safety violations

Test Coverage:
- 19 tests (7 provider + 9 interceptor + 3 integration)
- 95.75% overall coverage (100% statements on implementation files)
- All tests passing, zero lint errors

Documentation:
- Comprehensive RLS-CONTEXT-USAGE.md with examples and migration guide

Files Created:
- apps/api/src/common/interceptors/rls-context.interceptor.ts
- apps/api/src/common/interceptors/rls-context.interceptor.spec.ts
- apps/api/src/common/interceptors/rls-context.integration.spec.ts
- apps/api/src/prisma/rls-context.provider.ts
- apps/api/src/prisma/rls-context.provider.spec.ts
- apps/api/src/prisma/RLS-CONTEXT-USAGE.md

Fixes #351

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 12:25:50 -06:00

97 lines
2.8 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from "vitest";
import { getRlsClient, runWithRlsClient, type TransactionClient } from "./rls-context.provider";
describe("RlsContextProvider", () => {
let mockPrismaClient: TransactionClient;
beforeEach(() => {
// Create a mock transaction client (excludes $connect, $disconnect, etc.)
mockPrismaClient = {
$executeRaw: vi.fn(),
} as unknown as TransactionClient;
});
describe("getRlsClient", () => {
it("should return undefined when no RLS context is set", () => {
const client = getRlsClient();
expect(client).toBeUndefined();
});
it("should return the RLS client when context is set", () => {
runWithRlsClient(mockPrismaClient, () => {
const client = getRlsClient();
expect(client).toBe(mockPrismaClient);
});
});
it("should return undefined after context is cleared", () => {
runWithRlsClient(mockPrismaClient, () => {
const client = getRlsClient();
expect(client).toBe(mockPrismaClient);
});
// After runWithRlsClient completes, context should be cleared
const client = getRlsClient();
expect(client).toBeUndefined();
});
});
describe("runWithRlsClient", () => {
it("should execute callback with RLS client available", () => {
const callback = vi.fn(() => {
const client = getRlsClient();
expect(client).toBe(mockPrismaClient);
});
runWithRlsClient(mockPrismaClient, callback);
expect(callback).toHaveBeenCalledTimes(1);
});
it("should clear context after callback completes", () => {
runWithRlsClient(mockPrismaClient, () => {
// Context is set here
});
// Context should be cleared after execution
const client = getRlsClient();
expect(client).toBeUndefined();
});
it("should clear context even if callback throws", () => {
const error = new Error("Test error");
expect(() => {
runWithRlsClient(mockPrismaClient, () => {
throw error;
});
}).toThrow(error);
// Context should still be cleared
const client = getRlsClient();
expect(client).toBeUndefined();
});
it("should support nested contexts", () => {
const outerClient = mockPrismaClient;
const innerClient = {
$executeRaw: vi.fn(),
} as unknown as TransactionClient;
runWithRlsClient(outerClient, () => {
expect(getRlsClient()).toBe(outerClient);
runWithRlsClient(innerClient, () => {
expect(getRlsClient()).toBe(innerClient);
});
// Should restore outer context
expect(getRlsClient()).toBe(outerClient);
});
// Should clear completely after outer context ends
expect(getRlsClient()).toBeUndefined();
});
});
});