diff --git a/web/service/base.spec.ts b/web/service/base.spec.ts new file mode 100644 index 0000000000..d6ed242ed9 --- /dev/null +++ b/web/service/base.spec.ts @@ -0,0 +1,231 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { handleStream } from './base' + +describe('handleStream', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('Invalid response data handling', () => { + it('should handle null bufferObj from JSON.parse gracefully', async () => { + // Arrange + const onData = vi.fn() + const onCompleted = vi.fn() + + // Create a mock response that returns 'data: null' + const mockReader = { + read: vi.fn() + .mockResolvedValueOnce({ + done: false, + value: new TextEncoder().encode('data: null\n'), + }) + .mockResolvedValueOnce({ + done: true, + value: undefined, + }), + } + + const mockResponse = { + ok: true, + body: { + getReader: () => mockReader, + }, + } as unknown as Response + + // Act + handleStream(mockResponse, onData, onCompleted) + + // Wait for the stream to be processed + await new Promise(resolve => setTimeout(resolve, 50)) + + // Assert + expect(onData).toHaveBeenCalledWith('', true, { + conversationId: undefined, + messageId: '', + errorMessage: 'Invalid response data', + errorCode: 'invalid_data', + }) + expect(onCompleted).toHaveBeenCalledWith(true, 'Invalid response data') + }) + + it('should handle non-object bufferObj from JSON.parse gracefully', async () => { + // Arrange + const onData = vi.fn() + const onCompleted = vi.fn() + + // Create a mock response that returns a primitive value + const mockReader = { + read: vi.fn() + .mockResolvedValueOnce({ + done: false, + value: new TextEncoder().encode('data: "string"\n'), + }) + .mockResolvedValueOnce({ + done: true, + value: undefined, + }), + } + + const mockResponse = { + ok: true, + body: { + getReader: () => mockReader, + }, + } as unknown as Response + + // Act + handleStream(mockResponse, onData, onCompleted) + + // Wait for the stream to be processed + await new Promise(resolve => setTimeout(resolve, 50)) + + // Assert + expect(onData).toHaveBeenCalledWith('', true, { + conversationId: undefined, + messageId: '', + errorMessage: 'Invalid response data', + errorCode: 'invalid_data', + }) + expect(onCompleted).toHaveBeenCalledWith(true, 'Invalid response data') + }) + + it('should handle valid message event correctly', async () => { + // Arrange + const onData = vi.fn() + const onCompleted = vi.fn() + + const validMessage = { + event: 'message', + answer: 'Hello world', + conversation_id: 'conv-123', + task_id: 'task-456', + id: 'msg-789', + } + + const mockReader = { + read: vi.fn() + .mockResolvedValueOnce({ + done: false, + value: new TextEncoder().encode(`data: ${JSON.stringify(validMessage)}\n`), + }) + .mockResolvedValueOnce({ + done: true, + value: undefined, + }), + } + + const mockResponse = { + ok: true, + body: { + getReader: () => mockReader, + }, + } as unknown as Response + + // Act + handleStream(mockResponse, onData, onCompleted) + + // Wait for the stream to be processed + await new Promise(resolve => setTimeout(resolve, 50)) + + // Assert + expect(onData).toHaveBeenCalledWith('Hello world', true, { + conversationId: 'conv-123', + taskId: 'task-456', + messageId: 'msg-789', + }) + expect(onCompleted).toHaveBeenCalled() + }) + + it('should handle error status 400 correctly', async () => { + // Arrange + const onData = vi.fn() + const onCompleted = vi.fn() + + const errorMessage = { + status: 400, + message: 'Bad request', + code: 'bad_request', + } + + const mockReader = { + read: vi.fn() + .mockResolvedValueOnce({ + done: false, + value: new TextEncoder().encode(`data: ${JSON.stringify(errorMessage)}\n`), + }) + .mockResolvedValueOnce({ + done: true, + value: undefined, + }), + } + + const mockResponse = { + ok: true, + body: { + getReader: () => mockReader, + }, + } as unknown as Response + + // Act + handleStream(mockResponse, onData, onCompleted) + + // Wait for the stream to be processed + await new Promise(resolve => setTimeout(resolve, 50)) + + // Assert + expect(onData).toHaveBeenCalledWith('', false, { + conversationId: undefined, + messageId: '', + errorMessage: 'Bad request', + errorCode: 'bad_request', + }) + expect(onCompleted).toHaveBeenCalledWith(true, 'Bad request') + }) + + it('should handle malformed JSON gracefully', async () => { + // Arrange + const onData = vi.fn() + const onCompleted = vi.fn() + + const mockReader = { + read: vi.fn() + .mockResolvedValueOnce({ + done: false, + value: new TextEncoder().encode('data: {invalid json}\n'), + }) + .mockResolvedValueOnce({ + done: true, + value: undefined, + }), + } + + const mockResponse = { + ok: true, + body: { + getReader: () => mockReader, + }, + } as unknown as Response + + // Act + handleStream(mockResponse, onData, onCompleted) + + // Wait for the stream to be processed + await new Promise(resolve => setTimeout(resolve, 50)) + + // Assert - malformed JSON triggers the catch block which calls onData and returns + expect(onData).toHaveBeenCalled() + expect(onCompleted).toHaveBeenCalled() + }) + + it('should throw error when response is not ok', () => { + // Arrange + const onData = vi.fn() + const mockResponse = { + ok: false, + } as unknown as Response + + // Act & Assert + expect(() => handleStream(mockResponse, onData)).toThrow('Network response was not ok') + }) + }) +}) diff --git a/web/service/base.ts b/web/service/base.ts index d9f3dba53a..2ab115f96c 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -217,6 +217,17 @@ export const handleStream = ( }) return } + if (!bufferObj || typeof bufferObj !== 'object') { + onData('', isFirstMessage, { + conversationId: undefined, + messageId: '', + errorMessage: 'Invalid response data', + errorCode: 'invalid_data', + }) + hasError = true + onCompleted?.(true, 'Invalid response data') + return + } if (bufferObj.status === 400 || !bufferObj.event) { onData('', false, { conversationId: undefined,