/** * Workspace Isolation Verification Tests * * SEC-API-4: These tests verify that all multi-tenant services properly include * workspaceId filtering in their Prisma queries to ensure tenant isolation. * * Purpose: * - Verify findMany/findFirst queries include workspaceId in where clause * - Verify create operations set workspaceId from context * - Verify update/delete operations check workspaceId * - Use Prisma query spying to verify actual queries include workspaceId * * Note: This is a VERIFICATION test suite - it tests that workspaceId is properly * included in all queries, not that RLS is implemented at the database level. */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; // Services under test import { TasksService } from "../../tasks/tasks.service"; import { ProjectsService } from "../../projects/projects.service"; import { EventsService } from "../../events/events.service"; import { KnowledgeService } from "../../knowledge/knowledge.service"; // Dependencies import { PrismaService } from "../../prisma/prisma.service"; import { ActivityService } from "../../activity/activity.service"; import { LinkSyncService } from "../../knowledge/services/link-sync.service"; import { KnowledgeCacheService } from "../../knowledge/services/cache.service"; import { EmbeddingService } from "../../knowledge/services/embedding.service"; import { OllamaEmbeddingService } from "../../knowledge/services/ollama-embedding.service"; import { EmbeddingQueueService } from "../../knowledge/queues/embedding-queue.service"; // Types import { TaskStatus, TaskPriority, ProjectStatus, EntryStatus } from "@prisma/client"; import { NotFoundException } from "@nestjs/common"; /** * Test fixture IDs */ const WORKSPACE_A = "workspace-a-550e8400-e29b-41d4-a716-446655440001"; const WORKSPACE_B = "workspace-b-550e8400-e29b-41d4-a716-446655440002"; const USER_ID = "user-550e8400-e29b-41d4-a716-446655440003"; const ENTITY_ID = "entity-550e8400-e29b-41d4-a716-446655440004"; describe("SEC-API-4: Workspace Isolation Verification", () => { /** * ============================================================================ * TASKS SERVICE - Workspace Isolation Tests * ============================================================================ */ describe("TasksService - Workspace Isolation", () => { let service: TasksService; let mockPrismaService: Record; let mockActivityService: Record; beforeEach(async () => { mockPrismaService = { task: { create: vi.fn(), findMany: vi.fn(), count: vi.fn(), findUnique: vi.fn(), update: vi.fn(), delete: vi.fn(), }, }; mockActivityService = { logTaskCreated: vi.fn().mockResolvedValue({}), logTaskUpdated: vi.fn().mockResolvedValue({}), logTaskDeleted: vi.fn().mockResolvedValue({}), logTaskCompleted: vi.fn().mockResolvedValue({}), logTaskAssigned: vi.fn().mockResolvedValue({}), }; const module: TestingModule = await Test.createTestingModule({ providers: [ TasksService, { provide: PrismaService, useValue: mockPrismaService }, { provide: ActivityService, useValue: mockActivityService }, ], }).compile(); service = module.get(TasksService); vi.clearAllMocks(); }); describe("create() - workspaceId binding", () => { it("should connect task to provided workspaceId", async () => { const mockTask = { id: ENTITY_ID, workspaceId: WORKSPACE_A, title: "Test Task", status: TaskStatus.NOT_STARTED, priority: TaskPriority.MEDIUM, creatorId: USER_ID, assigneeId: null, projectId: null, parentId: null, description: null, dueDate: null, sortOrder: 0, metadata: {}, createdAt: new Date(), updatedAt: new Date(), completedAt: null, }; (mockPrismaService.task as Record).create = vi .fn() .mockResolvedValue(mockTask); await service.create(WORKSPACE_A, USER_ID, { title: "Test Task" }); expect(mockPrismaService.task.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ workspace: { connect: { id: WORKSPACE_A } }, }), }) ); }); it("should NOT allow task creation without workspaceId binding", async () => { const createCall = (mockPrismaService.task as Record).create as ReturnType< typeof vi.fn >; createCall.mockResolvedValue({ id: ENTITY_ID, workspaceId: WORKSPACE_A, title: "Test", }); await service.create(WORKSPACE_A, USER_ID, { title: "Test" }); // Verify the create call explicitly includes workspace connection const callArgs = createCall.mock.calls[0][0]; expect(callArgs.data.workspace).toBeDefined(); expect(callArgs.data.workspace.connect.id).toBe(WORKSPACE_A); }); }); describe("findAll() - workspaceId filtering", () => { it("should include workspaceId in where clause when provided", async () => { (mockPrismaService.task as Record).findMany = vi .fn() .mockResolvedValue([]); (mockPrismaService.task as Record).count = vi.fn().mockResolvedValue(0); await service.findAll({ workspaceId: WORKSPACE_A }); expect(mockPrismaService.task.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ workspaceId: WORKSPACE_A, }), }) ); expect(mockPrismaService.task.count).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ workspaceId: WORKSPACE_A, }), }) ); }); it("should maintain workspaceId filter when combined with other filters", async () => { (mockPrismaService.task as Record).findMany = vi .fn() .mockResolvedValue([]); (mockPrismaService.task as Record).count = vi.fn().mockResolvedValue(0); await service.findAll({ workspaceId: WORKSPACE_A, status: TaskStatus.IN_PROGRESS, priority: TaskPriority.HIGH, }); const findManyCall = (mockPrismaService.task as Record) .findMany as ReturnType; const whereClause = findManyCall.mock.calls[0][0].where; expect(whereClause.workspaceId).toBe(WORKSPACE_A); expect(whereClause.status).toBe(TaskStatus.IN_PROGRESS); expect(whereClause.priority).toBe(TaskPriority.HIGH); }); it("should use empty where clause if workspaceId not provided (SECURITY CONCERN)", async () => { // NOTE: This test documents current behavior - findAll accepts queries without workspaceId // This is a potential security issue that should be addressed (mockPrismaService.task as Record).findMany = vi .fn() .mockResolvedValue([]); (mockPrismaService.task as Record).count = vi.fn().mockResolvedValue(0); await service.findAll({}); const findManyCall = (mockPrismaService.task as Record) .findMany as ReturnType; const whereClause = findManyCall.mock.calls[0][0].where; // Document that empty query leads to empty where clause expect(whereClause).toEqual({}); }); }); describe("findOne() - workspaceId filtering", () => { it("should include workspaceId in findUnique query", async () => { const mockTask = { id: ENTITY_ID, workspaceId: WORKSPACE_A, title: "Test", subtasks: [], }; (mockPrismaService.task as Record).findUnique = vi .fn() .mockResolvedValue(mockTask); await service.findOne(ENTITY_ID, WORKSPACE_A); expect(mockPrismaService.task.findUnique).toHaveBeenCalledWith( expect.objectContaining({ where: { id: ENTITY_ID, workspaceId: WORKSPACE_A, }, }) ); }); it("should NOT return task from different workspace", async () => { (mockPrismaService.task as Record).findUnique = vi .fn() .mockResolvedValue(null); await expect(service.findOne(ENTITY_ID, WORKSPACE_B)).rejects.toThrow(NotFoundException); // Verify query was scoped to WORKSPACE_B expect(mockPrismaService.task.findUnique).toHaveBeenCalledWith( expect.objectContaining({ where: { id: ENTITY_ID, workspaceId: WORKSPACE_B, }, }) ); }); }); describe("update() - workspaceId filtering", () => { it("should verify task belongs to workspace before update", async () => { const mockTask = { id: ENTITY_ID, workspaceId: WORKSPACE_A, title: "Original", status: TaskStatus.NOT_STARTED, }; (mockPrismaService.task as Record).findUnique = vi .fn() .mockResolvedValue(mockTask); (mockPrismaService.task as Record).update = vi .fn() .mockResolvedValue({ ...mockTask, title: "Updated" }); await service.update(ENTITY_ID, WORKSPACE_A, USER_ID, { title: "Updated" }); // Verify lookup includes workspaceId expect(mockPrismaService.task.findUnique).toHaveBeenCalledWith({ where: { id: ENTITY_ID, workspaceId: WORKSPACE_A }, }); // Verify update includes workspaceId expect(mockPrismaService.task.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: ENTITY_ID, workspaceId: WORKSPACE_A, }, }) ); }); it("should reject update for task in different workspace", async () => { (mockPrismaService.task as Record).findUnique = vi .fn() .mockResolvedValue(null); await expect( service.update(ENTITY_ID, WORKSPACE_B, USER_ID, { title: "Hacked" }) ).rejects.toThrow(NotFoundException); expect(mockPrismaService.task.update).not.toHaveBeenCalled(); }); }); describe("remove() - workspaceId filtering", () => { it("should verify task belongs to workspace before delete", async () => { const mockTask = { id: ENTITY_ID, workspaceId: WORKSPACE_A, title: "To Delete", }; (mockPrismaService.task as Record).findUnique = vi .fn() .mockResolvedValue(mockTask); (mockPrismaService.task as Record).delete = vi .fn() .mockResolvedValue(mockTask); await service.remove(ENTITY_ID, WORKSPACE_A, USER_ID); // Verify lookup includes workspaceId expect(mockPrismaService.task.findUnique).toHaveBeenCalledWith({ where: { id: ENTITY_ID, workspaceId: WORKSPACE_A }, }); // Verify delete includes workspaceId expect(mockPrismaService.task.delete).toHaveBeenCalledWith({ where: { id: ENTITY_ID, workspaceId: WORKSPACE_A, }, }); }); it("should reject delete for task in different workspace", async () => { (mockPrismaService.task as Record).findUnique = vi .fn() .mockResolvedValue(null); await expect(service.remove(ENTITY_ID, WORKSPACE_B, USER_ID)).rejects.toThrow( NotFoundException ); expect(mockPrismaService.task.delete).not.toHaveBeenCalled(); }); }); }); /** * ============================================================================ * PROJECTS SERVICE - Workspace Isolation Tests * ============================================================================ */ describe("ProjectsService - Workspace Isolation", () => { let service: ProjectsService; let mockPrismaService: Record; let mockActivityService: Record; beforeEach(async () => { mockPrismaService = { project: { create: vi.fn(), findMany: vi.fn(), count: vi.fn(), findUnique: vi.fn(), update: vi.fn(), delete: vi.fn(), }, }; mockActivityService = { logProjectCreated: vi.fn().mockResolvedValue({}), logProjectUpdated: vi.fn().mockResolvedValue({}), logProjectDeleted: vi.fn().mockResolvedValue({}), }; const module: TestingModule = await Test.createTestingModule({ providers: [ ProjectsService, { provide: PrismaService, useValue: mockPrismaService }, { provide: ActivityService, useValue: mockActivityService }, ], }).compile(); service = module.get(ProjectsService); vi.clearAllMocks(); }); describe("create() - workspaceId binding", () => { it("should connect project to provided workspaceId", async () => { const mockProject = { id: ENTITY_ID, workspaceId: WORKSPACE_A, name: "Test Project", status: ProjectStatus.PLANNING, creatorId: USER_ID, description: null, color: null, startDate: null, endDate: null, metadata: {}, createdAt: new Date(), updatedAt: new Date(), }; (mockPrismaService.project as Record).create = vi .fn() .mockResolvedValue(mockProject); await service.create(WORKSPACE_A, USER_ID, { name: "Test Project" }); expect(mockPrismaService.project.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ workspace: { connect: { id: WORKSPACE_A } }, }), }) ); }); }); describe("findAll() - workspaceId filtering", () => { it("should include workspaceId in where clause when provided", async () => { (mockPrismaService.project as Record).findMany = vi .fn() .mockResolvedValue([]); (mockPrismaService.project as Record).count = vi.fn().mockResolvedValue(0); await service.findAll({ workspaceId: WORKSPACE_A }); expect(mockPrismaService.project.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ workspaceId: WORKSPACE_A, }), }) ); }); it("should maintain workspaceId filter with status filter", async () => { (mockPrismaService.project as Record).findMany = vi .fn() .mockResolvedValue([]); (mockPrismaService.project as Record).count = vi.fn().mockResolvedValue(0); await service.findAll({ workspaceId: WORKSPACE_A, status: ProjectStatus.ACTIVE, }); const findManyCall = (mockPrismaService.project as Record) .findMany as ReturnType; const whereClause = findManyCall.mock.calls[0][0].where; expect(whereClause.workspaceId).toBe(WORKSPACE_A); expect(whereClause.status).toBe(ProjectStatus.ACTIVE); }); }); describe("findOne() - workspaceId filtering", () => { it("should include workspaceId in findUnique query", async () => { const mockProject = { id: ENTITY_ID, workspaceId: WORKSPACE_A, name: "Test", tasks: [], events: [], _count: { tasks: 0, events: 0 }, }; (mockPrismaService.project as Record).findUnique = vi .fn() .mockResolvedValue(mockProject); await service.findOne(ENTITY_ID, WORKSPACE_A); expect(mockPrismaService.project.findUnique).toHaveBeenCalledWith( expect.objectContaining({ where: { id: ENTITY_ID, workspaceId: WORKSPACE_A, }, }) ); }); it("should NOT return project from different workspace", async () => { (mockPrismaService.project as Record).findUnique = vi .fn() .mockResolvedValue(null); await expect(service.findOne(ENTITY_ID, WORKSPACE_B)).rejects.toThrow(NotFoundException); }); }); describe("update() - workspaceId filtering", () => { it("should verify project belongs to workspace before update", async () => { const mockProject = { id: ENTITY_ID, workspaceId: WORKSPACE_A, name: "Original", status: ProjectStatus.PLANNING, }; (mockPrismaService.project as Record).findUnique = vi .fn() .mockResolvedValue(mockProject); (mockPrismaService.project as Record).update = vi .fn() .mockResolvedValue({ ...mockProject, name: "Updated" }); await service.update(ENTITY_ID, WORKSPACE_A, USER_ID, { name: "Updated" }); expect(mockPrismaService.project.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: ENTITY_ID, workspaceId: WORKSPACE_A, }, }) ); }); it("should reject update for project in different workspace", async () => { (mockPrismaService.project as Record).findUnique = vi .fn() .mockResolvedValue(null); await expect( service.update(ENTITY_ID, WORKSPACE_B, USER_ID, { name: "Hacked" }) ).rejects.toThrow(NotFoundException); expect(mockPrismaService.project.update).not.toHaveBeenCalled(); }); }); describe("remove() - workspaceId filtering", () => { it("should verify project belongs to workspace before delete", async () => { const mockProject = { id: ENTITY_ID, workspaceId: WORKSPACE_A, name: "To Delete", }; (mockPrismaService.project as Record).findUnique = vi .fn() .mockResolvedValue(mockProject); (mockPrismaService.project as Record).delete = vi .fn() .mockResolvedValue(mockProject); await service.remove(ENTITY_ID, WORKSPACE_A, USER_ID); expect(mockPrismaService.project.delete).toHaveBeenCalledWith({ where: { id: ENTITY_ID, workspaceId: WORKSPACE_A, }, }); }); }); }); /** * ============================================================================ * EVENTS SERVICE - Workspace Isolation Tests * ============================================================================ */ describe("EventsService - Workspace Isolation", () => { let service: EventsService; let mockPrismaService: Record; let mockActivityService: Record; beforeEach(async () => { mockPrismaService = { event: { create: vi.fn(), findMany: vi.fn(), count: vi.fn(), findUnique: vi.fn(), update: vi.fn(), delete: vi.fn(), }, }; mockActivityService = { logEventCreated: vi.fn().mockResolvedValue({}), logEventUpdated: vi.fn().mockResolvedValue({}), logEventDeleted: vi.fn().mockResolvedValue({}), }; const module: TestingModule = await Test.createTestingModule({ providers: [ EventsService, { provide: PrismaService, useValue: mockPrismaService }, { provide: ActivityService, useValue: mockActivityService }, ], }).compile(); service = module.get(EventsService); vi.clearAllMocks(); }); describe("create() - workspaceId binding", () => { it("should connect event to provided workspaceId", async () => { const mockEvent = { id: ENTITY_ID, workspaceId: WORKSPACE_A, title: "Test Event", startTime: new Date(), creatorId: USER_ID, description: null, endTime: null, location: null, allDay: false, recurrence: null, projectId: null, metadata: {}, createdAt: new Date(), updatedAt: new Date(), }; (mockPrismaService.event as Record).create = vi .fn() .mockResolvedValue(mockEvent); await service.create(WORKSPACE_A, USER_ID, { title: "Test Event", startTime: new Date(), }); expect(mockPrismaService.event.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ workspace: { connect: { id: WORKSPACE_A } }, }), }) ); }); }); describe("findAll() - workspaceId filtering", () => { it("should include workspaceId in where clause when provided", async () => { (mockPrismaService.event as Record).findMany = vi .fn() .mockResolvedValue([]); (mockPrismaService.event as Record).count = vi.fn().mockResolvedValue(0); await service.findAll({ workspaceId: WORKSPACE_A }); expect(mockPrismaService.event.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ workspaceId: WORKSPACE_A, }), }) ); }); it("should maintain workspaceId filter with date range filter", async () => { (mockPrismaService.event as Record).findMany = vi .fn() .mockResolvedValue([]); (mockPrismaService.event as Record).count = vi.fn().mockResolvedValue(0); const startFrom = new Date("2026-01-01"); const startTo = new Date("2026-12-31"); await service.findAll({ workspaceId: WORKSPACE_A, startFrom, startTo, }); const findManyCall = (mockPrismaService.event as Record) .findMany as ReturnType; const whereClause = findManyCall.mock.calls[0][0].where; expect(whereClause.workspaceId).toBe(WORKSPACE_A); expect(whereClause.startTime).toBeDefined(); }); }); describe("findOne() - workspaceId filtering", () => { it("should include workspaceId in findUnique query", async () => { const mockEvent = { id: ENTITY_ID, workspaceId: WORKSPACE_A, title: "Test", }; (mockPrismaService.event as Record).findUnique = vi .fn() .mockResolvedValue(mockEvent); await service.findOne(ENTITY_ID, WORKSPACE_A); expect(mockPrismaService.event.findUnique).toHaveBeenCalledWith( expect.objectContaining({ where: { id: ENTITY_ID, workspaceId: WORKSPACE_A, }, }) ); }); it("should NOT return event from different workspace", async () => { (mockPrismaService.event as Record).findUnique = vi .fn() .mockResolvedValue(null); await expect(service.findOne(ENTITY_ID, WORKSPACE_B)).rejects.toThrow(NotFoundException); }); }); describe("update() - workspaceId filtering", () => { it("should verify event belongs to workspace before update", async () => { const mockEvent = { id: ENTITY_ID, workspaceId: WORKSPACE_A, title: "Original", startTime: new Date(), }; (mockPrismaService.event as Record).findUnique = vi .fn() .mockResolvedValue(mockEvent); (mockPrismaService.event as Record).update = vi .fn() .mockResolvedValue({ ...mockEvent, title: "Updated" }); await service.update(ENTITY_ID, WORKSPACE_A, USER_ID, { title: "Updated" }); expect(mockPrismaService.event.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: ENTITY_ID, workspaceId: WORKSPACE_A, }, }) ); }); it("should reject update for event in different workspace", async () => { (mockPrismaService.event as Record).findUnique = vi .fn() .mockResolvedValue(null); await expect( service.update(ENTITY_ID, WORKSPACE_B, USER_ID, { title: "Hacked" }) ).rejects.toThrow(NotFoundException); expect(mockPrismaService.event.update).not.toHaveBeenCalled(); }); }); describe("remove() - workspaceId filtering", () => { it("should verify event belongs to workspace before delete", async () => { const mockEvent = { id: ENTITY_ID, workspaceId: WORKSPACE_A, title: "To Delete", }; (mockPrismaService.event as Record).findUnique = vi .fn() .mockResolvedValue(mockEvent); (mockPrismaService.event as Record).delete = vi .fn() .mockResolvedValue(mockEvent); await service.remove(ENTITY_ID, WORKSPACE_A, USER_ID); expect(mockPrismaService.event.delete).toHaveBeenCalledWith({ where: { id: ENTITY_ID, workspaceId: WORKSPACE_A, }, }); }); }); }); /** * ============================================================================ * KNOWLEDGE SERVICE - Workspace Isolation Tests * ============================================================================ */ describe("KnowledgeService - Workspace Isolation", () => { let service: KnowledgeService; let mockPrismaService: Record; beforeEach(async () => { mockPrismaService = { knowledgeEntry: { create: vi.fn(), findMany: vi.fn(), count: vi.fn(), findUnique: vi.fn(), update: vi.fn(), delete: vi.fn(), }, knowledgeEntryVersion: { create: vi.fn(), count: vi.fn(), findMany: vi.fn(), findUnique: vi.fn(), }, knowledgeEntryTag: { deleteMany: vi.fn(), create: vi.fn(), }, knowledgeTag: { findUnique: vi.fn(), create: vi.fn(), }, $transaction: vi.fn((callback) => callback(mockPrismaService)), }; const mockLinkSyncService = { syncLinks: vi.fn().mockResolvedValue(undefined), }; const mockCacheService = { getEntry: vi.fn().mockResolvedValue(null), setEntry: vi.fn().mockResolvedValue(undefined), invalidateEntry: vi.fn().mockResolvedValue(undefined), invalidateSearches: vi.fn().mockResolvedValue(undefined), invalidateGraphs: vi.fn().mockResolvedValue(undefined), invalidateGraphsForEntry: vi.fn().mockResolvedValue(undefined), }; const mockEmbeddingService = { isConfigured: vi.fn().mockReturnValue(false), prepareContentForEmbedding: vi.fn( (title: string, content: string) => `${title} ${content}` ), batchGenerateEmbeddings: vi.fn().mockResolvedValue(0), }; const mockOllamaEmbeddingService = { isConfigured: vi.fn().mockResolvedValue(false), prepareContentForEmbedding: vi.fn( (title: string, content: string) => `${title} ${content}` ), }; const mockEmbeddingQueueService = { queueEmbeddingJob: vi.fn().mockResolvedValue("job-123"), }; const module: TestingModule = await Test.createTestingModule({ providers: [ KnowledgeService, { provide: PrismaService, useValue: mockPrismaService }, { provide: LinkSyncService, useValue: mockLinkSyncService }, { provide: KnowledgeCacheService, useValue: mockCacheService }, { provide: EmbeddingService, useValue: mockEmbeddingService }, { provide: OllamaEmbeddingService, useValue: mockOllamaEmbeddingService }, { provide: EmbeddingQueueService, useValue: mockEmbeddingQueueService }, ], }).compile(); service = module.get(KnowledgeService); vi.clearAllMocks(); }); describe("findAll() - workspaceId filtering", () => { it("should include workspaceId in where clause", async () => { (mockPrismaService.knowledgeEntry as Record).count = vi .fn() .mockResolvedValue(0); (mockPrismaService.knowledgeEntry as Record).findMany = vi .fn() .mockResolvedValue([]); await service.findAll(WORKSPACE_A, {}); expect(mockPrismaService.knowledgeEntry.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ workspaceId: WORKSPACE_A, }), }) ); expect(mockPrismaService.knowledgeEntry.count).toHaveBeenCalledWith({ where: expect.objectContaining({ workspaceId: WORKSPACE_A, }), }); }); it("should maintain workspaceId filter with status filter", async () => { (mockPrismaService.knowledgeEntry as Record).count = vi .fn() .mockResolvedValue(0); (mockPrismaService.knowledgeEntry as Record).findMany = vi .fn() .mockResolvedValue([]); await service.findAll(WORKSPACE_A, { status: EntryStatus.PUBLISHED }); const findManyCall = (mockPrismaService.knowledgeEntry as Record) .findMany as ReturnType; const whereClause = findManyCall.mock.calls[0][0].where; expect(whereClause.workspaceId).toBe(WORKSPACE_A); expect(whereClause.status).toBe(EntryStatus.PUBLISHED); }); }); describe("findOne() - workspaceId filtering", () => { it("should use composite workspaceId_slug key", async () => { const mockEntry = { id: ENTITY_ID, workspaceId: WORKSPACE_A, slug: "test-entry", title: "Test", content: "Content", contentHtml: "

Content

", summary: null, status: EntryStatus.PUBLISHED, visibility: "WORKSPACE", createdAt: new Date(), updatedAt: new Date(), createdBy: USER_ID, updatedBy: USER_ID, tags: [], }; (mockPrismaService.knowledgeEntry as Record).findUnique = vi .fn() .mockResolvedValue(mockEntry); await service.findOne(WORKSPACE_A, "test-entry"); expect(mockPrismaService.knowledgeEntry.findUnique).toHaveBeenCalledWith( expect.objectContaining({ where: { workspaceId_slug: { workspaceId: WORKSPACE_A, slug: "test-entry", }, }, }) ); }); it("should NOT return entry from different workspace", async () => { (mockPrismaService.knowledgeEntry as Record).findUnique = vi .fn() .mockResolvedValue(null); await expect(service.findOne(WORKSPACE_B, "test-entry")).rejects.toThrow(NotFoundException); }); }); describe("create() - workspaceId binding", () => { it("should include workspaceId in create data", async () => { const mockEntry = { id: ENTITY_ID, workspaceId: WORKSPACE_A, slug: "new-entry", title: "New Entry", content: "Content", contentHtml: "

Content

", summary: null, status: EntryStatus.DRAFT, visibility: "PRIVATE", createdAt: new Date(), updatedAt: new Date(), createdBy: USER_ID, updatedBy: USER_ID, tags: [], }; // Mock for ensureUniqueSlug check (mockPrismaService.knowledgeEntry as Record).findUnique = vi .fn() .mockResolvedValue(null); // Mock for transaction (mockPrismaService.$transaction as ReturnType).mockImplementation( async (callback: (tx: Record) => Promise) => { const txMock = { knowledgeEntry: { create: vi.fn().mockResolvedValue(mockEntry), findUnique: vi.fn().mockResolvedValue(mockEntry), }, knowledgeEntryVersion: { create: vi.fn().mockResolvedValue({}), }, knowledgeEntryTag: { deleteMany: vi.fn(), }, knowledgeTag: { findUnique: vi.fn(), create: vi.fn(), }, }; return callback(txMock); } ); await service.create(WORKSPACE_A, USER_ID, { title: "New Entry", content: "Content", }); // Verify transaction was called with workspaceId expect(mockPrismaService.$transaction).toHaveBeenCalled(); }); }); describe("update() - workspaceId filtering", () => { it("should use composite workspaceId_slug key for update", async () => { const mockEntry = { id: ENTITY_ID, workspaceId: WORKSPACE_A, slug: "test-entry", title: "Test", content: "Content", contentHtml: "

Content

", summary: null, status: EntryStatus.PUBLISHED, visibility: "WORKSPACE", createdAt: new Date(), updatedAt: new Date(), createdBy: USER_ID, updatedBy: USER_ID, versions: [{ version: 1 }], tags: [], }; (mockPrismaService.knowledgeEntry as Record).findUnique = vi .fn() .mockResolvedValue(mockEntry); (mockPrismaService.$transaction as ReturnType).mockImplementation( async (callback: (tx: Record) => Promise) => { const txMock = { knowledgeEntry: { update: vi.fn().mockResolvedValue(mockEntry), findUnique: vi.fn().mockResolvedValue(mockEntry), }, knowledgeEntryVersion: { create: vi.fn().mockResolvedValue({}), }, knowledgeEntryTag: { deleteMany: vi.fn(), }, knowledgeTag: { findUnique: vi.fn(), create: vi.fn(), }, }; return callback(txMock); } ); await service.update(WORKSPACE_A, "test-entry", USER_ID, { title: "Updated" }); // Verify findUnique uses composite key expect(mockPrismaService.knowledgeEntry.findUnique).toHaveBeenCalledWith( expect.objectContaining({ where: { workspaceId_slug: { workspaceId: WORKSPACE_A, slug: "test-entry", }, }, }) ); }); it("should reject update for entry in different workspace", async () => { (mockPrismaService.knowledgeEntry as Record).findUnique = vi .fn() .mockResolvedValue(null); await expect( service.update(WORKSPACE_B, "test-entry", USER_ID, { title: "Hacked" }) ).rejects.toThrow(NotFoundException); }); }); describe("remove() - workspaceId filtering", () => { it("should use composite workspaceId_slug key for soft delete", async () => { const mockEntry = { id: ENTITY_ID, workspaceId: WORKSPACE_A, slug: "test-entry", title: "Test", }; (mockPrismaService.knowledgeEntry as Record).findUnique = vi .fn() .mockResolvedValue(mockEntry); (mockPrismaService.knowledgeEntry as Record).update = vi .fn() .mockResolvedValue({ ...mockEntry, status: EntryStatus.ARCHIVED }); await service.remove(WORKSPACE_A, "test-entry", USER_ID); expect(mockPrismaService.knowledgeEntry.update).toHaveBeenCalledWith({ where: { workspaceId_slug: { workspaceId: WORKSPACE_A, slug: "test-entry", }, }, data: { status: EntryStatus.ARCHIVED, updatedBy: USER_ID, }, }); }); it("should reject remove for entry in different workspace", async () => { (mockPrismaService.knowledgeEntry as Record).findUnique = vi .fn() .mockResolvedValue(null); await expect(service.remove(WORKSPACE_B, "test-entry", USER_ID)).rejects.toThrow( NotFoundException ); }); }); describe("batchGenerateEmbeddings() - workspaceId filtering", () => { it("should filter by workspaceId when generating embeddings", async () => { (mockPrismaService.knowledgeEntry as Record).findMany = vi .fn() .mockResolvedValue([]); await service.batchGenerateEmbeddings(WORKSPACE_A); expect(mockPrismaService.knowledgeEntry.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ workspaceId: WORKSPACE_A, }), }) ); }); }); }); /** * ============================================================================ * CROSS-SERVICE SECURITY TESTS * ============================================================================ */ describe("Cross-Service Security Invariants", () => { it("should document that findAll without workspaceId is a security concern", () => { // This test documents the security finding: // TasksService.findAll, ProjectsService.findAll, and EventsService.findAll // accept empty query objects and will not filter by workspaceId. // // Recommendation: Make workspaceId a required parameter or throw an error // when workspaceId is not provided in multi-tenant context. // // KnowledgeService.findAll correctly requires workspaceId as first parameter. expect(true).toBe(true); }); it("should verify all services use composite keys or compound where clauses", () => { // This test documents that all multi-tenant services should: // 1. Use workspaceId in where clauses for findMany/findFirst // 2. Use compound where clauses (id + workspaceId) for findUnique/update/delete // 3. Set workspaceId during create operations // // Current status: // - TasksService: Uses compound where (id, workspaceId) - GOOD // - ProjectsService: Uses compound where (id, workspaceId) - GOOD // - EventsService: Uses compound where (id, workspaceId) - GOOD // - KnowledgeService: Uses composite key (workspaceId_slug) - GOOD expect(true).toBe(true); }); }); });