|
3 | 3 | * |
4 | 4 | * @vitest-environment node |
5 | 5 | */ |
6 | | -import { authMockFns, createMockRequest, permissionsMock, permissionsMockFns } from '@sim/testing' |
| 6 | + |
| 7 | +import { ResourceLockedError } from '@sim/platform-authz/resource-lock' |
| 8 | +import { |
| 9 | + authMockFns, |
| 10 | + createMockRequest, |
| 11 | + permissionsMock, |
| 12 | + permissionsMockFns, |
| 13 | + resourceLockMockFns, |
| 14 | +} from '@sim/testing' |
7 | 15 | import { drizzleOrmMock } from '@sim/testing/mocks' |
8 | 16 | import { beforeEach, describe, expect, it, vi } from 'vitest' |
9 | 17 |
|
@@ -43,6 +51,8 @@ describe('PUT /api/folders/reorder', () => { |
43 | 51 |
|
44 | 52 | beforeEach(() => { |
45 | 53 | vi.clearAllMocks() |
| 54 | + resourceLockMockFns.mockAssertFolderMutable.mockReset() |
| 55 | + resourceLockMockFns.mockAssertFolderMutable.mockResolvedValue(undefined) |
46 | 56 |
|
47 | 57 | authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } }) |
48 | 58 | mockGetUserEntityPermissions.mockResolvedValue('admin') |
@@ -257,6 +267,39 @@ describe('PUT /api/folders/reorder', () => { |
257 | 267 | expect(data.error).toBe('One or more folders were not found') |
258 | 268 | }) |
259 | 269 |
|
| 270 | + it('returns a 423 when a folder is concurrently locked before the write', async () => { |
| 271 | + // Regression test: the route checks lock state before calling |
| 272 | + // performReorderFolders (both of the route's own checks -- for update.id and |
| 273 | + // update.parentId, since parentId is `null` not `undefined` -- succeed below, |
| 274 | + // simulating the folder being unlocked at that moment), but that's a separate |
| 275 | + // round-trip -- an admin could lock the folder in the window between that |
| 276 | + // check and the transaction. The third assertFolderMutable call is the new |
| 277 | + // in-transaction recheck this test targets. |
| 278 | + mockWhere |
| 279 | + .mockReturnValueOnce([ |
| 280 | + { id: 'folder-1', workspaceId: 'workspace-123', resourceType: 'workflow' }, |
| 281 | + ]) |
| 282 | + .mockReturnValueOnce([{ id: 'folder-1', parentId: null }]) |
| 283 | + .mockReturnValueOnce([{ id: 'folder-1', workspaceId: 'workspace-123' }]) |
| 284 | + |
| 285 | + resourceLockMockFns.mockAssertFolderMutable |
| 286 | + .mockResolvedValueOnce(undefined) |
| 287 | + .mockResolvedValueOnce(undefined) |
| 288 | + .mockRejectedValueOnce(new ResourceLockedError('workflow', false, 'Folder is locked')) |
| 289 | + |
| 290 | + const req = createMockRequest('PUT', { |
| 291 | + workspaceId: 'workspace-123', |
| 292 | + updates: [{ id: 'folder-1', sortOrder: 2, parentId: null }], |
| 293 | + }) |
| 294 | + |
| 295 | + const response = await PUT(req) |
| 296 | + |
| 297 | + expect(response.status).toBe(423) |
| 298 | + expect(resourceLockMockFns.mockAssertFolderMutable).toHaveBeenCalledTimes(3) |
| 299 | + expect(mockDb.transaction).toHaveBeenCalledTimes(1) |
| 300 | + expect(mockTxUpdate).not.toHaveBeenCalled() |
| 301 | + }) |
| 302 | + |
260 | 303 | it('rejects a batch that would form a cycle', async () => { |
261 | 304 | mockWhere |
262 | 305 | .mockReturnValueOnce([ |
|
0 commit comments