Skip to content

Commit 5d4ddbc

Browse files
authored
Improve(chat): added multiple output option for chat panel & chat deploy (#310)
* improvement(chat): added multiple output selection for chat panel & chat deploy * added tests, changed subdomain-check to subdomains/validate to be more RESTful * added more tests * added even more tests * remove unused route * acknowledged PR comments, updated UI for output selector
1 parent 0dcd5ee commit 5d4ddbc

File tree

20 files changed

+3414
-250
lines changed

20 files changed

+3414
-250
lines changed
Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
/**
2+
* Tests for chat subdomain API route
3+
*
4+
* @vitest-environment node
5+
*/
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7+
import { createMockRequest } from '@/app/api/__test-utils__/utils'
8+
9+
describe('Chat Subdomain API Route', () => {
10+
const mockWorkflowSingleOutput = {
11+
id: 'response-id',
12+
content: 'Test response',
13+
timestamp: new Date().toISOString(),
14+
type: 'workflow'
15+
}
16+
17+
// Mock functions
18+
const mockAddCorsHeaders = vi.fn().mockImplementation((response) => response)
19+
const mockValidateChatAuth = vi.fn().mockResolvedValue({ authorized: true })
20+
const mockSetChatAuthCookie = vi.fn()
21+
const mockExecuteWorkflowForChat = vi.fn().mockResolvedValue(mockWorkflowSingleOutput)
22+
23+
// Mock database return values
24+
const mockChatResult = [
25+
{
26+
id: 'chat-id',
27+
workflowId: 'workflow-id',
28+
userId: 'user-id',
29+
isActive: true,
30+
authType: 'public',
31+
title: 'Test Chat',
32+
description: 'Test chat description',
33+
customizations: {
34+
welcomeMessage: 'Welcome to the test chat',
35+
primaryColor: '#000000'
36+
},
37+
outputConfigs: [
38+
{ blockId: 'block-1', path: 'output' }
39+
]
40+
}
41+
]
42+
43+
const mockWorkflowResult = [
44+
{
45+
isDeployed: true
46+
}
47+
]
48+
49+
beforeEach(() => {
50+
vi.resetModules()
51+
52+
// Mock chat API utils
53+
vi.doMock('../utils', () => ({
54+
addCorsHeaders: mockAddCorsHeaders,
55+
validateChatAuth: mockValidateChatAuth,
56+
setChatAuthCookie: mockSetChatAuthCookie,
57+
validateAuthToken: vi.fn().mockReturnValue(true),
58+
executeWorkflowForChat: mockExecuteWorkflowForChat,
59+
}))
60+
61+
// Mock logger
62+
vi.doMock('@/lib/logs/console-logger', () => ({
63+
createLogger: vi.fn().mockReturnValue({
64+
debug: vi.fn(),
65+
info: vi.fn(),
66+
warn: vi.fn(),
67+
error: vi.fn(),
68+
}),
69+
}))
70+
71+
// Mock database
72+
vi.doMock('@/db', () => {
73+
const mockLimitChat = vi.fn().mockReturnValue(mockChatResult)
74+
const mockWhereChat = vi.fn().mockReturnValue({ limit: mockLimitChat })
75+
76+
const mockLimitWorkflow = vi.fn().mockReturnValue(mockWorkflowResult)
77+
const mockWhereWorkflow = vi.fn().mockReturnValue({ limit: mockLimitWorkflow })
78+
79+
const mockFrom = vi.fn()
80+
.mockImplementation((table) => {
81+
// Check which table is being queried
82+
if (table === 'workflow') {
83+
return { where: mockWhereWorkflow }
84+
}
85+
return { where: mockWhereChat }
86+
})
87+
88+
const mockSelect = vi.fn().mockReturnValue({ from: mockFrom })
89+
90+
return {
91+
db: {
92+
select: mockSelect
93+
}
94+
}
95+
})
96+
97+
// Mock API response helpers
98+
vi.doMock('@/app/api/workflows/utils', () => ({
99+
createErrorResponse: vi.fn().mockImplementation((message, status = 400, code) => {
100+
return new Response(
101+
JSON.stringify({
102+
error: code || 'Error',
103+
message
104+
}),
105+
{ status }
106+
)
107+
}),
108+
createSuccessResponse: vi.fn().mockImplementation((data) => {
109+
return new Response(
110+
JSON.stringify(data),
111+
{ status: 200 }
112+
)
113+
})
114+
}))
115+
})
116+
117+
afterEach(() => {
118+
vi.clearAllMocks()
119+
})
120+
121+
describe('GET endpoint', () => {
122+
it('should return chat info for a valid subdomain', async () => {
123+
const req = createMockRequest('GET')
124+
const params = Promise.resolve({ subdomain: 'test-chat' })
125+
126+
const { GET } = await import('./route')
127+
128+
const response = await GET(req, { params })
129+
130+
expect(response.status).toBe(200)
131+
132+
const data = await response.json()
133+
expect(data).toHaveProperty('id', 'chat-id')
134+
expect(data).toHaveProperty('title', 'Test Chat')
135+
expect(data).toHaveProperty('description', 'Test chat description')
136+
expect(data).toHaveProperty('customizations')
137+
expect(data.customizations).toHaveProperty('welcomeMessage', 'Welcome to the test chat')
138+
})
139+
140+
it('should return 404 for non-existent subdomain', async () => {
141+
vi.doMock('@/db', () => {
142+
const mockLimit = vi.fn().mockReturnValue([])
143+
const mockWhere = vi.fn().mockReturnValue({ limit: mockLimit })
144+
const mockFrom = vi.fn().mockReturnValue({ where: mockWhere })
145+
const mockSelect = vi.fn().mockReturnValue({ from: mockFrom })
146+
147+
return {
148+
db: {
149+
select: mockSelect
150+
}
151+
}
152+
})
153+
154+
const req = createMockRequest('GET')
155+
const params = Promise.resolve({ subdomain: 'nonexistent' })
156+
157+
const { GET } = await import('./route')
158+
159+
const response = await GET(req, { params })
160+
161+
expect(response.status).toBe(404)
162+
163+
const data = await response.json()
164+
expect(data).toHaveProperty('error')
165+
expect(data).toHaveProperty('message', 'Chat not found')
166+
})
167+
168+
it('should return 403 for inactive chat', async () => {
169+
vi.doMock('@/db', () => {
170+
const mockLimit = vi.fn().mockReturnValue([
171+
{
172+
id: 'chat-id',
173+
isActive: false,
174+
authType: 'public',
175+
}
176+
])
177+
const mockWhere = vi.fn().mockReturnValue({ limit: mockLimit })
178+
const mockFrom = vi.fn().mockReturnValue({ where: mockWhere })
179+
const mockSelect = vi.fn().mockReturnValue({ from: mockFrom })
180+
181+
return {
182+
db: {
183+
select: mockSelect
184+
}
185+
}
186+
})
187+
188+
const req = createMockRequest('GET')
189+
const params = Promise.resolve({ subdomain: 'inactive-chat' })
190+
191+
const { GET } = await import('./route')
192+
193+
const response = await GET(req, { params })
194+
195+
expect(response.status).toBe(403)
196+
197+
const data = await response.json()
198+
expect(data).toHaveProperty('error')
199+
expect(data).toHaveProperty('message', 'This chat is currently unavailable')
200+
})
201+
202+
it('should return 401 when authentication is required', async () => {
203+
const originalValidateChatAuth = mockValidateChatAuth.getMockImplementation()
204+
mockValidateChatAuth.mockImplementationOnce(async () => ({
205+
authorized: false,
206+
error: 'auth_required_password'
207+
}))
208+
209+
const req = createMockRequest('GET')
210+
const params = Promise.resolve({ subdomain: 'password-protected-chat' })
211+
212+
const { GET } = await import('./route')
213+
214+
const response = await GET(req, { params })
215+
216+
expect(response.status).toBe(401)
217+
218+
const data = await response.json()
219+
expect(data).toHaveProperty('error')
220+
expect(data).toHaveProperty('message', 'auth_required_password')
221+
222+
if (originalValidateChatAuth) {
223+
mockValidateChatAuth.mockImplementation(originalValidateChatAuth)
224+
}
225+
})
226+
})
227+
228+
describe('POST endpoint', () => {
229+
230+
231+
it('should handle authentication requests without messages', async () => {
232+
const req = createMockRequest('POST', { password: 'test-password' })
233+
const params = Promise.resolve({ subdomain: 'password-protected-chat' })
234+
235+
const { POST } = await import('./route')
236+
237+
const response = await POST(req, { params })
238+
239+
expect(response.status).toBe(200)
240+
241+
const data = await response.json()
242+
expect(data).toHaveProperty('authenticated', true)
243+
244+
expect(mockSetChatAuthCookie).toHaveBeenCalled()
245+
})
246+
247+
it('should return 400 for requests without message', async () => {
248+
const req = createMockRequest('POST', {})
249+
const params = Promise.resolve({ subdomain: 'test-chat' })
250+
251+
const { POST } = await import('./route')
252+
253+
const response = await POST(req, { params })
254+
255+
expect(response.status).toBe(400)
256+
257+
const data = await response.json()
258+
expect(data).toHaveProperty('error')
259+
expect(data).toHaveProperty('message', 'No message provided')
260+
})
261+
262+
it('should return 401 for unauthorized access', async () => {
263+
const originalValidateChatAuth = mockValidateChatAuth.getMockImplementation()
264+
mockValidateChatAuth.mockImplementationOnce(async () => ({
265+
authorized: false,
266+
error: 'Authentication required'
267+
}))
268+
269+
const req = createMockRequest('POST', { message: 'Hello' })
270+
const params = Promise.resolve({ subdomain: 'protected-chat' })
271+
272+
const { POST } = await import('./route')
273+
274+
const response = await POST(req, { params })
275+
276+
expect(response.status).toBe(401)
277+
278+
const data = await response.json()
279+
expect(data).toHaveProperty('error')
280+
expect(data).toHaveProperty('message', 'Authentication required')
281+
282+
if (originalValidateChatAuth) {
283+
mockValidateChatAuth.mockImplementation(originalValidateChatAuth)
284+
}
285+
})
286+
287+
it('should return 503 when workflow is not available', async () => {
288+
vi.doMock('@/db', () => {
289+
const mockLimitChat = vi.fn().mockReturnValue([
290+
{
291+
id: 'chat-id',
292+
workflowId: 'unavailable-workflow',
293+
isActive: true,
294+
authType: 'public',
295+
}
296+
])
297+
const mockWhereChat = vi.fn().mockReturnValue({ limit: mockLimitChat })
298+
299+
// Second call returns non-deployed workflow
300+
const mockLimitWorkflow = vi.fn().mockReturnValue([
301+
{
302+
isDeployed: false
303+
}
304+
])
305+
const mockWhereWorkflow = vi.fn().mockReturnValue({ limit: mockLimitWorkflow })
306+
307+
// Mock from function to return different where implementations
308+
const mockFrom = vi.fn()
309+
.mockImplementationOnce(() => ({ where: mockWhereChat })) // First call (chat)
310+
.mockImplementationOnce(() => ({ where: mockWhereWorkflow })) // Second call (workflow)
311+
312+
const mockSelect = vi.fn().mockReturnValue({ from: mockFrom })
313+
314+
return {
315+
db: {
316+
select: mockSelect
317+
}
318+
}
319+
})
320+
321+
const req = createMockRequest('POST', { message: 'Hello' })
322+
const params = Promise.resolve({ subdomain: 'test-chat' })
323+
324+
const { POST } = await import('./route')
325+
326+
const response = await POST(req, { params })
327+
328+
expect(response.status).toBe(503)
329+
330+
const data = await response.json()
331+
expect(data).toHaveProperty('error')
332+
expect(data).toHaveProperty('message', 'Chat workflow is not available')
333+
})
334+
335+
it('should handle workflow execution errors gracefully', async () => {
336+
const originalExecuteWorkflow = mockExecuteWorkflowForChat.getMockImplementation()
337+
mockExecuteWorkflowForChat.mockImplementationOnce(async () => {
338+
throw new Error('Execution failed')
339+
})
340+
341+
const req = createMockRequest('POST', { message: 'Trigger error' })
342+
const params = Promise.resolve({ subdomain: 'test-chat' })
343+
344+
const { POST } = await import('./route')
345+
346+
const response = await POST(req, { params })
347+
348+
expect(response.status).toBe(503)
349+
350+
const data = await response.json()
351+
expect(data).toHaveProperty('error')
352+
expect(data).toHaveProperty('message', 'Chat workflow is not available')
353+
354+
if (originalExecuteWorkflow) {
355+
mockExecuteWorkflowForChat.mockImplementation(originalExecuteWorkflow)
356+
}
357+
})
358+
})
359+
})

0 commit comments

Comments
 (0)