/** * Tests for knowledge base documents API route * * @vitest-environment node */ import { auditMock, authMockFns, createMockRequest, knowledgeApiUtilsMock } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { mockDbChain } = vi.hoisted(() => { const mockDbChain = { select: vi.fn().mockReturnThis(), from: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), offset: vi.fn().mockReturnThis(), insert: vi.fn().mockReturnThis(), values: vi.fn().mockReturnThis(), update: vi.fn().mockReturnThis(), set: vi.fn().mockReturnThis(), transaction: vi.fn(), } return { mockDbChain } }) vi.mock('@sim/db', () => ({ db: mockDbChain, })) vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock) vi.mock('@/lib/knowledge/documents/service', () => ({ getDocuments: vi.fn(), createSingleDocument: vi.fn(), createDocumentRecords: vi.fn(), processDocumentsWithQueue: vi.fn(), getProcessingConfig: vi.fn(), bulkDocumentOperation: vi.fn(), updateDocument: vi.fn(), deleteDocument: vi.fn(), markDocumentAsFailedTimeout: vi.fn(), retryDocumentProcessing: vi.fn(), KnowledgeBaseFileOwnershipError: class KnowledgeBaseFileOwnershipError extends Error {}, })) vi.mock('@/lib/billing/calculations/usage-monitor', () => ({ checkActorUsageLimits: vi.fn().mockResolvedValue({ isExceeded: false }), })) vi.mock('@sim/audit', () => auditMock) import { createDocumentRecords, createSingleDocument, getDocuments, getProcessingConfig, processDocumentsWithQueue, } from '@/lib/knowledge/documents/service' import { GET, POST } from '@/app/api/knowledge/[id]/documents/route' import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils' describe('Knowledge Base Documents API Route', () => { const mockDocument = { id: 'doc-123', knowledgeBaseId: 'kb-123', filename: 'test-document.pdf', fileUrl: 'https://example.com/test-document.pdf', fileSize: 1024, mimeType: 'application/pdf', chunkCount: 5, tokenCount: 100, characterCount: 500, processingStatus: 'completed' as const, processingStartedAt: new Date(), processingCompletedAt: new Date(), processingError: null, enabled: true, uploadedAt: new Date(), tag1: null, tag2: null, tag3: null, tag4: null, tag5: null, tag6: null, tag7: null, number1: null, number2: null, number3: null, number4: null, number5: null, date1: null, date2: null, boolean1: null, boolean2: null, boolean3: null, deletedAt: null, } const resetMocks = () => { vi.clearAllMocks() Object.values(mockDbChain).forEach((fn) => { if (typeof fn === 'function') { fn.mockClear().mockReset() if (fn !== mockDbChain.transaction) { fn.mockReturnThis() } } }) } beforeEach(() => { resetMocks() vi.stubGlobal('crypto', { randomUUID: vi.fn().mockReturnValue('mock-uuid-1234-5678'), }) }) afterEach(() => { vi.clearAllMocks() }) describe('GET /api/knowledge/[id]/documents', () => { const mockParams = Promise.resolve({ id: 'kb-123' }) it('should retrieve documents successfully for authenticated user', async () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' }, }) vi.mocked(checkKnowledgeBaseAccess).mockResolvedValue({ hasAccess: true, knowledgeBase: { id: 'kb-123', userId: 'user-123' }, }) vi.mocked(getDocuments).mockResolvedValue({ documents: [mockDocument], pagination: { total: 1, limit: 50, offset: 0, hasMore: false, }, }) const req = createMockRequest('GET') const response = await GET(req, { params: mockParams }) const data = await response.json() expect(response.status).toBe(200) expect(data.success).toBe(true) expect(data.data.documents).toHaveLength(1) expect(data.data.documents[0].id).toBe('doc-123') expect(vi.mocked(checkKnowledgeBaseAccess)).toHaveBeenCalledWith('kb-123', 'user-123') expect(vi.mocked(getDocuments)).toHaveBeenCalledWith( 'kb-123', { enabledFilter: undefined, search: undefined, limit: 50, offset: 0, }, expect.any(String) ) }) it('should return documents with default filter', async () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' }, }) vi.mocked(checkKnowledgeBaseAccess).mockResolvedValue({ hasAccess: true, knowledgeBase: { id: 'kb-123', userId: 'user-123' }, }) vi.mocked(getDocuments).mockResolvedValue({ documents: [mockDocument], pagination: { total: 1, limit: 50, offset: 0, hasMore: false, }, }) const req = createMockRequest('GET') const response = await GET(req, { params: mockParams }) expect(response.status).toBe(200) expect(vi.mocked(getDocuments)).toHaveBeenCalledWith( 'kb-123', { enabledFilter: undefined, search: undefined, limit: 50, offset: 0, }, expect.any(String) ) }) it('should filter documents by enabled status when requested', async () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' }, }) vi.mocked(checkKnowledgeBaseAccess).mockResolvedValue({ hasAccess: true, knowledgeBase: { id: 'kb-123', userId: 'user-123' }, }) vi.mocked(getDocuments).mockResolvedValue({ documents: [mockDocument], pagination: { total: 1, limit: 50, offset: 0, hasMore: false, }, }) const url = 'http://localhost:3000/api/knowledge/kb-123/documents?enabledFilter=disabled' const req = new Request(url, { method: 'GET' }) as any const response = await GET(req, { params: mockParams }) expect(response.status).toBe(200) expect(vi.mocked(getDocuments)).toHaveBeenCalledWith( 'kb-123', { enabledFilter: 'disabled', search: undefined, limit: 50, offset: 0, }, expect.any(String) ) }) it('should return unauthorized for unauthenticated user', async () => { authMockFns.mockGetSession.mockResolvedValue(null) const req = createMockRequest('GET') const response = await GET(req, { params: mockParams }) const data = await response.json() expect(response.status).toBe(401) expect(data.error).toBe('Unauthorized') }) it('should return not found for non-existent knowledge base', async () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' }, }) vi.mocked(checkKnowledgeBaseAccess).mockResolvedValue({ hasAccess: false, notFound: true, }) const req = createMockRequest('GET') const response = await GET(req, { params: mockParams }) const data = await response.json() expect(response.status).toBe(404) expect(data.error).toBe('Knowledge base not found') }) it('should return unauthorized for knowledge base without access', async () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' }, }) vi.mocked(checkKnowledgeBaseAccess).mockResolvedValue({ hasAccess: false }) const req = createMockRequest('GET') const response = await GET(req, { params: mockParams }) const data = await response.json() expect(response.status).toBe(401) expect(data.error).toBe('Unauthorized') }) it('should handle database errors', async () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' }, }) vi.mocked(checkKnowledgeBaseAccess).mockResolvedValue({ hasAccess: true, knowledgeBase: { id: 'kb-123', userId: 'user-123' }, }) vi.mocked(getDocuments).mockRejectedValue(new Error('Database error')) const req = createMockRequest('GET') const response = await GET(req, { params: mockParams }) const data = await response.json() expect(response.status).toBe(500) expect(data.error).toBe('Failed to fetch documents') }) }) describe('POST /api/knowledge/[id]/documents - Single Document', () => { const mockParams = Promise.resolve({ id: 'kb-123' }) const validDocumentData = { filename: 'test-document.pdf', fileUrl: 'https://example.com/test-document.pdf', fileSize: 1024, mimeType: 'application/pdf', } it('should create single document successfully', async () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' }, }) vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({ hasAccess: true, knowledgeBase: { id: 'kb-123', userId: 'user-123' }, }) const createdDocument = { id: 'doc-123', knowledgeBaseId: 'kb-123', filename: validDocumentData.filename, fileUrl: validDocumentData.fileUrl, fileSize: validDocumentData.fileSize, mimeType: validDocumentData.mimeType, chunkCount: 0, tokenCount: 0, characterCount: 0, enabled: true, uploadedAt: new Date(), tag1: null, tag2: null, tag3: null, tag4: null, tag5: null, tag6: null, tag7: null, } vi.mocked(createSingleDocument).mockResolvedValue(createdDocument) const req = createMockRequest('POST', validDocumentData) const response = await POST(req, { params: mockParams }) const data = await response.json() expect(response.status).toBe(200) expect(data.success).toBe(true) expect(data.data.filename).toBe(validDocumentData.filename) expect(data.data.fileUrl).toBe(validDocumentData.fileUrl) expect(vi.mocked(createSingleDocument)).toHaveBeenCalledWith( validDocumentData, 'kb-123', expect.any(String), 'user-123' ) }) it('should validate single document data', async () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' }, }) vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({ hasAccess: true, knowledgeBase: { id: 'kb-123', userId: 'user-123' }, }) const invalidData = { filename: '', // Invalid: empty filename fileUrl: 'invalid-url', // Invalid: not a valid URL fileSize: 0, // Invalid: size must be > 0 mimeType: '', // Invalid: empty mime type } const req = createMockRequest('POST', invalidData) const response = await POST(req, { params: mockParams }) const data = await response.json() expect(response.status).toBe(400) expect(data.error).toBe('Invalid request data') expect(data.details).toBeDefined() }) }) describe('POST /api/knowledge/[id]/documents - Bulk Documents', () => { const mockParams = Promise.resolve({ id: 'kb-123' }) const validBulkData = { bulk: true, documents: [ { filename: 'doc1.pdf', fileUrl: 'https://example.com/doc1.pdf', fileSize: 1024, mimeType: 'application/pdf', }, { filename: 'doc2.pdf', fileUrl: 'https://example.com/doc2.pdf', fileSize: 2048, mimeType: 'application/pdf', }, ], processingOptions: { recipe: 'default', lang: 'en', }, } it('should create bulk documents successfully', async () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' }, }) vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({ hasAccess: true, knowledgeBase: { id: 'kb-123', userId: 'user-123' }, }) const createdDocuments = [ { documentId: 'doc-1', filename: 'doc1.pdf', fileUrl: 'https://example.com/doc1.pdf', fileSize: 1024, mimeType: 'application/pdf', }, { documentId: 'doc-2', filename: 'doc2.pdf', fileUrl: 'https://example.com/doc2.pdf', fileSize: 2048, mimeType: 'application/pdf', }, ] vi.mocked(createDocumentRecords).mockResolvedValue(createdDocuments) vi.mocked(processDocumentsWithQueue).mockResolvedValue(undefined) vi.mocked(getProcessingConfig).mockReturnValue({ maxConcurrentDocuments: 8, batchSize: 20, delayBetweenBatches: 100, delayBetweenDocuments: 0, }) const req = createMockRequest('POST', validBulkData) const response = await POST(req, { params: mockParams }) const data = await response.json() expect(response.status).toBe(200) expect(data.success).toBe(true) expect(data.data.total).toBe(2) expect(data.data.documentsCreated).toHaveLength(2) expect(data.data.processingMethod).toBe('background') expect(vi.mocked(createDocumentRecords)).toHaveBeenCalledWith( validBulkData.documents, 'kb-123', expect.any(String), 'user-123' ) expect(vi.mocked(processDocumentsWithQueue)).toHaveBeenCalled() }) it('should validate bulk document data', async () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' }, }) vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({ hasAccess: true, knowledgeBase: { id: 'kb-123', userId: 'user-123' }, }) const invalidBulkData = { bulk: true, documents: [ { filename: '', // Invalid: empty filename fileUrl: 'invalid-url', fileSize: 0, mimeType: '', }, ], processingOptions: { recipe: 'default', lang: 'en', }, } const req = createMockRequest('POST', invalidBulkData) const response = await POST(req, { params: mockParams }) const data = await response.json() expect(response.status).toBe(400) expect(data.error).toBe('Invalid request data') expect(data.details).toBeDefined() }) it('should handle processing errors gracefully', async () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' }, }) vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({ hasAccess: true, knowledgeBase: { id: 'kb-123', userId: 'user-123' }, }) const createdDocuments = [ { documentId: 'doc-1', filename: 'doc1.pdf', fileUrl: 'https://example.com/doc1.pdf', fileSize: 1024, mimeType: 'application/pdf', }, ] vi.mocked(createDocumentRecords).mockResolvedValue(createdDocuments) vi.mocked(processDocumentsWithQueue).mockResolvedValue(undefined) vi.mocked(getProcessingConfig).mockReturnValue({ maxConcurrentDocuments: 8, batchSize: 20, delayBetweenBatches: 100, delayBetweenDocuments: 0, }) const req = createMockRequest('POST', validBulkData) const response = await POST(req, { params: mockParams }) const data = await response.json() expect(response.status).toBe(200) expect(data.success).toBe(true) }) }) describe('POST /api/knowledge/[id]/documents - Authentication & Authorization', () => { const mockParams = Promise.resolve({ id: 'kb-123' }) const validDocumentData = { filename: 'test-document.pdf', fileUrl: 'https://example.com/test-document.pdf', fileSize: 1024, mimeType: 'application/pdf', } it('should return unauthorized for unauthenticated user', async () => { authMockFns.mockGetSession.mockResolvedValue(null) const req = createMockRequest('POST', validDocumentData) const response = await POST(req, { params: mockParams }) const data = await response.json() expect(response.status).toBe(401) expect(data.error).toBe('Unauthorized') }) it('should return not found for non-existent knowledge base', async () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' }, }) vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({ hasAccess: false, notFound: true, }) const req = createMockRequest('POST', validDocumentData) const response = await POST(req, { params: mockParams }) const data = await response.json() expect(response.status).toBe(404) expect(data.error).toBe('Knowledge base not found') }) it('should return unauthorized for knowledge base without access', async () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' }, }) vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({ hasAccess: false }) const req = createMockRequest('POST', validDocumentData) const response = await POST(req, { params: mockParams }) const data = await response.json() expect(response.status).toBe(401) expect(data.error).toBe('Unauthorized') }) it('should handle database errors during creation', async () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' }, }) vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({ hasAccess: true, knowledgeBase: { id: 'kb-123', userId: 'user-123' }, }) vi.mocked(createSingleDocument).mockRejectedValue(new Error('Database error')) const req = createMockRequest('POST', validDocumentData) const response = await POST(req, { params: mockParams }) const data = await response.json() expect(response.status).toBe(500) expect(data.error).toBe('Database error') }) }) })