@@ -2,13 +2,6 @@ import { createSession, createWorkspaceRecord, loggerMock } from '@sim/testing'
22import { NextRequest } from 'next/server'
33import { beforeEach , describe , expect , it , vi } from 'vitest'
44
5- /**
6- * Tests for workspace invitation by ID API route
7- * Tests GET (details + token acceptance), DELETE (cancellation)
8- *
9- * @vitest -environment node
10- */
11-
125const mockGetSession = vi . fn ( )
136const mockHasWorkspaceAdminAccess = vi . fn ( )
147
@@ -227,7 +220,7 @@ describe('Workspace Invitation [invitationId] API Route', () => {
227220 expect ( response . headers . get ( 'location' ) ) . toBe ( 'https://test.sim.ai/workspace/workspace-456/w' )
228221 } )
229222
230- it ( 'should redirect to error page when invitation expired' , async ( ) => {
223+ it ( 'should redirect to error page with token preserved when invitation expired' , async ( ) => {
231224 const session = createSession ( {
232225 userId : mockUser . id ,
233226@@ -250,12 +243,13 @@ describe('Workspace Invitation [invitationId] API Route', () => {
250243 const response = await GET ( request , { params } )
251244
252245 expect ( response . status ) . toBe ( 307 )
253- expect ( response . headers . get ( 'location' ) ) . toBe (
254- 'https://test.sim.ai/invite/invitation-789?error=expired'
246+ const location = response . headers . get ( 'location' )
247+ expect ( location ) . toBe (
248+ 'https://test.sim.ai/invite/invitation-789?error=expired&token=token-abc123'
255249 )
256250 } )
257251
258- it ( 'should redirect to error page when email mismatch' , async ( ) => {
252+ it ( 'should redirect to error page with token preserved when email mismatch' , async ( ) => {
259253 const session = createSession ( {
260254 userId : mockUser . id ,
261255@@ -277,12 +271,13 @@ describe('Workspace Invitation [invitationId] API Route', () => {
277271 const response = await GET ( request , { params } )
278272
279273 expect ( response . status ) . toBe ( 307 )
280- expect ( response . headers . get ( 'location' ) ) . toBe (
281- 'https://test.sim.ai/invite/invitation-789?error=email-mismatch'
274+ const location = response . headers . get ( 'location' )
275+ expect ( location ) . toBe (
276+ 'https://test.sim.ai/invite/invitation-789?error=email-mismatch&token=token-abc123'
282277 )
283278 } )
284279
285- it ( 'should return 404 when invitation not found' , async ( ) => {
280+ it ( 'should return 404 when invitation not found (without token) ' , async ( ) => {
286281 const session = createSession ( { userId : mockUser . id , email : mockUser . email } )
287282 mockGetSession . mockResolvedValue ( session )
288283 dbSelectResults = [ [ ] ]
@@ -296,6 +291,189 @@ describe('Workspace Invitation [invitationId] API Route', () => {
296291 expect ( response . status ) . toBe ( 404 )
297292 expect ( data ) . toEqual ( { error : 'Invitation not found or has expired' } )
298293 } )
294+
295+ it ( 'should redirect to error page with token preserved when invitation not found (with token)' , async ( ) => {
296+ const session = createSession ( { userId : mockUser . id , email : mockUser . email } )
297+ mockGetSession . mockResolvedValue ( session )
298+ dbSelectResults = [ [ ] ]
299+
300+ const request = new NextRequest (
301+ 'http://localhost/api/workspaces/invitations/non-existent?token=some-invalid-token'
302+ )
303+ const params = Promise . resolve ( { invitationId : 'non-existent' } )
304+
305+ const response = await GET ( request , { params } )
306+
307+ expect ( response . status ) . toBe ( 307 )
308+ const location = response . headers . get ( 'location' )
309+ expect ( location ) . toBe (
310+ 'https://test.sim.ai/invite/non-existent?error=invalid-token&token=some-invalid-token'
311+ )
312+ } )
313+
314+ it ( 'should redirect to error page with token preserved when invitation already processed' , async ( ) => {
315+ const session = createSession ( {
316+ userId : mockUser . id ,
317+ 318+ name : mockUser . name ,
319+ } )
320+ mockGetSession . mockResolvedValue ( session )
321+
322+ const acceptedInvitation = {
323+ ...mockInvitation ,
324+ status : 'accepted' ,
325+ }
326+
327+ dbSelectResults = [ [ acceptedInvitation ] , [ mockWorkspace ] ]
328+
329+ const request = new NextRequest (
330+ 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123'
331+ )
332+ const params = Promise . resolve ( { invitationId : 'token-abc123' } )
333+
334+ const response = await GET ( request , { params } )
335+
336+ expect ( response . status ) . toBe ( 307 )
337+ const location = response . headers . get ( 'location' )
338+ expect ( location ) . toBe (
339+ 'https://test.sim.ai/invite/invitation-789?error=already-processed&token=token-abc123'
340+ )
341+ } )
342+
343+ it ( 'should redirect to error page with token preserved when workspace not found' , async ( ) => {
344+ const session = createSession ( {
345+ userId : mockUser . id ,
346+ 347+ name : mockUser . name ,
348+ } )
349+ mockGetSession . mockResolvedValue ( session )
350+
351+ dbSelectResults = [ [ mockInvitation ] , [ ] ]
352+
353+ const request = new NextRequest (
354+ 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123'
355+ )
356+ const params = Promise . resolve ( { invitationId : 'token-abc123' } )
357+
358+ const response = await GET ( request , { params } )
359+
360+ expect ( response . status ) . toBe ( 307 )
361+ const location = response . headers . get ( 'location' )
362+ expect ( location ) . toBe (
363+ 'https://test.sim.ai/invite/invitation-789?error=workspace-not-found&token=token-abc123'
364+ )
365+ } )
366+
367+ it ( 'should redirect to error page with token preserved when user not found' , async ( ) => {
368+ const session = createSession ( {
369+ userId : mockUser . id ,
370+ 371+ name : mockUser . name ,
372+ } )
373+ mockGetSession . mockResolvedValue ( session )
374+
375+ dbSelectResults = [ [ mockInvitation ] , [ mockWorkspace ] , [ ] ]
376+
377+ const request = new NextRequest (
378+ 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123'
379+ )
380+ const params = Promise . resolve ( { invitationId : 'token-abc123' } )
381+
382+ const response = await GET ( request , { params } )
383+
384+ expect ( response . status ) . toBe ( 307 )
385+ const location = response . headers . get ( 'location' )
386+ expect ( location ) . toBe (
387+ 'https://test.sim.ai/invite/invitation-789?error=user-not-found&token=token-abc123'
388+ )
389+ } )
390+
391+ it ( 'should URL encode special characters in token when preserving in error redirects' , async ( ) => {
392+ const session = createSession ( {
393+ userId : mockUser . id ,
394+ 395+ name : mockUser . name ,
396+ } )
397+ mockGetSession . mockResolvedValue ( session )
398+
399+ dbSelectResults = [
400+ [ mockInvitation ] ,
401+ [ mockWorkspace ] ,
402+ [ { ...
mockUser , email :
'[email protected] ' } ] , 403+ ]
404+
405+ const specialToken = 'token+with/special=chars&more'
406+ const request = new NextRequest (
407+ `http://localhost/api/workspaces/invitations/token-abc123?token=${ encodeURIComponent ( specialToken ) } `
408+ )
409+ const params = Promise . resolve ( { invitationId : 'token-abc123' } )
410+
411+ const response = await GET ( request , { params } )
412+
413+ expect ( response . status ) . toBe ( 307 )
414+ const location = response . headers . get ( 'location' )
415+ expect ( location ) . toContain ( 'error=email-mismatch' )
416+ expect ( location ) . toContain ( `token=${ encodeURIComponent ( specialToken ) } ` )
417+ } )
418+ } )
419+
420+ describe ( 'Token Preservation - Full Flow Scenario' , ( ) => {
421+ it ( 'should preserve token through email mismatch so user can retry with correct account' , async ( ) => {
422+ const wrongSession = createSession ( {
423+ userId : 'wrong-user' ,
424+ 425+ name : 'Wrong User' ,
426+ } )
427+ mockGetSession . mockResolvedValue ( wrongSession )
428+
429+ dbSelectResults = [
430+ [ mockInvitation ] ,
431+ [ mockWorkspace ] ,
432+ [ { id :
'wrong-user' , email :
'[email protected] ' } ] , 433+ ]
434+
435+ const request1 = new NextRequest (
436+ 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123'
437+ )
438+ const params1 = Promise . resolve ( { invitationId : 'token-abc123' } )
439+
440+ const response1 = await GET ( request1 , { params : params1 } )
441+
442+ expect ( response1 . status ) . toBe ( 307 )
443+ const location1 = response1 . headers . get ( 'location' )
444+ expect ( location1 ) . toBe (
445+ 'https://test.sim.ai/invite/invitation-789?error=email-mismatch&token=token-abc123'
446+ )
447+
448+ vi . clearAllMocks ( )
449+ dbSelectCallIndex = 0
450+
451+ const correctSession = createSession ( {
452+ userId : mockUser . id ,
453+ 454+ name : mockUser . name ,
455+ } )
456+ mockGetSession . mockResolvedValue ( correctSession )
457+
458+ dbSelectResults = [
459+ [ mockInvitation ] ,
460+ [ mockWorkspace ] ,
461+ [ { ...
mockUser , email :
'[email protected] ' } ] , 462+ [ ] ,
463+ ]
464+
465+ const request2 = new NextRequest (
466+ 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123'
467+ )
468+ const params2 = Promise . resolve ( { invitationId : 'token-abc123' } )
469+
470+ const response2 = await GET ( request2 , { params : params2 } )
471+
472+ expect ( response2 . status ) . toBe ( 307 )
473+ expect ( response2 . headers . get ( 'location' ) ) . toBe (
474+ 'https://test.sim.ai/workspace/workspace-456/w'
475+ )
476+ } )
299477 } )
300478
301479 describe ( 'DELETE /api/workspaces/invitations/[invitationId]' , ( ) => {
0 commit comments