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