44import { beforeEach , describe , expect , it , vi } from 'vitest'
55import type { IRoomManager } from '@/rooms'
66
7- const { mockGetWorkflowState, mockVerifyWorkflowAccess } = vi . hoisted ( ( ) => ( {
8- mockGetWorkflowState : vi . fn ( ) ,
9- mockVerifyWorkflowAccess : vi . fn ( ) ,
10- } ) )
7+ const { mockGetWorkflowState, mockVerifyWorkflowAccess, mockResolveCurrentWorkflowRole } =
8+ vi . hoisted ( ( ) => ( {
9+ mockGetWorkflowState : vi . fn ( ) ,
10+ mockVerifyWorkflowAccess : vi . fn ( ) ,
11+ mockResolveCurrentWorkflowRole : vi . fn ( ) ,
12+ } ) )
1113
1214vi . mock ( '@sim/db' , ( ) => ( {
1315 db : { select : vi . fn ( ) } ,
@@ -20,6 +22,7 @@ vi.mock('@/database/operations', () => ({
2022
2123vi . mock ( '@/middleware/permissions' , ( ) => ( {
2224 verifyWorkflowAccess : mockVerifyWorkflowAccess ,
25+ resolveCurrentWorkflowRole : mockResolveCurrentWorkflowRole ,
2326} ) )
2427
2528import { setupWorkflowHandlers } from '@/handlers/workflow'
@@ -86,6 +89,7 @@ describe('setupWorkflowHandlers', () => {
8689 vi . clearAllMocks ( )
8790 mockGetWorkflowState . mockResolvedValue ( { id : 'workflow-1' , state : { } } )
8891 mockVerifyWorkflowAccess . mockResolvedValue ( { hasAccess : true , role : 'admin' } )
92+ mockResolveCurrentWorkflowRole . mockResolvedValue ( 'admin' )
8993 } )
9094
9195 it ( 'includes workflowId when authentication is missing' , async ( ) => {
@@ -149,6 +153,52 @@ describe('setupWorkflowHandlers', () => {
149153 } )
150154 } )
151155
156+ it ( 'denies the join when access is revoked while the join is in flight' , async ( ) => {
157+ mockResolveCurrentWorkflowRole . mockResolvedValue ( null )
158+
159+ const { socket, handlers } = createSocket ( )
160+ const roomManager = createRoomManager ( )
161+
162+ setupWorkflowHandlers (
163+ socket as unknown as Parameters < typeof setupWorkflowHandlers > [ 0 ] ,
164+ roomManager
165+ )
166+
167+ await handlers [ 'join-workflow' ] ( { workflowId : 'workflow-1' , tabSessionId : 'tab-1' } )
168+
169+ expect ( socket . emit ) . toHaveBeenCalledWith ( 'join-workflow-error' , {
170+ workflowId : 'workflow-1' ,
171+ error : 'Access denied to workflow' ,
172+ code : 'ACCESS_DENIED' ,
173+ retryable : false ,
174+ } )
175+ expect ( socket . join ) . not . toHaveBeenCalled ( )
176+ expect ( roomManager . addUserToRoom ) . not . toHaveBeenCalled ( )
177+ } )
178+
179+ it ( 'joins with the re-validated role, passing the join-time role as fallback' , async ( ) => {
180+ mockVerifyWorkflowAccess . mockResolvedValue ( { hasAccess : true , role : 'write' } )
181+ mockResolveCurrentWorkflowRole . mockResolvedValue ( 'read' )
182+
183+ const { socket, handlers } = createSocket ( )
184+ const roomManager = createRoomManager ( )
185+
186+ setupWorkflowHandlers (
187+ socket as unknown as Parameters < typeof setupWorkflowHandlers > [ 0 ] ,
188+ roomManager
189+ )
190+
191+ await handlers [ 'join-workflow' ] ( { workflowId : 'workflow-1' , tabSessionId : 'tab-1' } )
192+
193+ expect ( mockResolveCurrentWorkflowRole ) . toHaveBeenCalledWith ( 'user-1' , 'workflow-1' , 'write' )
194+ expect ( socket . join ) . toHaveBeenCalledWith ( 'workflow-1' )
195+ expect ( roomManager . addUserToRoom ) . toHaveBeenCalledWith (
196+ 'workflow-1' ,
197+ 'socket-1' ,
198+ expect . objectContaining ( { role : 'read' } )
199+ )
200+ } )
201+
152202 it ( 'marks workflow access verification failures as retryable' , async ( ) => {
153203 mockVerifyWorkflowAccess . mockRejectedValue ( new Error ( 'database unavailable' ) )
154204
0 commit comments