|
| 1 | +/** |
| 2 | + * Folder reorder against real PostgreSQL with the transaction tripwire in `throw` mode: the lock |
| 3 | + * checks run inside the reorder transaction, so every query they issue must ride that |
| 4 | + * transaction's connection rather than check a second one out of the global pool. |
| 5 | + */ |
| 6 | +import { db } from '@sim/db' |
| 7 | +import { folder, permissions, user, workspace } from '@sim/db/schema' |
| 8 | +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' |
| 9 | +import { authMock, authMockFns, createMockRequest } from '@sim/testing' |
| 10 | +import { generateId } from '@sim/utils/id' |
| 11 | +import { eq, inArray } from 'drizzle-orm' |
| 12 | +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' |
| 13 | + |
| 14 | +vi.mock('@/lib/auth', () => authMock) |
| 15 | + |
| 16 | +import { PUT } from '@/app/api/folders/reorder/route' |
| 17 | + |
| 18 | +const userId = generateId() |
| 19 | +const workspaceId = generateId() |
| 20 | +const lockedRootId = generateId() |
| 21 | +const lockedChildId = generateId() |
| 22 | +const openRootId = generateId() |
| 23 | +const openChildId = generateId() |
| 24 | +const destinationId = generateId() |
| 25 | + |
| 26 | +function reorder(updates: Array<{ id: string; sortOrder: number; parentId?: string | null }>) { |
| 27 | + return PUT( |
| 28 | + createMockRequest('PUT', { workspaceId, resourceType: 'workflow', updates }), |
| 29 | + undefined |
| 30 | + ) |
| 31 | +} |
| 32 | + |
| 33 | +describe('PUT /api/folders/reorder in PostgreSQL', () => { |
| 34 | + beforeAll(async () => { |
| 35 | + const now = new Date() |
| 36 | + await db.insert(user).values({ |
| 37 | + id: userId, |
| 38 | + name: 'Folder reorder fixture', |
| 39 | + email: `${userId}@fixture.test`, |
| 40 | + emailVerified: true, |
| 41 | + createdAt: now, |
| 42 | + updatedAt: now, |
| 43 | + }) |
| 44 | + await insertWorkspaceFixture(db, { |
| 45 | + id: workspaceId, |
| 46 | + name: 'Folder reorder fixture', |
| 47 | + ownerId: userId, |
| 48 | + billedAccountUserId: userId, |
| 49 | + }) |
| 50 | + await db.insert(permissions).values({ |
| 51 | + id: generateId(), |
| 52 | + userId, |
| 53 | + entityType: 'workspace', |
| 54 | + entityId: workspaceId, |
| 55 | + permissionType: 'admin', |
| 56 | + }) |
| 57 | + const base = { resourceType: 'workflow' as const, userId, workspaceId } |
| 58 | + await db.insert(folder).values([ |
| 59 | + { ...base, id: lockedRootId, name: 'Locked root', locked: true }, |
| 60 | + { ...base, id: openRootId, name: 'Open root' }, |
| 61 | + { ...base, id: destinationId, name: 'Destination' }, |
| 62 | + ]) |
| 63 | + await db.insert(folder).values([ |
| 64 | + { ...base, id: lockedChildId, name: 'Locked child', parentId: lockedRootId }, |
| 65 | + { ...base, id: openChildId, name: 'Open child', parentId: openRootId }, |
| 66 | + ]) |
| 67 | + authMockFns.mockGetSession.mockResolvedValue({ user: { id: userId } }) |
| 68 | + }) |
| 69 | + |
| 70 | + afterAll(async () => { |
| 71 | + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) |
| 72 | + await db.delete(user).where(eq(user.id, userId)) |
| 73 | + }) |
| 74 | + |
| 75 | + it('reparents and reorders unlocked folders', async () => { |
| 76 | + const response = await reorder([ |
| 77 | + { id: openChildId, sortOrder: 3, parentId: destinationId }, |
| 78 | + { id: openRootId, sortOrder: 1 }, |
| 79 | + ]) |
| 80 | + |
| 81 | + expect(response.status).toBe(200) |
| 82 | + expect(await response.json()).toEqual({ success: true, updated: 2 }) |
| 83 | + const rows = await db |
| 84 | + .select({ id: folder.id, parentId: folder.parentId, sortOrder: folder.sortOrder }) |
| 85 | + .from(folder) |
| 86 | + .where(inArray(folder.id, [openChildId, openRootId])) |
| 87 | + expect(rows).toEqual( |
| 88 | + expect.arrayContaining([ |
| 89 | + { id: openChildId, parentId: destinationId, sortOrder: 3 }, |
| 90 | + { id: openRootId, parentId: null, sortOrder: 1 }, |
| 91 | + ]) |
| 92 | + ) |
| 93 | + }) |
| 94 | + |
| 95 | + it('refuses to move a folder that inherits a lock from its ancestor', async () => { |
| 96 | + const response = await reorder([{ id: lockedChildId, sortOrder: 0, parentId: destinationId }]) |
| 97 | + |
| 98 | + expect(response.status).toBe(423) |
| 99 | + expect(await response.json()).toEqual({ error: 'Folder is locked by an ancestor folder' }) |
| 100 | + const [row] = await db |
| 101 | + .select({ parentId: folder.parentId }) |
| 102 | + .from(folder) |
| 103 | + .where(eq(folder.id, lockedChildId)) |
| 104 | + expect(row.parentId).toBe(lockedRootId) |
| 105 | + }) |
| 106 | + |
| 107 | + it('refuses to move a folder into a locked destination', async () => { |
| 108 | + const response = await reorder([{ id: destinationId, sortOrder: 0, parentId: lockedRootId }]) |
| 109 | + |
| 110 | + expect(response.status).toBe(423) |
| 111 | + expect(await response.json()).toEqual({ error: 'Folder is locked' }) |
| 112 | + }) |
| 113 | +}) |
0 commit comments