From 14a738a2a5c8e6838f92e4e57c53973271ad6719 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:32:40 +0000 Subject: [PATCH] CodeRabbit Generated Unit Tests: Add comprehensive Jest unit tests for backend modules --- backend/tests/clean.test.js | 408 +++++++++++++++++++++ backend/tests/embeddingService.test.js | 486 +++++++++++++++++++++++++ backend/tests/geminiService.test.js | 184 ++++++++++ backend/tests/groqService.test.js | 173 +++++++++ backend/tests/queryVectordb.test.js | 456 +++++++++++++++++++++++ backend/tests/tempAuthCheck.test.js | 250 +++++++++++++ 6 files changed, 1957 insertions(+) create mode 100644 backend/tests/clean.test.js create mode 100644 backend/tests/embeddingService.test.js create mode 100644 backend/tests/geminiService.test.js create mode 100644 backend/tests/groqService.test.js create mode 100644 backend/tests/queryVectordb.test.js create mode 100644 backend/tests/tempAuthCheck.test.js diff --git a/backend/tests/clean.test.js b/backend/tests/clean.test.js new file mode 100644 index 0000000..ca2d9ac --- /dev/null +++ b/backend/tests/clean.test.js @@ -0,0 +1,408 @@ +// tests/clean.test.js + +const { clean } = require('../controllers/clean'); + +// Mock groqService +jest.mock('../utils/llm/groqService', () => ({ + getClient: jest.fn(), +})); + +// Mock config +jest.mock('../utils/config', () => ({ + GROQ_API_KEYS: ['mock-key'], +})); + +const groqService = require('../utils/llm/groqService'); + +describe('clean() Function', () => { + let mockGroqClient; + + beforeEach(() => { + jest.clearAllMocks(); + + // Setup default mock Groq client + mockGroqClient = { + chat: { + completions: { + create: jest.fn(), + }, + }, + }; + + groqService.getClient.mockReturnValue(mockGroqClient); + }); + + describe('Happy Path - Successful Cleaning', () => { + it('should successfully clean and parse valid transcription on first attempt', async () => { + const rawText = 'Um, so like, you know, AI is really cool and stuff.'; + const expectedJson = [ + { + summary: 'Discussion about AI being cool.', + refined_text: '- AI is really cool.\\n', + }, + ]; + + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [ + { + message: { + content: JSON.stringify(expectedJson), + }, + }, + ], + }); + + const result = await clean(rawText); + + expect(result).toEqual(expectedJson); + expect(mockGroqClient.chat.completions.create).toHaveBeenCalledTimes(1); + }); + + it('should extract JSON from response with surrounding text', async () => { + const rawText = 'Test transcription'; + const expectedJson = [ + { summary: 'Test', refined_text: '- Test\\n' }, + ]; + const responseWithExtra = `Here is the JSON:\n${JSON.stringify(expectedJson)}\nThat's it!`; + + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [{ message: { content: responseWithExtra } }], + }); + + const result = await clean(rawText); + + expect(result).toEqual(expectedJson); + }); + + it('should extract JSON array with code block markers', async () => { + const rawText = 'Test transcription'; + const expectedJson = [ + { summary: 'Test', refined_text: '- Test\\n' }, + ]; + const responseWithCodeBlock = `\`\`\`json\n${JSON.stringify(expectedJson)}\n\`\`\``; + + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [{ message: { content: responseWithCodeBlock } }], + }); + + const result = await clean(rawText); + + expect(result).toEqual(expectedJson); + }); + + it('should handle multiple chunks in response', async () => { + const rawText = 'Long meeting transcription...'; + const expectedJson = [ + { summary: 'Introduction', refined_text: '- Hello everyone\\n' }, + { summary: 'Main topic', refined_text: '- Let\'s discuss AI\\n' }, + { summary: 'Conclusion', refined_text: '- Thank you\\n' }, + ]; + + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [{ message: { content: JSON.stringify(expectedJson) } }], + }); + + const result = await clean(rawText); + + expect(result).toEqual(expectedJson); + expect(result.length).toBe(3); + }); + }); + + describe('Retry Mechanism', () => { + it('should retry up to 3 times on failure', async () => { + const rawText = 'Test text'; + + mockGroqClient.chat.completions.create + .mockRejectedValueOnce(new Error('API Error')) + .mockRejectedValueOnce(new Error('API Error')) + .mockResolvedValueOnce({ + choices: [ + { + message: { + content: JSON.stringify([{ summary: 'Success', refined_text: '- Success\\n' }]), + }, + }, + ], + }); + + const result = await clean(rawText); + + expect(mockGroqClient.chat.completions.create).toHaveBeenCalledTimes(3); + expect(result).toHaveLength(1); + }); + + it('should retry when no valid JSON array found', async () => { + const rawText = 'Test text'; + + mockGroqClient.chat.completions.create + .mockResolvedValueOnce({ + choices: [{ message: { content: 'Invalid response without JSON' } }], + }) + .mockResolvedValueOnce({ + choices: [ + { + message: { + content: JSON.stringify([{ summary: 'Valid', refined_text: '- Valid\\n' }]), + }, + }, + ], + }); + + const result = await clean(rawText); + + expect(mockGroqClient.chat.completions.create).toHaveBeenCalledTimes(2); + expect(result).toEqual([{ summary: 'Valid', refined_text: '- Valid\\n' }]); + }); + + it('should throw error after 3 failed attempts', async () => { + const rawText = 'Test text'; + const apiError = new Error('Persistent API Error'); + + mockGroqClient.chat.completions.create.mockRejectedValue(apiError); + + await expect(clean(rawText)).rejects.toThrow('Persistent API Error'); + expect(mockGroqClient.chat.completions.create).toHaveBeenCalledTimes(3); + }); + + it('should throw error after 3 attempts with invalid responses', async () => { + const rawText = 'Test text'; + + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [{ message: { content: 'No JSON here at all' } }], + }); + + await expect(clean(rawText)).rejects.toThrow('Failed to clean transcription after multiple attempts.'); + expect(mockGroqClient.chat.completions.create).toHaveBeenCalledTimes(3); + }); + }); + + describe('API Request Configuration', () => { + it('should call Groq API with correct parameters', async () => { + const rawText = 'Test input'; + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [{ message: { content: '[{"summary":"Test","refined_text":"- Test\\n"}]' } }], + }); + + await clean(rawText); + + expect(mockGroqClient.chat.completions.create).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ role: 'system' }), + expect.objectContaining({ role: 'user', content: rawText }), + ]), + model: 'openai/gpt-oss-120b', + temperature: 1, + max_completion_tokens: 8192, + top_p: 1, + stream: false, + reasoning_effort: 'medium', + stop: null, + }) + ); + }); + + it('should use groqService.getClient() for key rotation', async () => { + const rawText = 'Test'; + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [{ message: { content: '[{"summary":"T","refined_text":"- T\\n"}]' } }], + }); + + await clean(rawText); + + expect(groqService.getClient).toHaveBeenCalledTimes(1); + }); + }); + + describe('Edge Cases', () => { + it('should handle empty choices array', async () => { + const rawText = 'Test'; + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [], + }); + + await expect(clean(rawText)).rejects.toThrow(); + }); + + it('should handle undefined message content', async () => { + const rawText = 'Test'; + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [{ message: {} }], + }); + + await expect(clean(rawText)).rejects.toThrow(); + }); + + it('should handle malformed JSON in response', async () => { + const rawText = 'Test'; + mockGroqClient.chat.completions.create + .mockResolvedValueOnce({ + choices: [{ message: { content: '[{invalid json}]' } }], + }) + .mockResolvedValueOnce({ + choices: [{ message: { content: '[{"summary":"OK","refined_text":"- OK\\n"}]' } }], + }); + + const result = await clean(rawText); + expect(result).toBeDefined(); + }); + + it('should handle empty JSON array', async () => { + const rawText = 'Test'; + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [{ message: { content: '[]' } }], + }); + + const result = await clean(rawText); + expect(result).toEqual([]); + }); + + it('should handle nested arrays in response', async () => { + const rawText = 'Test'; + const nestedResponse = '[[{"summary":"Nested","refined_text":"- Nested\\n"}]]'; + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [{ message: { content: nestedResponse } }], + }); + + const result = await clean(rawText); + // Should extract the outer array + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('Input Validation', () => { + it('should process very long transcription text', async () => { + const longText = 'A'.repeat(10000); + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [{ message: { content: '[{"summary":"Long","refined_text":"- Long\\n"}]' } }], + }); + + const result = await clean(longText); + + expect(result).toBeDefined(); + expect(mockGroqClient.chat.completions.create).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ content: longText }), + ]), + }) + ); + }); + + it('should handle special characters in input', async () => { + const specialText = 'Test with "quotes" and \'apostrophes\' and \\backslashes\\'; + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [{ message: { content: '[{"summary":"Special","refined_text":"- Special\\n"}]' } }], + }); + + const result = await clean(specialText); + expect(result).toBeDefined(); + }); + + it('should handle unicode characters', async () => { + const unicodeText = 'Test with emoji πŸ˜€ and symbols βˆ‘βˆβˆ«'; + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [{ message: { content: '[{"summary":"Unicode","refined_text":"- Unicode\\n"}]' } }], + }); + + const result = await clean(unicodeText); + expect(result).toBeDefined(); + }); + + it('should handle newlines and tabs in input', async () => { + const formattedText = 'Line 1\nLine 2\tTabbed'; + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [{ message: { content: '[{"summary":"Formatted","refined_text":"- Formatted\\n"}]' } }], + }); + + const result = await clean(formattedText); + expect(result).toBeDefined(); + }); + }); + + describe('Error Scenarios', () => { + it('should handle network timeout errors', async () => { + const rawText = 'Test'; + const timeoutError = new Error('ETIMEDOUT'); + timeoutError.code = 'ETIMEDOUT'; + + mockGroqClient.chat.completions.create.mockRejectedValue(timeoutError); + + await expect(clean(rawText)).rejects.toThrow('ETIMEDOUT'); + expect(mockGroqClient.chat.completions.create).toHaveBeenCalledTimes(3); + }); + + it('should handle 429 rate limit errors', async () => { + const rawText = 'Test'; + const rateLimitError = new Error('Rate limit exceeded'); + rateLimitError.status = 429; + + mockGroqClient.chat.completions.create.mockRejectedValue(rateLimitError); + + await expect(clean(rawText)).rejects.toThrow(); + expect(mockGroqClient.chat.completions.create).toHaveBeenCalledTimes(3); + }); + + it('should handle 401 authentication errors', async () => { + const rawText = 'Test'; + const authError = new Error('Unauthorized'); + authError.status = 401; + + mockGroqClient.chat.completions.create.mockRejectedValue(authError); + + await expect(clean(rawText)).rejects.toThrow('Unauthorized'); + }); + + it('should handle unexpected response structure', async () => { + const rawText = 'Test'; + mockGroqClient.chat.completions.create.mockResolvedValue({ + unexpected: 'structure', + }); + + await expect(clean(rawText)).rejects.toThrow(); + }); + }); + + describe('Logging Behavior', () => { + let consoleSpy; + + beforeEach(() => { + consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + jest.spyOn(console, 'warn').mockImplementation(); + jest.spyOn(console, 'error').mockImplementation(); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + console.warn.mockRestore(); + console.error.mockRestore(); + }); + + it('should log attempt number on each retry', async () => { + const rawText = 'Test'; + mockGroqClient.chat.completions.create + .mockRejectedValueOnce(new Error('Fail')) + .mockResolvedValueOnce({ + choices: [{ message: { content: '[{"summary":"OK","refined_text":"- OK\\n"}]' } }], + }); + + await clean(rawText); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Attempt 1')); + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Attempt 2')); + }); + + it('should log successful parsing', async () => { + const rawText = 'Test'; + mockGroqClient.chat.completions.create.mockResolvedValue({ + choices: [{ message: { content: '[{"summary":"OK","refined_text":"- OK\\n"}]' } }], + }); + + await clean(rawText); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Parsed')); + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('successfully')); + }); + }); +}); \ No newline at end of file diff --git a/backend/tests/embeddingService.test.js b/backend/tests/embeddingService.test.js new file mode 100644 index 0000000..02dbf0e --- /dev/null +++ b/backend/tests/embeddingService.test.js @@ -0,0 +1,486 @@ +// tests/embeddingService.test.js + +const { getEmbedding } = require('../controllers/embedding/embeddingService'); + +// Mock dependencies +jest.mock('@google/genai'); +jest.mock('../../utils/llm/geminiService'); +jest.mock('../../utils/config', () => ({ + GEMINI_API_KEYS: ['test-key-1', 'test-key-2'], +})); + +const { GoogleGenAI } = require('@google/genai'); +const geminiService = require('../../utils/llm/geminiService'); + +describe('embeddingService - getEmbedding()', () => { + let mockAIInstance; + + beforeEach(() => { + jest.clearAllMocks(); + + // Setup default mock AI instance + mockAIInstance = { + models: { + embedContent: jest.fn(), + }, + }; + + GoogleGenAI.mockReturnValue(mockAIInstance); + geminiService.getNextKey = jest.fn().mockReturnValue('test-api-key'); + }); + + describe('Happy Path - Standard Response Shapes', () => { + it('should successfully generate embedding with embedding.values shape', async () => { + const mockVector = new Array(768).fill(0.1); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + const result = await getEmbedding('test text'); + + expect(result).toEqual(mockVector); + expect(result.length).toBe(768); + }); + + it('should handle embeddings array with values', async () => { + const mockVector = new Array(768).fill(0.2); + mockAIInstance.models.embedContent.mockResolvedValue({ + embeddings: [{ values: mockVector }], + }); + + const result = await getEmbedding('test text'); + + expect(result).toEqual(mockVector); + }); + + it('should handle data array response shape', async () => { + const mockVector = new Array(768).fill(0.3); + mockAIInstance.models.embedContent.mockResolvedValue({ + data: [{ embedding: mockVector }], + }); + + const result = await getEmbedding('test text'); + + expect(result).toEqual(mockVector); + }); + + it('should handle output.embeddings shape', async () => { + const mockVector = new Array(768).fill(0.4); + mockAIInstance.models.embedContent.mockResolvedValue({ + output: { + embeddings: [mockVector], + }, + }); + + const result = await getEmbedding('test text'); + + expect(result).toEqual(mockVector); + }); + + it('should handle nested embeddings with values in output', async () => { + const mockVector = new Array(768).fill(0.5); + mockAIInstance.models.embedContent.mockResolvedValue({ + output: { + embeddings: [{ values: mockVector }], + }, + }); + + const result = await getEmbedding('test text'); + + expect(result).toEqual(mockVector); + }); + }); + + describe('SDK Method Fallback Mechanism', () => { + it('should try multiple SDK methods until one succeeds', async () => { + const mockVector = new Array(768).fill(0.6); + + mockAIInstance.models.embedContent = undefined; + mockAIInstance.models.embed = jest.fn().mockResolvedValue({ + embedding: { values: mockVector }, + }); + + const result = await getEmbedding('test text'); + + expect(result).toEqual(mockVector); + }); + + it('should fall back to top-level embedContent method', async () => { + const mockVector = new Array(768).fill(0.7); + + mockAIInstance.models = {}; + mockAIInstance.embedContent = jest.fn().mockResolvedValue({ + embedding: { values: mockVector }, + }); + + const result = await getEmbedding('test text'); + + expect(result).toEqual(mockVector); + }); + + it('should throw error when no SDK method works', async () => { + mockAIInstance.models = {}; + mockAIInstance.embedContent = undefined; + + await expect(getEmbedding('test text')).rejects.toThrow( + 'No embedding method found' + ); + }); + + it('should propagate last error if all methods fail', async () => { + const testError = new Error('API Error'); + mockAIInstance.models.embedContent = jest.fn().mockRejectedValue(testError); + mockAIInstance.models.embed = jest.fn().mockRejectedValue(testError); + mockAIInstance.models.embed_content = jest.fn().mockRejectedValue(testError); + + await expect(getEmbedding('test text')).rejects.toThrow(); + }); + }); + + describe('Key Rotation Integration', () => { + it('should call geminiService.getNextKey() to rotate keys', async () => { + const mockVector = new Array(768).fill(0.8); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + await getEmbedding('test text'); + + expect(geminiService.getNextKey).toHaveBeenCalledTimes(1); + }); + + it('should create GoogleGenAI instance with rotated key', async () => { + geminiService.getNextKey.mockReturnValue('rotated-key-xyz'); + const mockVector = new Array(768).fill(0.9); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + await getEmbedding('test text'); + + expect(GoogleGenAI).toHaveBeenCalledWith({ apiKey: 'rotated-key-xyz' }); + }); + + it('should use different keys on consecutive calls', async () => { + geminiService.getNextKey + .mockReturnValueOnce('key-1') + .mockReturnValueOnce('key-2') + .mockReturnValueOnce('key-3'); + + const mockVector = new Array(768).fill(0.5); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + await getEmbedding('text 1'); + await getEmbedding('text 2'); + await getEmbedding('text 3'); + + expect(GoogleGenAI).toHaveBeenNthCalledWith(1, { apiKey: 'key-1' }); + expect(GoogleGenAI).toHaveBeenNthCalledWith(2, { apiKey: 'key-2' }); + expect(GoogleGenAI).toHaveBeenNthCalledWith(3, { apiKey: 'key-3' }); + }); + }); + + describe('Request Configuration', () => { + it('should use default model and dimensionality', async () => { + const mockVector = new Array(768).fill(1.0); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + await getEmbedding('test text'); + + expect(mockAIInstance.models.embedContent).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'gemini-embedding-001', + contents: [{ parts: [{ text: 'test text' }] }], + config: { outputDimensionality: 768 }, + }) + ); + }); + + it('should accept custom output dimensionality', async () => { + const mockVector = new Array(256).fill(1.0); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + await getEmbedding('test text', { outputDimensionality: 256 }); + + expect(mockAIInstance.models.embedContent).toHaveBeenCalledWith( + expect.objectContaining({ + config: { outputDimensionality: 256 }, + }) + ); + }); + + it('should accept custom model override', async () => { + const mockVector = new Array(768).fill(1.0); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + await getEmbedding('test text', { model: 'custom-embedding-model' }); + + expect(mockAIInstance.models.embedContent).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'custom-embedding-model', + }) + ); + }); + + it('should format text input correctly', async () => { + const testText = 'This is a test with multiple words.'; + const mockVector = new Array(768).fill(1.0); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + await getEmbedding(testText); + + expect(mockAIInstance.models.embedContent).toHaveBeenCalledWith( + expect.objectContaining({ + contents: [{ parts: [{ text: testText }] }], + }) + ); + }); + }); + + describe('Input Validation', () => { + it('should throw TypeError for non-string input', async () => { + await expect(getEmbedding(123)).rejects.toThrow(TypeError); + await expect(getEmbedding(123)).rejects.toThrow('non-empty string'); + }); + + it('should throw TypeError for null input', async () => { + await expect(getEmbedding(null)).rejects.toThrow(TypeError); + }); + + it('should throw TypeError for undefined input', async () => { + await expect(getEmbedding(undefined)).rejects.toThrow(TypeError); + }); + + it('should throw TypeError for empty string', async () => { + await expect(getEmbedding('')).rejects.toThrow(TypeError); + }); + + it('should throw TypeError for whitespace-only string', async () => { + await expect(getEmbedding(' ')).rejects.toThrow(TypeError); + await expect(getEmbedding('\n\t ')).rejects.toThrow(TypeError); + }); + + it('should throw TypeError for object input', async () => { + await expect(getEmbedding({ text: 'hello' })).rejects.toThrow(TypeError); + }); + + it('should throw TypeError for array input', async () => { + await expect(getEmbedding(['hello', 'world'])).rejects.toThrow(TypeError); + }); + }); + + describe('Edge Cases - Valid Inputs', () => { + it('should handle single character input', async () => { + const mockVector = new Array(768).fill(1.0); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + const result = await getEmbedding('a'); + + expect(result).toEqual(mockVector); + }); + + it('should handle very long text input', async () => { + const longText = 'A'.repeat(10000); + const mockVector = new Array(768).fill(1.0); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + const result = await getEmbedding(longText); + + expect(result).toBeDefined(); + }); + + it('should handle special characters', async () => { + const specialText = '!@#$%^&*()_+-=[]{}|;:\'",.<>?/~`'; + const mockVector = new Array(768).fill(1.0); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + const result = await getEmbedding(specialText); + + expect(result).toEqual(mockVector); + }); + + it('should handle unicode and emoji', async () => { + const unicodeText = 'Hello δΈ–η•Œ 🌍 βˆ‘βˆβˆ«'; + const mockVector = new Array(768).fill(1.0); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + const result = await getEmbedding(unicodeText); + + expect(result).toEqual(mockVector); + }); + + it('should handle newlines and formatting', async () => { + const formattedText = 'Line 1\nLine 2\n\tTabbed line\rCarriage return'; + const mockVector = new Array(768).fill(1.0); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + const result = await getEmbedding(formattedText); + + expect(result).toEqual(mockVector); + }); + }); + + describe('Error Response Handling', () => { + it('should throw error when response has no usable vector', async () => { + mockAIInstance.models.embedContent.mockResolvedValue({ + someOtherField: 'unexpected', + }); + + await expect(getEmbedding('test')).rejects.toThrow( + 'Embedding response did not contain a usable vector' + ); + }); + + it('should throw error when vector is empty array', async () => { + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: [] }, + }); + + await expect(getEmbedding('test')).rejects.toThrow( + 'Embedding response did not contain a usable vector' + ); + }); + + it('should throw error when vector is not an array', async () => { + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: 'not-an-array' }, + }); + + await expect(getEmbedding('test')).rejects.toThrow(); + }); + + it('should handle API errors gracefully', async () => { + const apiError = new Error('API rate limit exceeded'); + mockAIInstance.models.embedContent.mockRejectedValue(apiError); + + await expect(getEmbedding('test')).rejects.toThrow('API rate limit exceeded'); + }); + + it('should handle network timeouts', async () => { + const timeoutError = new Error('ETIMEDOUT'); + timeoutError.code = 'ETIMEDOUT'; + mockAIInstance.models.embedContent.mockRejectedValue(timeoutError); + + await expect(getEmbedding('test')).rejects.toThrow('ETIMEDOUT'); + }); + + it('should handle authentication errors', async () => { + const authError = new Error('401 Unauthorized'); + mockAIInstance.models.embedContent.mockRejectedValue(authError); + + await expect(getEmbedding('test')).rejects.toThrow('401 Unauthorized'); + }); + }); + + describe('Vector Quality', () => { + it('should return vector of correct dimensionality', async () => { + const mockVector = new Array(768).fill(0.1); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + const result = await getEmbedding('test'); + + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(768); + }); + + it('should return numeric values in vector', async () => { + const mockVector = [0.1, 0.2, 0.3, 0.4, 0.5]; + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + const result = await getEmbedding('test', { outputDimensionality: 5 }); + + result.forEach(value => { + expect(typeof value).toBe('number'); + expect(isNaN(value)).toBe(false); + }); + }); + + it('should handle vector with negative values', async () => { + const mockVector = [-0.5, 0.3, -0.1, 0.8, -0.2]; + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + const result = await getEmbedding('test', { outputDimensionality: 5 }); + + expect(result).toEqual(mockVector); + }); + + it('should handle vector with zero values', async () => { + const mockVector = new Array(768).fill(0); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + const result = await getEmbedding('test'); + + expect(result.every(v => v === 0)).toBe(true); + }); + }); + + describe('Concurrent Requests', () => { + it('should handle multiple concurrent embedding requests', async () => { + const mockVector = new Array(768).fill(1.0); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + const promises = [ + getEmbedding('text 1'), + getEmbedding('text 2'), + getEmbedding('text 3'), + ]; + + const results = await Promise.all(promises); + + expect(results.length).toBe(3); + results.forEach(result => { + expect(result).toEqual(mockVector); + }); + }); + + it('should rotate keys correctly across concurrent requests', async () => { + geminiService.getNextKey + .mockReturnValueOnce('key-1') + .mockReturnValueOnce('key-2') + .mockReturnValueOnce('key-3'); + + const mockVector = new Array(768).fill(1.0); + mockAIInstance.models.embedContent.mockResolvedValue({ + embedding: { values: mockVector }, + }); + + await Promise.all([ + getEmbedding('text 1'), + getEmbedding('text 2'), + getEmbedding('text 3'), + ]); + + expect(geminiService.getNextKey).toHaveBeenCalledTimes(3); + }); + }); +}); \ No newline at end of file diff --git a/backend/tests/geminiService.test.js b/backend/tests/geminiService.test.js new file mode 100644 index 0000000..71b5c1f --- /dev/null +++ b/backend/tests/geminiService.test.js @@ -0,0 +1,184 @@ +// tests/geminiService.test.js + +// Mock config before requiring geminiService +jest.mock('../utils/config', () => ({ + GEMINI_API_KEYS: ['gemini-key-1', 'gemini-key-2', 'gemini-key-3'], +})); + +// Mock BaseKeyRotationService +jest.mock('../utils/llm/baseKeyRotation'); + +describe('GeminiService', () => { + let geminiService; + let BaseKeyRotationService; + + beforeEach(() => { + jest.clearAllMocks(); + + // Clear module cache to get fresh instance + jest.resetModules(); + + // Re-require after clearing cache + BaseKeyRotationService = require('../utils/llm/baseKeyRotation'); + geminiService = require('../utils/llm/geminiService'); + }); + + describe('Initialization', () => { + it('should be a singleton instance', () => { + const instance1 = require('../utils/llm/geminiService'); + const instance2 = require('../utils/llm/geminiService'); + + expect(instance1).toBe(instance2); + }); + + it('should extend BaseKeyRotationService', () => { + expect(BaseKeyRotationService).toHaveBeenCalled(); + }); + + it('should initialize with GEMINI_API_KEYS from config', () => { + expect(BaseKeyRotationService).toHaveBeenCalledWith( + ['gemini-key-1', 'gemini-key-2', 'gemini-key-3'], + 'Gemini' + ); + }); + + it('should inherit getNextKey from BaseKeyRotationService', () => { + expect(typeof geminiService.getNextKey).toBe('function'); + }); + }); + + describe('Key Rotation Behavior', () => { + beforeEach(() => { + // Provide real implementation for testing rotation + let currentIndex = 0; + const keys = ['gemini-key-1', 'gemini-key-2', 'gemini-key-3']; + geminiService.getNextKey = jest.fn().mockImplementation(() => { + const key = keys[currentIndex % keys.length]; + currentIndex++; + return key; + }); + }); + + it('should rotate through keys in sequence', () => { + const key1 = geminiService.getNextKey(); + const key2 = geminiService.getNextKey(); + const key3 = geminiService.getNextKey(); + + expect(key1).toBe('gemini-key-1'); + expect(key2).toBe('gemini-key-2'); + expect(key3).toBe('gemini-key-3'); + }); + + it('should cycle back to first key after last key', () => { + geminiService.getNextKey(); // key-1 + geminiService.getNextKey(); // key-2 + geminiService.getNextKey(); // key-3 + const key4 = geminiService.getNextKey(); // should be key-1 again + + expect(key4).toBe('gemini-key-1'); + }); + + it('should maintain state across multiple calls', () => { + for (let i = 0; i < 10; i++) { + geminiService.getNextKey(); + } + + expect(geminiService.getNextKey).toHaveBeenCalledTimes(10); + }); + }); + + describe('Error Handling', () => { + it('should propagate error if getNextKey throws', () => { + geminiService.getNextKey = jest.fn().mockImplementation(() => { + throw new Error('No API keys configured for Gemini'); + }); + + expect(() => geminiService.getNextKey()).toThrow('No API keys configured for Gemini'); + }); + }); + + describe('Integration with Embedding Service', () => { + it('should provide keys compatible with GoogleGenAI client', () => { + geminiService.getNextKey = jest.fn().mockReturnValue('valid-gemini-api-key-xyz'); + + const key = geminiService.getNextKey(); + + expect(typeof key).toBe('string'); + expect(key.length).toBeGreaterThan(0); + }); + + it('should handle rapid successive key requests', () => { + let keyIndex = 0; + const keys = ['key-A', 'key-B', 'key-C']; + + geminiService.getNextKey = jest.fn().mockImplementation(() => { + const key = keys[keyIndex % keys.length]; + keyIndex++; + return key; + }); + + const requestedKeys = []; + for (let i = 0; i < 100; i++) { + requestedKeys.push(geminiService.getNextKey()); + } + + expect(requestedKeys.length).toBe(100); + expect(geminiService.getNextKey).toHaveBeenCalledTimes(100); + }); + }); + + describe('Edge Cases', () => { + it('should handle single key configuration', () => { + jest.resetModules(); + jest.mock('../utils/config', () => ({ + GEMINI_API_KEYS: ['single-key'], + })); + + const singleKeyService = require('../utils/llm/geminiService'); + singleKeyService.getNextKey = jest.fn().mockReturnValue('single-key'); + + const key1 = singleKeyService.getNextKey(); + const key2 = singleKeyService.getNextKey(); + + expect(key1).toBe('single-key'); + expect(key2).toBe('single-key'); + }); + + it('should handle empty key array gracefully through parent class', () => { + jest.resetModules(); + jest.mock('../utils/config', () => ({ + GEMINI_API_KEYS: [], + })); + + const emptyKeyService = require('../utils/llm/geminiService'); + emptyKeyService.getNextKey = jest.fn().mockImplementation(() => { + throw new Error('No API keys configured for Gemini'); + }); + + expect(() => emptyKeyService.getNextKey()).toThrow(); + }); + }); + + describe('Concurrent Access', () => { + it('should handle concurrent key requests correctly', async () => { + let keyIndex = 0; + const keys = ['key-1', 'key-2', 'key-3']; + + geminiService.getNextKey = jest.fn().mockImplementation(() => { + const key = keys[keyIndex % keys.length]; + keyIndex++; + return key; + }); + + const promises = []; + for (let i = 0; i < 10; i++) { + promises.push(Promise.resolve(geminiService.getNextKey())); + } + + const results = await Promise.all(promises); + + expect(results.length).toBe(10); + expect(geminiService.getNextKey).toHaveBeenCalledTimes(10); + }); + }); +}); \ No newline at end of file diff --git a/backend/tests/groqService.test.js b/backend/tests/groqService.test.js new file mode 100644 index 0000000..1c06a4e --- /dev/null +++ b/backend/tests/groqService.test.js @@ -0,0 +1,173 @@ +// tests/groqService.test.js + +const Groq = require('groq-sdk'); + +// Mock Groq SDK +jest.mock('groq-sdk'); + +// Mock config before requiring groqService +jest.mock('../utils/config', () => ({ + GROQ_API_KEYS: ['groq-key-1', 'groq-key-2', 'groq-key-3'], +})); + +// Mock BaseKeyRotationService +jest.mock('../utils/llm/baseKeyRotation'); + +describe('GroqService', () => { + let groqService; + let BaseKeyRotationService; + + beforeEach(() => { + jest.clearAllMocks(); + + // Clear module cache to get fresh instance + jest.resetModules(); + + // Re-require after clearing cache + BaseKeyRotationService = require('../utils/llm/baseKeyRotation'); + groqService = require('../utils/llm/groqService'); + }); + + describe('Initialization', () => { + it('should be a singleton instance', () => { + const instance1 = require('../utils/llm/groqService'); + const instance2 = require('../utils/llm/groqService'); + + expect(instance1).toBe(instance2); + }); + + it('should extend BaseKeyRotationService', () => { + expect(BaseKeyRotationService).toHaveBeenCalled(); + }); + + it('should initialize with GROQ_API_KEYS from config', () => { + expect(BaseKeyRotationService).toHaveBeenCalledWith( + ['groq-key-1', 'groq-key-2', 'groq-key-3'], + 'Groq' + ); + }); + }); + + describe('getClient()', () => { + beforeEach(() => { + // Mock the getNextKey method on the instance + groqService.getNextKey = jest.fn(); + }); + + it('should call getNextKey to retrieve an API key', () => { + groqService.getNextKey.mockReturnValue('groq-key-1'); + + groqService.getClient(); + + expect(groqService.getNextKey).toHaveBeenCalledTimes(1); + }); + + it('should return a new Groq client instance', () => { + groqService.getNextKey.mockReturnValue('groq-key-1'); + const mockGroqInstance = { chat: {} }; + Groq.mockReturnValue(mockGroqInstance); + + const client = groqService.getClient(); + + expect(Groq).toHaveBeenCalledWith({ apiKey: 'groq-key-1' }); + expect(client).toBe(mockGroqInstance); + }); + + it('should create new client with rotated key on subsequent calls', () => { + groqService.getNextKey + .mockReturnValueOnce('groq-key-1') + .mockReturnValueOnce('groq-key-2') + .mockReturnValueOnce('groq-key-3'); + + groqService.getClient(); + groqService.getClient(); + groqService.getClient(); + + expect(Groq).toHaveBeenNthCalledWith(1, { apiKey: 'groq-key-1' }); + expect(Groq).toHaveBeenNthCalledWith(2, { apiKey: 'groq-key-2' }); + expect(Groq).toHaveBeenNthCalledWith(3, { apiKey: 'groq-key-3' }); + }); + + it('should create independent client instances', () => { + groqService.getNextKey.mockReturnValue('groq-key-1'); + const mockInstance1 = { id: 1 }; + const mockInstance2 = { id: 2 }; + Groq.mockReturnValueOnce(mockInstance1).mockReturnValueOnce(mockInstance2); + + const client1 = groqService.getClient(); + const client2 = groqService.getClient(); + + expect(client1).not.toBe(client2); + expect(client1).toBe(mockInstance1); + expect(client2).toBe(mockInstance2); + }); + }); + + describe('Error Handling', () => { + it('should propagate error if getNextKey throws', () => { + groqService.getNextKey = jest.fn().mockImplementation(() => { + throw new Error('No API keys configured for Groq'); + }); + + expect(() => groqService.getClient()).toThrow('No API keys configured for Groq'); + }); + + it('should propagate error if Groq constructor throws', () => { + groqService.getNextKey.mockReturnValue('invalid-key'); + Groq.mockImplementation(() => { + throw new Error('Invalid API key format'); + }); + + expect(() => groqService.getClient()).toThrow('Invalid API key format'); + }); + }); + + describe('Integration with Key Rotation', () => { + it('should maintain rotation state across multiple getClient calls', () => { + let keyIndex = 0; + const keys = ['key-A', 'key-B', 'key-C']; + + groqService.getNextKey = jest.fn().mockImplementation(() => { + const key = keys[keyIndex % keys.length]; + keyIndex++; + return key; + }); + + groqService.getClient(); + groqService.getClient(); + groqService.getClient(); + groqService.getClient(); + + expect(Groq).toHaveBeenNthCalledWith(1, { apiKey: 'key-A' }); + expect(Groq).toHaveBeenNthCalledWith(2, { apiKey: 'key-B' }); + expect(Groq).toHaveBeenNthCalledWith(3, { apiKey: 'key-C' }); + expect(Groq).toHaveBeenNthCalledWith(4, { apiKey: 'key-A' }); + }); + }); + + describe('Edge Cases', () => { + it('should handle empty string API key', () => { + groqService.getNextKey.mockReturnValue(''); + + groqService.getClient(); + + expect(Groq).toHaveBeenCalledWith({ apiKey: '' }); + }); + + it('should handle null API key from getNextKey', () => { + groqService.getNextKey.mockReturnValue(null); + + groqService.getClient(); + + expect(Groq).toHaveBeenCalledWith({ apiKey: null }); + }); + + it('should handle undefined API key from getNextKey', () => { + groqService.getNextKey.mockReturnValue(undefined); + + groqService.getClient(); + + expect(Groq).toHaveBeenCalledWith({ apiKey: undefined }); + }); + }); +}); \ No newline at end of file diff --git a/backend/tests/queryVectordb.test.js b/backend/tests/queryVectordb.test.js new file mode 100644 index 0000000..f2020b3 --- /dev/null +++ b/backend/tests/queryVectordb.test.js @@ -0,0 +1,456 @@ +// tests/queryVectordb.test.js + +const { queryTranscriptions, queryChats } = require('../controllers/queryVectordb'); + +// Mock dependencies +jest.mock('@qdrant/js-client-rest'); +jest.mock('../controllers/embedding/embeddingService'); +jest.mock('../utils/config', () => ({ + QDRANT_URL: 'http://mock-qdrant:6333', + QDRANT_API_KEY: 'mock-api-key', + TRANSCRIPTION_COLLECTION: 'test_transcriptions', + CHAT_COLLECTION: 'test_chats', +})); + +const { QdrantClient } = require('@qdrant/js-client-rest'); +const { getEmbedding } = require('../controllers/embedding/embeddingService'); + +describe('queryVectordb Module', () => { + let mockClient; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock QdrantClient instance + mockClient = { + search: jest.fn(), + }; + QdrantClient.mockReturnValue(mockClient); + + // Mock embedding generation + getEmbedding.mockResolvedValue(new Array(768).fill(0.1)); + }); + + describe('queryTranscriptions()', () => { + describe('Happy Path', () => { + it('should successfully query and return transcription chunks', async () => { + const mockResults = [ + { + payload: { + jobId: 'job-123', + text: 'Transcription chunk 1', + refined_text: '- Discussion about AI\n', + }, + score: 0.95, + }, + { + payload: { + jobId: 'job-123', + text: 'Transcription chunk 2', + refined_text: '- More discussion\n', + }, + score: 0.89, + }, + ]; + + mockClient.search.mockResolvedValue(mockResults); + + const result = await queryTranscriptions('AI discussion', 'job-123', 5); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual(mockResults[0].payload); + expect(result[1]).toEqual(mockResults[1].payload); + }); + + it('should call getEmbedding with user prompt', async () => { + mockClient.search.mockResolvedValue([]); + + await queryTranscriptions('test prompt', 'job-123'); + + expect(getEmbedding).toHaveBeenCalledWith('test prompt'); + }); + + it('should call Qdrant search with correct parameters', async () => { + const mockVector = new Array(768).fill(0.5); + getEmbedding.mockResolvedValue(mockVector); + mockClient.search.mockResolvedValue([]); + + await queryTranscriptions('test query', 'job-456', 10); + + expect(mockClient.search).toHaveBeenCalledWith( + 'test_transcriptions', + expect.objectContaining({ + vector: mockVector, + filter: { + must: [ + { + key: 'jobId', + match: { value: 'job-456' }, + }, + ], + }, + limit: 10, + with_payload: true, + with_vectors: false, + }) + ); + }); + + it('should use default limit of 5 when not specified', async () => { + mockClient.search.mockResolvedValue([]); + + await queryTranscriptions('test', 'job-123'); + + expect(mockClient.search).toHaveBeenCalledWith( + 'test_transcriptions', + expect.objectContaining({ + limit: 5, + }) + ); + }); + + it('should return empty array when no results found', async () => { + mockClient.search.mockResolvedValue([]); + + const result = await queryTranscriptions('test', 'job-123'); + + expect(result).toEqual([]); + }); + }); + + describe('Error Handling', () => { + it('should return empty array when embedding generation fails', async () => { + getEmbedding.mockResolvedValue(null); + + const result = await queryTranscriptions('test', 'job-123'); + + expect(result).toEqual([]); + expect(mockClient.search).not.toHaveBeenCalled(); + }); + + it('should return empty array when embedding is empty array', async () => { + getEmbedding.mockResolvedValue([]); + + const result = await queryTranscriptions('test', 'job-123'); + + expect(result).toEqual([]); + }); + + it('should throw error when Qdrant search fails', async () => { + const qdrantError = new Error('Qdrant connection failed'); + mockClient.search.mockRejectedValue(qdrantError); + + await expect(queryTranscriptions('test', 'job-123')).rejects.toThrow( + 'Qdrant connection failed' + ); + }); + + it('should throw error on Qdrant timeout', async () => { + const timeoutError = new Error('ETIMEDOUT'); + timeoutError.code = 'ETIMEDOUT'; + mockClient.search.mockRejectedValue(timeoutError); + + await expect(queryTranscriptions('test', 'job-123')).rejects.toThrow('ETIMEDOUT'); + }); + + it('should handle authentication errors', async () => { + const authError = new Error('401 Unauthorized'); + mockClient.search.mockRejectedValue(authError); + + await expect(queryTranscriptions('test', 'job-123')).rejects.toThrow('401 Unauthorized'); + }); + }); + + describe('Edge Cases', () => { + it('should handle very long user prompts', async () => { + const longPrompt = 'A'.repeat(5000); + mockClient.search.mockResolvedValue([]); + + await queryTranscriptions(longPrompt, 'job-123'); + + expect(getEmbedding).toHaveBeenCalledWith(longPrompt); + }); + + it('should handle special characters in jobId', async () => { + mockClient.search.mockResolvedValue([]); + + await queryTranscriptions('test', 'job-with-special-chars-!@#'); + + expect(mockClient.search).toHaveBeenCalledWith( + 'test_transcriptions', + expect.objectContaining({ + filter: { + must: [ + { + key: 'jobId', + match: { value: 'job-with-special-chars-!@#' }, + }, + ], + }, + }) + ); + }); + + it('should handle large limit values', async () => { + mockClient.search.mockResolvedValue([]); + + await queryTranscriptions('test', 'job-123', 1000); + + expect(mockClient.search).toHaveBeenCalledWith( + 'test_transcriptions', + expect.objectContaining({ limit: 1000 }) + ); + }); + + it('should handle limit of 1', async () => { + const mockResult = [{ payload: { text: 'Single result' } }]; + mockClient.search.mockResolvedValue(mockResult); + + const result = await queryTranscriptions('test', 'job-123', 1); + + expect(result).toHaveLength(1); + }); + }); + }); + + describe('queryChats()', () => { + describe('Happy Path', () => { + it('should successfully query and return chat pairs', async () => { + const mockResults = [ + { + payload: { + jobId: 'job-123', + userChat: 'What is AI?', + aiChat: 'AI stands for Artificial Intelligence...', + }, + score: 0.92, + }, + { + payload: { + jobId: 'job-123', + userChat: 'Tell me more', + aiChat: 'Here are more details...', + }, + score: 0.85, + }, + ]; + + mockClient.search.mockResolvedValue(mockResults); + + const result = await queryChats('AI questions', 'job-123', 3); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual(mockResults[0].payload); + expect(result[1]).toEqual(mockResults[1].payload); + }); + + it('should call getEmbedding with user prompt', async () => { + mockClient.search.mockResolvedValue([]); + + await queryChats('test prompt', 'job-123'); + + expect(getEmbedding).toHaveBeenCalledWith('test prompt'); + }); + + it('should call Qdrant search with correct collection name', async () => { + mockClient.search.mockResolvedValue([]); + + await queryChats('test', 'job-123'); + + expect(mockClient.search).toHaveBeenCalledWith( + 'test_chats', + expect.any(Object) + ); + }); + + it('should use default limit of 3 when not specified', async () => { + mockClient.search.mockResolvedValue([]); + + await queryChats('test', 'job-123'); + + expect(mockClient.search).toHaveBeenCalledWith( + 'test_chats', + expect.objectContaining({ limit: 3 }) + ); + }); + + it('should filter by jobId correctly', async () => { + mockClient.search.mockResolvedValue([]); + + await queryChats('test', 'specific-job-id', 5); + + expect(mockClient.search).toHaveBeenCalledWith( + 'test_chats', + expect.objectContaining({ + filter: { + must: [ + { + key: 'jobId', + match: { value: 'specific-job-id' }, + }, + ], + }, + }) + ); + }); + + it('should return empty array when no chat history found', async () => { + mockClient.search.mockResolvedValue([]); + + const result = await queryChats('test', 'job-123'); + + expect(result).toEqual([]); + }); + }); + + describe('Error Handling', () => { + it('should return empty array when embedding generation fails', async () => { + getEmbedding.mockResolvedValue(null); + + const result = await queryChats('test', 'job-123'); + + expect(result).toEqual([]); + expect(mockClient.search).not.toHaveBeenCalled(); + }); + + it('should return empty array when embedding is empty', async () => { + getEmbedding.mockResolvedValue([]); + + const result = await queryChats('test', 'job-123'); + + expect(result).toEqual([]); + }); + + it('should throw error when Qdrant search fails', async () => { + const error = new Error('Collection not found'); + mockClient.search.mockRejectedValue(error); + + await expect(queryChats('test', 'job-123')).rejects.toThrow('Collection not found'); + }); + + it('should handle network errors', async () => { + const networkError = new Error('ECONNREFUSED'); + networkError.code = 'ECONNREFUSED'; + mockClient.search.mockRejectedValue(networkError); + + await expect(queryChats('test', 'job-123')).rejects.toThrow('ECONNREFUSED'); + }); + }); + + describe('Edge Cases', () => { + it('should handle empty jobId', async () => { + mockClient.search.mockResolvedValue([]); + + await queryChats('test', '', 3); + + expect(mockClient.search).toHaveBeenCalledWith( + 'test_chats', + expect.objectContaining({ + filter: { + must: [{ key: 'jobId', match: { value: '' } }], + }, + }) + ); + }); + + it('should handle unicode in user prompt', async () => { + const unicodePrompt = 'Test with Γ©mojis πŸ˜€ and δΈ­ζ–‡'; + mockClient.search.mockResolvedValue([]); + + await queryChats(unicodePrompt, 'job-123'); + + expect(getEmbedding).toHaveBeenCalledWith(unicodePrompt); + }); + + it('should handle single result', async () => { + const singleResult = [ + { + payload: { + userChat: 'Question', + aiChat: 'Answer', + jobId: 'job-123', + }, + }, + ]; + mockClient.search.mockResolvedValue(singleResult); + + const result = await queryChats('test', 'job-123'); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual(singleResult[0].payload); + }); + + it('should handle custom high limit', async () => { + mockClient.search.mockResolvedValue([]); + + await queryChats('test', 'job-123', 50); + + expect(mockClient.search).toHaveBeenCalledWith( + 'test_chats', + expect.objectContaining({ limit: 50 }) + ); + }); + }); + }); + + describe('Module Integration', () => { + it('should initialize QdrantClient with correct configuration', () => { + expect(QdrantClient).toHaveBeenCalledWith({ + url: 'http://mock-qdrant:6333', + apiKey: 'mock-api-key', + timeout: 60000, + }); + }); + + it('should use same client instance for both functions', async () => { + mockClient.search.mockResolvedValue([]); + + await queryTranscriptions('test1', 'job-1'); + await queryChats('test2', 'job-2'); + + expect(mockClient.search).toHaveBeenCalledTimes(2); + }); + + it('should handle concurrent queries to both collections', async () => { + mockClient.search.mockResolvedValue([]); + + await Promise.all([ + queryTranscriptions('test1', 'job-1'), + queryChats('test2', 'job-2'), + ]); + + expect(mockClient.search).toHaveBeenCalledTimes(2); + }); + }); + + describe('Performance', () => { + it('should request payload but not vectors for efficiency', async () => { + mockClient.search.mockResolvedValue([]); + + await queryTranscriptions('test', 'job-123'); + + expect(mockClient.search).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + with_payload: true, + with_vectors: false, + }) + ); + }); + + it('should limit query results appropriately', async () => { + const manyResults = Array.from({ length: 100 }, (_, i) => ({ + payload: { text: `Result ${i}` }, + })); + mockClient.search.mockResolvedValue(manyResults); + + const result = await queryTranscriptions('test', 'job-123', 5); + + // Client should respect limit parameter + expect(mockClient.search).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ limit: 5 }) + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/tests/tempAuthCheck.test.js b/backend/tests/tempAuthCheck.test.js new file mode 100644 index 0000000..b1caa7f --- /dev/null +++ b/backend/tests/tempAuthCheck.test.js @@ -0,0 +1,250 @@ +// tests/tempAuthCheck.test.js + +const tempAuthCheck = require('../middlewares/tempAuthCheck'); + +// Mock config +jest.mock('../utils/config', () => ({ + ALLOWED_AUTH_CODES: ['valid-code-1', 'valid-code-2', 'test-secret-xyz'], +})); + +describe('tempAuthCheck Middleware', () => { + let mockReq; + let mockRes; + let nextFunction; + + beforeEach(() => { + // Setup mock request, response, and next function + mockReq = { + headers: {}, + }; + mockRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + nextFunction = jest.fn(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Happy Path - Valid Authentication', () => { + it('should call next() when valid auth code is provided in headers', () => { + mockReq.headers['x-auth-code'] = 'valid-code-1'; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(nextFunction).toHaveBeenCalledTimes(1); + expect(mockRes.status).not.toHaveBeenCalled(); + expect(mockRes.json).not.toHaveBeenCalled(); + }); + + it('should call next() for second valid auth code', () => { + mockReq.headers['x-auth-code'] = 'valid-code-2'; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(nextFunction).toHaveBeenCalledTimes(1); + expect(mockRes.status).not.toHaveBeenCalled(); + }); + + it('should call next() for third valid auth code', () => { + mockReq.headers['x-auth-code'] = 'test-secret-xyz'; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(nextFunction).toHaveBeenCalledTimes(1); + expect(mockRes.status).not.toHaveBeenCalled(); + }); + }); + + describe('Error Cases - Missing Authentication', () => { + it('should return 401 when x-auth-code header is missing', () => { + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Unauthorized: No authentication code provided.', + }); + expect(nextFunction).not.toHaveBeenCalled(); + }); + + it('should return 401 when x-auth-code header is undefined', () => { + mockReq.headers['x-auth-code'] = undefined; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Unauthorized: No authentication code provided.', + }); + expect(nextFunction).not.toHaveBeenCalled(); + }); + + it('should return 401 when x-auth-code header is null', () => { + mockReq.headers['x-auth-code'] = null; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(nextFunction).not.toHaveBeenCalled(); + }); + + it('should return 401 when x-auth-code header is empty string', () => { + mockReq.headers['x-auth-code'] = ''; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(nextFunction).not.toHaveBeenCalled(); + }); + }); + + describe('Error Cases - Invalid Authentication', () => { + it('should return 401 when auth code does not match allowed codes', () => { + mockReq.headers['x-auth-code'] = 'invalid-code'; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Unauthorized: Invalid authentication code.', + }); + expect(nextFunction).not.toHaveBeenCalled(); + }); + + it('should return 401 for similar but incorrect auth code', () => { + mockReq.headers['x-auth-code'] = 'valid-code-1-extra'; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(nextFunction).not.toHaveBeenCalled(); + }); + + it('should return 401 for auth code with wrong case', () => { + mockReq.headers['x-auth-code'] = 'VALID-CODE-1'; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(nextFunction).not.toHaveBeenCalled(); + }); + + it('should return 401 for auth code with extra whitespace', () => { + mockReq.headers['x-auth-code'] = ' valid-code-1 '; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(nextFunction).not.toHaveBeenCalled(); + }); + + it('should return 401 for numeric auth code', () => { + mockReq.headers['x-auth-code'] = '12345'; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(nextFunction).not.toHaveBeenCalled(); + }); + + it('should return 401 for SQL injection attempt', () => { + mockReq.headers['x-auth-code'] = "' OR '1'='1"; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(nextFunction).not.toHaveBeenCalled(); + }); + + it('should return 401 for special character auth code', () => { + mockReq.headers['x-auth-code'] = '@#$%^&*()'; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(nextFunction).not.toHaveBeenCalled(); + }); + }); + + describe('Edge Cases', () => { + it('should handle boolean false as invalid auth code', () => { + mockReq.headers['x-auth-code'] = false; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(nextFunction).not.toHaveBeenCalled(); + }); + + it('should handle number 0 as invalid auth code', () => { + mockReq.headers['x-auth-code'] = 0; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(nextFunction).not.toHaveBeenCalled(); + }); + + it('should handle object as invalid auth code', () => { + mockReq.headers['x-auth-code'] = { code: 'valid-code-1' }; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(nextFunction).not.toHaveBeenCalled(); + }); + + it('should handle array as invalid auth code', () => { + mockReq.headers['x-auth-code'] = ['valid-code-1']; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(nextFunction).not.toHaveBeenCalled(); + }); + + it('should be case-sensitive for auth codes', () => { + mockReq.headers['x-auth-code'] = 'Valid-Code-1'; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(nextFunction).not.toHaveBeenCalled(); + }); + }); + + describe('Response Structure', () => { + it('should return proper JSON error structure for missing code', () => { + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + error: expect.any(String), + }) + ); + }); + + it('should return proper JSON error structure for invalid code', () => { + mockReq.headers['x-auth-code'] = 'wrong-code'; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + error: expect.any(String), + }) + ); + }); + + it('should chain status and json calls correctly', () => { + mockReq.headers['x-auth-code'] = 'invalid'; + + tempAuthCheck(mockReq, mockRes, nextFunction); + + expect(mockRes.status).toHaveReturnedWith(mockRes); + expect(mockRes.json).toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file