@@ -8,16 +8,14 @@ import {
88 authMockFns ,
99 createMockRequest ,
1010 dbChainMockFns ,
11+ foldersLifecycleMock ,
12+ foldersLifecycleMockFns ,
1113 type MockUser ,
1214 permissionsMock ,
1315 permissionsMockFns ,
1416 queueTableRows ,
1517 resetDbChainMock ,
1618 schemaMock ,
17- workflowsOrchestrationMock ,
18- workflowsOrchestrationMockFns ,
19- workflowsUtilsMock ,
20- workflowsUtilsMockFns ,
2119} from '@sim/testing'
2220import { afterAll , beforeEach , describe , expect , it , vi } from 'vitest'
2321
@@ -36,8 +34,11 @@ const { mockLogger } = vi.hoisted(() => {
3634 }
3735} )
3836
39- const mockPerformDeleteFolder = workflowsOrchestrationMockFns . mockPerformDeleteFolder
40- const mockPerformUpdateFolder = workflowsOrchestrationMockFns . mockPerformUpdateFolder
37+ const mockDeleteFolder = foldersLifecycleMockFns . mockDeleteFolder
38+ const mockUpdateFolder = foldersLifecycleMockFns . mockUpdateFolder
39+
40+ /** Parent ids the mocked engine treats as closing a cycle for the folder under test. */
41+ const cyclicParentIds = new Set < string > ( )
4142
4243const mockGetUserEntityPermissions = permissionsMockFns . mockGetUserEntityPermissions
4344
@@ -48,8 +49,7 @@ vi.mock('@sim/logger', () => ({
4849 getRequestContext : ( ) => undefined ,
4950} ) )
5051vi . mock ( '@/lib/workspaces/permissions/utils' , ( ) => permissionsMock )
51- vi . mock ( '@/lib/workflows/orchestration' , ( ) => workflowsOrchestrationMock )
52- vi . mock ( '@/lib/workflows/utils' , ( ) => workflowsUtilsMock )
52+ vi . mock ( '@/lib/folders/lifecycle' , ( ) => foldersLifecycleMock )
5353
5454import { DELETE , PUT } from '@/app/api/folders/[id]/route'
5555
@@ -101,25 +101,19 @@ describe('Individual Folder API Route', () => {
101101 resetDbChainMock ( )
102102
103103 mockGetUserEntityPermissions . mockResolvedValue ( 'admin' )
104- mockPerformDeleteFolder . mockResolvedValue ( {
104+ mockDeleteFolder . mockResolvedValue ( {
105105 success : true ,
106106 deletedItems : { folders : 1 , workflows : 0 } ,
107107 } )
108- mockPerformUpdateFolder . mockImplementation ( async ( params ) => {
108+ mockUpdateFolder . mockImplementation ( async ( params ) => {
109109 if ( params . parentId && params . parentId === params . folderId ) {
110110 return {
111111 success : false ,
112112 error : 'Folder cannot be its own parent' ,
113113 errorCode : 'validation' ,
114114 }
115115 }
116- if (
117- params . parentId &&
118- ( await workflowsUtilsMockFns . mockCheckForCircularReference (
119- params . folderId ,
120- params . parentId
121- ) )
122- ) {
116+ if ( params . parentId && cyclicParentIds . has ( params . parentId ) ) {
123117 return {
124118 success : false ,
125119 error : 'Cannot create circular folder reference' ,
@@ -140,7 +134,7 @@ describe('Individual Folder API Route', () => {
140134 } ,
141135 }
142136 } )
143- workflowsUtilsMockFns . mockCheckForCircularReference . mockResolvedValue ( false )
137+ cyclicParentIds . clear ( )
144138 } )
145139
146140 describe ( 'PUT /api/folders/[id]' , ( ) => {
@@ -250,6 +244,26 @@ describe('Individual Folder API Route', () => {
250244 expect ( data ) . toHaveProperty ( 'folder' )
251245 } )
252246
247+ it ( 'rejects a locked write on a resource type that has no lock semantics' , async ( ) => {
248+ mockAuthenticatedUser ( )
249+ queueFolderLookup ( )
250+
251+ const req = createMockRequest (
252+ 'PUT' ,
253+ { locked : true } ,
254+ { } ,
255+ 'http://localhost:3000/api/folders/folder-1?resourceType=knowledge_base'
256+ )
257+ const params = Promise . resolve ( { id : 'folder-1' } )
258+
259+ const response = await PUT ( req , { params } )
260+
261+ expect ( response . status ) . toBe ( 400 )
262+ const data = await response . json ( )
263+ expect ( data . error ) . toBe ( 'Folder locking is only supported for workflow folders' )
264+ expect ( mockUpdateFolder ) . not . toHaveBeenCalled ( )
265+ } )
266+
253267 it ( 'should return 400 when trying to set folder as its own parent' , async ( ) => {
254268 mockAuthenticatedUser ( )
255269
@@ -368,7 +382,7 @@ describe('Individual Folder API Route', () => {
368382 workspaceId : 'workspace-123' ,
369383 } )
370384
371- workflowsUtilsMockFns . mockCheckForCircularReference . mockResolvedValue ( true )
385+ cyclicParentIds . add ( 'folder-1' )
372386
373387 const req = createMockRequest ( 'PUT' , {
374388 name : 'Updated Folder 3' ,
@@ -382,9 +396,8 @@ describe('Individual Folder API Route', () => {
382396
383397 const data = await response . json ( )
384398 expect ( data ) . toHaveProperty ( 'error' , 'Cannot create circular folder reference' )
385- expect ( workflowsUtilsMockFns . mockCheckForCircularReference ) . toHaveBeenCalledWith (
386- 'folder-3' ,
387- 'folder-1'
399+ expect ( mockUpdateFolder ) . toHaveBeenCalledWith (
400+ expect . objectContaining ( { folderId : 'folder-3' , parentId : 'folder-1' } )
388401 )
389402 } )
390403 } )
@@ -405,14 +418,34 @@ describe('Individual Folder API Route', () => {
405418 const data = await response . json ( )
406419 expect ( data ) . toHaveProperty ( 'success' , true )
407420 expect ( data ) . toHaveProperty ( 'deletedItems' )
408- expect ( mockPerformDeleteFolder ) . toHaveBeenCalledWith ( {
421+ expect ( mockDeleteFolder ) . toHaveBeenCalledWith ( {
422+ resourceType : 'workflow' ,
409423 folderId : 'folder-1' ,
410424 workspaceId : 'workspace-123' ,
411425 userId : TEST_USER . id ,
412426 folderName : 'Test Folder' ,
413427 } )
414428 } )
415429
430+ it ( 'surfaces a delete-locked resource as 423, not a generic 500' , async ( ) => {
431+ mockAuthenticatedUser ( )
432+ queueFolderLookup ( )
433+ mockDeleteFolder . mockResolvedValueOnce ( {
434+ success : false ,
435+ error : 'Cannot delete folder: table Ledger is delete-locked' ,
436+ errorCode : 'locked' ,
437+ } )
438+
439+ const req = createMockRequest ( 'DELETE' )
440+ const params = Promise . resolve ( { id : 'folder-1' } )
441+
442+ const response = await DELETE ( req , { params } )
443+
444+ expect ( response . status ) . toBe ( 423 )
445+ const data = await response . json ( )
446+ expect ( data . error ) . toBe ( 'Cannot delete folder: table Ledger is delete-locked' )
447+ } )
448+
416449 it ( 'should return 401 for unauthenticated delete requests' , async ( ) => {
417450 mockUnauthenticated ( )
418451
@@ -458,7 +491,7 @@ describe('Individual Folder API Route', () => {
458491
459492 const data = await response . json ( )
460493 expect ( data ) . toHaveProperty ( 'success' , true )
461- expect ( mockPerformDeleteFolder ) . toHaveBeenCalled ( )
494+ expect ( mockDeleteFolder ) . toHaveBeenCalled ( )
462495 } )
463496
464497 it ( 'should allow folder deletion for admin permissions' , async ( ) => {
@@ -476,7 +509,7 @@ describe('Individual Folder API Route', () => {
476509
477510 const data = await response . json ( )
478511 expect ( data ) . toHaveProperty ( 'success' , true )
479- expect ( mockPerformDeleteFolder ) . toHaveBeenCalled ( )
512+ expect ( mockDeleteFolder ) . toHaveBeenCalled ( )
480513 } )
481514
482515 it ( 'should handle database errors during deletion' , async ( ) => {
0 commit comments