Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions web/service/base.spec.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
})
11 changes: 11 additions & 0 deletions web/service/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading