@@ -26,6 +26,11 @@ const mockDbUpdate = vi.fn()
2626const mockDbSelect = vi . fn ( )
2727const mockValidateWorkflowAccess = vi . fn ( )
2828
29+ const READ_VALIDATION = {
30+ requireDeployment : false ,
31+ action : 'read' ,
32+ } as const
33+
2934/**
3035 * Helper to set mock auth state consistently across getSession and hybrid auth.
3136 */
@@ -127,12 +132,14 @@ describe('Workflow By ID API Route', () => {
127132 expect ( response . status ) . toBe ( 401 )
128133 const data = await response . json ( )
129134 expect ( data . error ) . toBe ( 'Unauthorized' )
135+ expect ( mockValidateWorkflowAccess ) . toHaveBeenCalledWith ( req , 'workflow-123' , READ_VALIDATION )
130136 } )
131137
132138 it ( 'should return 404 when workflow does not exist' , async ( ) => {
133139 mockGetSession ( { user : { id : 'user-123' } } )
134-
135- mockGetWorkflowById . mockResolvedValue ( null )
140+ mockValidateWorkflowAccess . mockResolvedValue ( {
141+ error : { message : 'Workflow not found' , status : 404 } ,
142+ } )
136143
137144 const req = new NextRequest ( 'http://localhost:3000/api/workflows/nonexistent' )
138145 const params = Promise . resolve ( { id : 'nonexistent' } )
@@ -142,24 +149,20 @@ describe('Workflow By ID API Route', () => {
142149 expect ( response . status ) . toBe ( 404 )
143150 const data = await response . json ( )
144151 expect ( data . error ) . toBe ( 'Workflow not found' )
152+ expect ( mockValidateWorkflowAccess ) . toHaveBeenCalledWith ( req , 'nonexistent' , READ_VALIDATION )
145153 } )
146154
147155 it ( 'should return 404 for workspace api key targeting a workflow in another workspace' , async ( ) => {
148- const mockWorkflow = {
149- id : 'workflow-123' ,
150- userId : 'other-user' ,
151- name : 'Foreign Workflow' ,
152- workspaceId : 'workspace-b' ,
153- }
154-
155156 mockCheckHybridAuth . mockResolvedValue ( {
156157 success : true ,
157158 userId : 'api-user' ,
158159 authType : 'api_key' ,
159160 apiKeyType : 'workspace' ,
160161 workspaceId : 'workspace-a' ,
161162 } )
162- mockGetWorkflowById . mockResolvedValue ( mockWorkflow )
163+ mockValidateWorkflowAccess . mockResolvedValue ( {
164+ error : { message : 'Workflow not found' , status : 404 } ,
165+ } )
163166
164167 const req = new NextRequest ( 'http://localhost:3000/api/workflows/workflow-123' , {
165168 headers : { 'x-api-key' : 'test-key' } ,
@@ -171,32 +174,124 @@ describe('Workflow By ID API Route', () => {
171174 expect ( response . status ) . toBe ( 404 )
172175 const data = await response . json ( )
173176 expect ( data . error ) . toBe ( 'Workflow not found' )
177+ expect ( mockValidateWorkflowAccess ) . toHaveBeenCalledWith ( req , 'workflow-123' , READ_VALIDATION )
178+ expect ( mockGetWorkflowById ) . not . toHaveBeenCalled ( )
174179 expect ( mockAuthorizeWorkflowByWorkspacePermission ) . not . toHaveBeenCalled ( )
175180 } )
176181
177- it ( 'should return 401 for verified internal jwt without userId' , async ( ) => {
182+ it ( 'should allow verified internal jwt without userId through the narrow precheck' , async ( ) => {
183+ const mockWorkflow = {
184+ id : 'workflow-123' ,
185+ userId : 'other-user' ,
186+ name : 'Internal Workflow' ,
187+ workspaceId : 'workspace-456' ,
188+ isDeployed : false ,
189+ deployedAt : null ,
190+ variables : { } ,
191+ }
192+ const mockNormalizedData = {
193+ blocks : { } ,
194+ edges : [ ] ,
195+ loops : { } ,
196+ parallels : { } ,
197+ isFromNormalizedTables : true ,
198+ }
199+
178200 mockCheckHybridAuth . mockResolvedValue ( {
179201 success : true ,
180202 authType : 'internal_jwt' ,
181203 } )
182- mockGetWorkflowById . mockResolvedValue ( {
204+ mockGetWorkflowById . mockResolvedValue ( mockWorkflow )
205+ mockLoadWorkflowFromNormalizedTables . mockResolvedValue ( mockNormalizedData )
206+
207+ const req = new NextRequest ( 'http://localhost:3000/api/workflows/workflow-123' , {
208+ headers : { authorization : 'Bearer internal-token' } ,
209+ } )
210+ const params = Promise . resolve ( { id : 'workflow-123' } )
211+
212+ const response = await GET ( req , { params } )
213+
214+ expect ( response . status ) . toBe ( 200 )
215+ const data = await response . json ( )
216+ expect ( data . data . id ) . toBe ( 'workflow-123' )
217+ expect ( data . data . state . blocks ) . toEqual ( mockNormalizedData . blocks )
218+ expect ( data . data . variables ) . toEqual ( { } )
219+ expect ( mockValidateWorkflowAccess ) . not . toHaveBeenCalled ( )
220+ expect ( mockAuthorizeWorkflowByWorkspacePermission ) . not . toHaveBeenCalled ( )
221+ } )
222+
223+ it ( 'should deny personal api key reads when middleware rejects workspace policy' , async ( ) => {
224+ mockCheckHybridAuth . mockResolvedValue ( {
225+ success : true ,
226+ userId : 'api-user' ,
227+ authType : 'api_key' ,
228+ apiKeyType : 'personal' ,
229+ } )
230+ mockValidateWorkflowAccess . mockResolvedValue ( {
231+ error : { message : 'Unauthorized: Invalid API key' , status : 401 } ,
232+ } )
233+
234+ const req = new NextRequest ( 'http://localhost:3000/api/workflows/workflow-123' , {
235+ headers : { 'x-api-key' : 'personal-key' } ,
236+ } )
237+ const params = Promise . resolve ( { id : 'workflow-123' } )
238+
239+ const response = await GET ( req , { params } )
240+
241+ expect ( response . status ) . toBe ( 401 )
242+ expect ( await response . json ( ) ) . toEqual ( { error : 'Unauthorized: Invalid API key' } )
243+ expect ( mockValidateWorkflowAccess ) . toHaveBeenCalledWith ( req , 'workflow-123' , READ_VALIDATION )
244+ } )
245+
246+ it ( 'should allow personal api key reads when middleware returns scoped success' , async ( ) => {
247+ const mockWorkflow = {
183248 id : 'workflow-123' ,
184249 userId : 'other-user' ,
185- name : 'Internal Workflow' ,
250+ name : 'Scoped Personal Workflow' ,
186251 workspaceId : 'workspace-456' ,
252+ isDeployed : false ,
253+ deployedAt : null ,
254+ variables : { secret : 'ok' } ,
255+ }
256+ const mockNormalizedData = {
257+ blocks : { } ,
258+ edges : [ ] ,
259+ loops : { } ,
260+ parallels : { } ,
261+ isFromNormalizedTables : true ,
262+ }
263+
264+ mockCheckHybridAuth . mockResolvedValue ( {
265+ success : true ,
266+ userId : 'api-user' ,
267+ authType : 'api_key' ,
268+ apiKeyType : 'personal' ,
187269 } )
270+ mockValidateWorkflowAccess . mockResolvedValue ( {
271+ workflow : mockWorkflow ,
272+ auth : {
273+ success : true ,
274+ userId : 'api-user' ,
275+ authType : 'api_key' ,
276+ apiKeyType : 'personal' ,
277+ workspaceId : 'workspace-456' ,
278+ } ,
279+ } )
280+ mockLoadWorkflowFromNormalizedTables . mockResolvedValue ( mockNormalizedData )
188281
189282 const req = new NextRequest ( 'http://localhost:3000/api/workflows/workflow-123' , {
190- headers : { authorization : 'Bearer internal-token ' } ,
283+ headers : { 'x-api-key' : 'personal-key ' } ,
191284 } )
192285 const params = Promise . resolve ( { id : 'workflow-123' } )
193286
194287 const response = await GET ( req , { params } )
195288
196- expect ( response . status ) . toBe ( 401 )
289+ expect ( response . status ) . toBe ( 200 )
197290 const data = await response . json ( )
198- expect ( data . error ) . toBe ( 'Unauthorized' )
199- expect ( mockAuthorizeWorkflowByWorkspacePermission ) . not . toHaveBeenCalled ( )
291+ expect ( data . data . id ) . toBe ( 'workflow-123' )
292+ expect ( data . data . variables ) . toEqual ( { secret : 'ok' } )
293+ expect ( mockValidateWorkflowAccess ) . toHaveBeenCalledWith ( req , 'workflow-123' , READ_VALIDATION )
294+ expect ( mockGetWorkflowById ) . not . toHaveBeenCalled ( )
200295 } )
201296
202297 it ( 'should allow access when user has admin workspace permission' , async ( ) => {
@@ -216,13 +311,9 @@ describe('Workflow By ID API Route', () => {
216311 }
217312
218313 mockGetSession ( { user : { id : 'user-123' } } )
219-
220- mockGetWorkflowById . mockResolvedValue ( mockWorkflow )
221- mockAuthorizeWorkflowByWorkspacePermission . mockResolvedValue ( {
222- allowed : true ,
223- status : 200 ,
314+ mockValidateWorkflowAccess . mockResolvedValue ( {
224315 workflow : mockWorkflow ,
225- workspacePermission : 'admin' ,
316+ auth : { success : true , userId : 'user-123' , authType : 'session' } ,
226317 } )
227318
228319 mockLoadWorkflowFromNormalizedTables . mockResolvedValue ( mockNormalizedData )
@@ -235,6 +326,7 @@ describe('Workflow By ID API Route', () => {
235326 expect ( response . status ) . toBe ( 200 )
236327 const data = await response . json ( )
237328 expect ( data . data . id ) . toBe ( 'workflow-123' )
329+ expect ( mockValidateWorkflowAccess ) . toHaveBeenCalledWith ( req , 'workflow-123' , READ_VALIDATION )
238330 } )
239331
240332 it ( 'should allow access when user has workspace permissions' , async ( ) => {
@@ -254,13 +346,9 @@ describe('Workflow By ID API Route', () => {
254346 }
255347
256348 mockGetSession ( { user : { id : 'user-123' } } )
257-
258- mockGetWorkflowById . mockResolvedValue ( mockWorkflow )
259- mockAuthorizeWorkflowByWorkspacePermission . mockResolvedValue ( {
260- allowed : true ,
261- status : 200 ,
349+ mockValidateWorkflowAccess . mockResolvedValue ( {
262350 workflow : mockWorkflow ,
263- workspacePermission : 'read' ,
351+ auth : { success : true , userId : 'user-123' , authType : 'session' } ,
264352 } )
265353
266354 mockLoadWorkflowFromNormalizedTables . mockResolvedValue ( mockNormalizedData )
@@ -273,6 +361,7 @@ describe('Workflow By ID API Route', () => {
273361 expect ( response . status ) . toBe ( 200 )
274362 const data = await response . json ( )
275363 expect ( data . data . id ) . toBe ( 'workflow-123' )
364+ expect ( mockValidateWorkflowAccess ) . toHaveBeenCalledWith ( req , 'workflow-123' , READ_VALIDATION )
276365 } )
277366
278367 it ( 'should keep session access semantics unchanged for readable workflows' , async ( ) => {
@@ -284,12 +373,9 @@ describe('Workflow By ID API Route', () => {
284373 }
285374
286375 mockGetSession ( { user : { id : 'user-123' } } )
287- mockGetWorkflowById . mockResolvedValue ( mockWorkflow )
288- mockAuthorizeWorkflowByWorkspacePermission . mockResolvedValue ( {
289- allowed : true ,
290- status : 200 ,
376+ mockValidateWorkflowAccess . mockResolvedValue ( {
291377 workflow : mockWorkflow ,
292- workspacePermission : 'read' ,
378+ auth : { success : true , userId : 'user-123' , authType : 'session' } ,
293379 } )
294380
295381 const req = new NextRequest ( 'http://localhost:3000/api/workflows/workflow-123' )
@@ -300,30 +386,49 @@ describe('Workflow By ID API Route', () => {
300386 expect ( response . status ) . toBe ( 200 )
301387 const data = await response . json ( )
302388 expect ( data . data . id ) . toBe ( 'workflow-123' )
303- expect ( mockAuthorizeWorkflowByWorkspacePermission ) . toHaveBeenCalledWith ( {
304- workflowId : 'workflow-123' ,
305- userId : 'user-123' ,
306- action : 'read' ,
307- } )
389+ expect ( mockValidateWorkflowAccess ) . toHaveBeenCalledWith ( req , 'workflow-123' , READ_VALIDATION )
308390 } )
309391
310- it ( 'should deny access when user has no workspace permissions ' , async ( ) => {
392+ it ( 'should not use the internal precheck for user-backed internal callers ' , async ( ) => {
311393 const mockWorkflow = {
312394 id : 'workflow-123' ,
313395 userId : 'other-user' ,
314- name : 'Test Workflow' ,
396+ name : 'Internal User Workflow' ,
315397 workspaceId : 'workspace-456' ,
398+ isDeployed : false ,
399+ deployedAt : null ,
400+ variables : { } ,
316401 }
317402
318- mockGetSession ( { user : { id : 'user-123' } } )
319-
320- mockGetWorkflowById . mockResolvedValue ( mockWorkflow )
321- mockAuthorizeWorkflowByWorkspacePermission . mockResolvedValue ( {
322- allowed : false ,
323- status : 403 ,
324- message : 'Unauthorized: Access denied to read this workflow' ,
403+ mockCheckHybridAuth . mockResolvedValue ( {
404+ success : true ,
405+ userId : 'internal-user' ,
406+ authType : 'internal_jwt' ,
407+ } )
408+ mockValidateWorkflowAccess . mockResolvedValue ( {
325409 workflow : mockWorkflow ,
326- workspacePermission : null ,
410+ auth : { success : true , userId : 'internal-user' , authType : 'internal_jwt' } ,
411+ } )
412+
413+ const req = new NextRequest ( 'http://localhost:3000/api/workflows/workflow-123' , {
414+ headers : { authorization : 'Bearer internal-token' } ,
415+ } )
416+ const params = Promise . resolve ( { id : 'workflow-123' } )
417+
418+ const response = await GET ( req , { params } )
419+
420+ expect ( response . status ) . toBe ( 200 )
421+ expect ( mockValidateWorkflowAccess ) . toHaveBeenCalledWith ( req , 'workflow-123' , READ_VALIDATION )
422+ expect ( mockGetWorkflowById ) . not . toHaveBeenCalled ( )
423+ } )
424+
425+ it ( 'should deny access when user has no workspace permissions' , async ( ) => {
426+ mockGetSession ( { user : { id : 'user-123' } } )
427+ mockValidateWorkflowAccess . mockResolvedValue ( {
428+ error : {
429+ message : 'Unauthorized: Access denied to read this workflow' ,
430+ status : 403 ,
431+ } ,
327432 } )
328433
329434 const req = new NextRequest ( 'http://localhost:3000/api/workflows/workflow-123' )
@@ -334,6 +439,7 @@ describe('Workflow By ID API Route', () => {
334439 expect ( response . status ) . toBe ( 403 )
335440 const data = await response . json ( )
336441 expect ( data . error ) . toBe ( 'Unauthorized: Access denied to read this workflow' )
442+ expect ( mockValidateWorkflowAccess ) . toHaveBeenCalledWith ( req , 'workflow-123' , READ_VALIDATION )
337443 } )
338444
339445 it ( 'should use normalized tables when available' , async ( ) => {
@@ -353,13 +459,9 @@ describe('Workflow By ID API Route', () => {
353459 }
354460
355461 mockGetSession ( { user : { id : 'user-123' } } )
356-
357- mockGetWorkflowById . mockResolvedValue ( mockWorkflow )
358- mockAuthorizeWorkflowByWorkspacePermission . mockResolvedValue ( {
359- allowed : true ,
360- status : 200 ,
462+ mockValidateWorkflowAccess . mockResolvedValue ( {
361463 workflow : mockWorkflow ,
362- workspacePermission : 'admin' ,
464+ auth : { success : true , userId : 'user-123' , authType : 'session' } ,
363465 } )
364466
365467 mockLoadWorkflowFromNormalizedTables . mockResolvedValue ( mockNormalizedData )
@@ -373,6 +475,7 @@ describe('Workflow By ID API Route', () => {
373475 const data = await response . json ( )
374476 expect ( data . data . state . blocks ) . toEqual ( mockNormalizedData . blocks )
375477 expect ( data . data . state . edges ) . toEqual ( mockNormalizedData . edges )
478+ expect ( mockValidateWorkflowAccess ) . toHaveBeenCalledWith ( req , 'workflow-123' , READ_VALIDATION )
376479 } )
377480 } )
378481
@@ -966,8 +1069,7 @@ describe('Workflow By ID API Route', () => {
9661069 describe ( 'Error handling' , ( ) => {
9671070 it ( 'should handle database errors gracefully' , async ( ) => {
9681071 mockGetSession ( { user : { id : 'user-123' } } )
969-
970- mockGetWorkflowById . mockRejectedValue ( new Error ( 'Database connection timeout' ) )
1072+ mockValidateWorkflowAccess . mockRejectedValue ( new Error ( 'Database connection timeout' ) )
9711073
9721074 const req = new NextRequest ( 'http://localhost:3000/api/workflows/workflow-123' )
9731075 const params = Promise . resolve ( { id : 'workflow-123' } )
0 commit comments