forked from EduCraft/curriculum-project-hub
fix: enforce active organization boundary
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { mkdtemp, rm, stat } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { chmod, mkdtemp, readdir, rm, stat } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_ORG_ID, prisma, resetDb, seedTestOrganization } from "./helpers.js";
|
||||
import {
|
||||
archiveFeishuChatBinding,
|
||||
@@ -9,17 +9,21 @@ import {
|
||||
createFolder,
|
||||
createProjectFromFeishuChat,
|
||||
createProjectFromOrgAdmin,
|
||||
moveProjectToFolder,
|
||||
setMembersCanCreateProjects,
|
||||
} from "../../src/projectOnboarding.js";
|
||||
|
||||
const workspaceRoots: string[] = [];
|
||||
const itAsNonRoot = typeof process.getuid === "function" && process.getuid() === 0 ? it.skip : it;
|
||||
|
||||
describe("ADR-0021 project onboarding", () => {
|
||||
beforeEach(async () => {
|
||||
await dropPermissionSettingsFailureTrigger();
|
||||
await resetDb();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await dropPermissionSettingsFailureTrigger();
|
||||
while (workspaceRoots.length > 0) {
|
||||
const root = workspaceRoots.pop();
|
||||
if (root !== undefined) await rm(root, { recursive: true, force: true });
|
||||
@@ -27,6 +31,7 @@ describe("ADR-0021 project onboarding", () => {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await dropPermissionSettingsFailureTrigger();
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
@@ -111,6 +116,98 @@ describe("ADR-0021 project onboarding", () => {
|
||||
})).rejects.toThrow(/members cannot create projects/);
|
||||
});
|
||||
|
||||
it.each(["SUSPENDED", "ARCHIVED"] as const)(
|
||||
"blocks project creation and organization mutations when the org is %s",
|
||||
async (status) => {
|
||||
await seedUser(`u-${status}`, `ou_${status}`, "OWNER");
|
||||
const folder = await createFolder(prisma, {
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
name: `Before ${status}`,
|
||||
});
|
||||
const workspaceRoot = await tempWorkspaceRoot();
|
||||
const project = await createProjectFromOrgAdmin(prisma, {
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
actorFeishuOpenId: `ou_${status}`,
|
||||
name: `Existing ${status}`,
|
||||
workspaceRoot,
|
||||
});
|
||||
const organizationWorkspace = dirname(project.workspaceDir);
|
||||
const workspacesBefore = (await readdir(organizationWorkspace)).sort();
|
||||
await prisma.organization.update({ where: { id: DEFAULT_ORG_ID }, data: { status } });
|
||||
|
||||
await expect(createProjectFromOrgAdmin(prisma, {
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
actorFeishuOpenId: `ou_${status}`,
|
||||
name: "Blocked",
|
||||
workspaceRoot,
|
||||
})).rejects.toThrow(`organization ${DEFAULT_ORG_ID} is ${status}`);
|
||||
await expect(readdir(organizationWorkspace)).resolves.toEqual(workspacesBefore);
|
||||
await expect(bindFeishuChatToProject(prisma, {
|
||||
projectId: project.projectId,
|
||||
actorFeishuOpenId: `ou_${status}`,
|
||||
chatId: `chat-${status}`,
|
||||
})).rejects.toThrow(`organization ${DEFAULT_ORG_ID} is ${status}`);
|
||||
await expect(createFolder(prisma, {
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
name: "Blocked folder",
|
||||
})).rejects.toThrow(`organization ${DEFAULT_ORG_ID} is ${status}`);
|
||||
await expect(setMembersCanCreateProjects(prisma, {
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
enabled: false,
|
||||
})).rejects.toThrow(`organization ${DEFAULT_ORG_ID} is ${status}`);
|
||||
await expect(moveProjectToFolder(prisma, {
|
||||
projectId: project.projectId,
|
||||
folderId: folder.id,
|
||||
})).rejects.toThrow(`organization ${DEFAULT_ORG_ID} is ${status}`);
|
||||
},
|
||||
);
|
||||
|
||||
it("removes a newly created workspace when the database transaction later fails", async () => {
|
||||
await seedUser("u-compensate", "ou_compensate", "ADMIN");
|
||||
await installPermissionSettingsFailureTrigger(0);
|
||||
const workspaceRoot = await tempWorkspaceRoot();
|
||||
|
||||
await expect(createProjectFromOrgAdmin(prisma, {
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
actorFeishuOpenId: "ou_compensate",
|
||||
name: "Must Roll Back",
|
||||
workspaceRoot,
|
||||
})).rejects.toThrow(/forced permission settings failure/);
|
||||
|
||||
await expect(readdir(join(workspaceRoot, "test-default"))).resolves.toEqual([]);
|
||||
await expect(prisma.project.count()).resolves.toBe(0);
|
||||
});
|
||||
|
||||
itAsNonRoot("surfaces both the transaction and workspace cleanup failures", async () => {
|
||||
await seedUser("u-cleanup-failure", "ou_cleanup_failure", "ADMIN");
|
||||
await installPermissionSettingsFailureTrigger(1);
|
||||
const workspaceRoot = await tempWorkspaceRoot();
|
||||
const organizationWorkspace = join(workspaceRoot, "test-default");
|
||||
const pending = createProjectFromOrgAdmin(prisma, {
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
actorFeishuOpenId: "ou_cleanup_failure",
|
||||
name: "Cleanup Must Fail Loudly",
|
||||
workspaceRoot,
|
||||
});
|
||||
const outcome = pending.then(
|
||||
() => null,
|
||||
(error: unknown) => error,
|
||||
);
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
expect(await readdir(organizationWorkspace)).toHaveLength(1);
|
||||
}, { timeout: 2_000 });
|
||||
await chmod(organizationWorkspace, 0o500);
|
||||
try {
|
||||
const error = await outcome;
|
||||
expect(error).toBeInstanceOf(AggregateError);
|
||||
expect((error as AggregateError).errors).toHaveLength(2);
|
||||
await expect(prisma.project.count()).resolves.toBe(0);
|
||||
} finally {
|
||||
await chmod(organizationWorkspace, 0o700);
|
||||
}
|
||||
});
|
||||
|
||||
it("binds an existing project once, archives the binding, then allows a new active binding", async () => {
|
||||
await seedUser("u-owner", "ou_owner", "OWNER");
|
||||
const workspaceRoot = await tempWorkspaceRoot();
|
||||
@@ -176,3 +273,28 @@ async function tempWorkspaceRoot(): Promise<string> {
|
||||
workspaceRoots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
async function installPermissionSettingsFailureTrigger(delaySeconds: number): Promise<void> {
|
||||
await dropPermissionSettingsFailureTrigger();
|
||||
await prisma.$executeRawUnsafe(`
|
||||
CREATE FUNCTION cph_test_fail_permission_settings() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
PERFORM pg_sleep(${delaySeconds});
|
||||
RAISE EXCEPTION 'forced permission settings failure';
|
||||
END;
|
||||
$$
|
||||
`);
|
||||
await prisma.$executeRawUnsafe(`
|
||||
CREATE TRIGGER cph_test_fail_permission_settings
|
||||
BEFORE INSERT ON "PermissionSettings"
|
||||
FOR EACH ROW EXECUTE FUNCTION cph_test_fail_permission_settings()
|
||||
`);
|
||||
}
|
||||
|
||||
async function dropPermissionSettingsFailureTrigger(): Promise<void> {
|
||||
await prisma.$executeRawUnsafe(
|
||||
'DROP TRIGGER IF EXISTS cph_test_fail_permission_settings ON "PermissionSettings"',
|
||||
);
|
||||
await prisma.$executeRawUnsafe('DROP FUNCTION IF EXISTS cph_test_fail_permission_settings()');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user