mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 03:10:38 +09:00
100 lines
3 KiB
TypeScript
100 lines
3 KiB
TypeScript
|
|
import { randomUUID } from "node:crypto";
|
||
|
|
import { eq } from "drizzle-orm";
|
||
|
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||
|
|
import {
|
||
|
|
companies,
|
||
|
|
companyMemberships,
|
||
|
|
createDb,
|
||
|
|
principalPermissionGrants,
|
||
|
|
} from "@paperclipai/db";
|
||
|
|
import {
|
||
|
|
getEmbeddedPostgresTestSupport,
|
||
|
|
startEmbeddedPostgresTestDatabase,
|
||
|
|
} from "./helpers/embedded-postgres.js";
|
||
|
|
import { accessService } from "../services/access.js";
|
||
|
|
|
||
|
|
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
||
|
|
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
|
||
|
|
|
||
|
|
async function createCompanyWithOwner(db: ReturnType<typeof createDb>) {
|
||
|
|
const company = await db
|
||
|
|
.insert(companies)
|
||
|
|
.values({
|
||
|
|
name: `Access Service ${randomUUID()}`,
|
||
|
|
issuePrefix: `AS${randomUUID().slice(0, 6).toUpperCase()}`,
|
||
|
|
})
|
||
|
|
.returning()
|
||
|
|
.then((rows) => rows[0]!);
|
||
|
|
|
||
|
|
const owner = await db
|
||
|
|
.insert(companyMemberships)
|
||
|
|
.values({
|
||
|
|
companyId: company.id,
|
||
|
|
principalType: "user",
|
||
|
|
principalId: `owner-${randomUUID()}`,
|
||
|
|
status: "active",
|
||
|
|
membershipRole: "owner",
|
||
|
|
})
|
||
|
|
.returning()
|
||
|
|
.then((rows) => rows[0]!);
|
||
|
|
|
||
|
|
return { company, owner };
|
||
|
|
}
|
||
|
|
|
||
|
|
describeEmbeddedPostgres("access service", () => {
|
||
|
|
let db!: ReturnType<typeof createDb>;
|
||
|
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-access-service-");
|
||
|
|
db = createDb(tempDb.connectionString);
|
||
|
|
}, 20_000);
|
||
|
|
|
||
|
|
afterEach(async () => {
|
||
|
|
await db.delete(principalPermissionGrants);
|
||
|
|
await db.delete(companyMemberships);
|
||
|
|
await db.delete(companies);
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(async () => {
|
||
|
|
await tempDb?.cleanup();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("rejects combined access updates that would demote the last active owner", async () => {
|
||
|
|
const { company, owner } = await createCompanyWithOwner(db);
|
||
|
|
const access = accessService(db);
|
||
|
|
|
||
|
|
await expect(
|
||
|
|
access.updateMemberAndPermissions(
|
||
|
|
company.id,
|
||
|
|
owner.id,
|
||
|
|
{ membershipRole: "admin", grants: [] },
|
||
|
|
"admin-user",
|
||
|
|
),
|
||
|
|
).rejects.toThrow("Cannot remove the last active owner");
|
||
|
|
|
||
|
|
const unchanged = await db
|
||
|
|
.select()
|
||
|
|
.from(companyMemberships)
|
||
|
|
.where(eq(companyMemberships.id, owner.id))
|
||
|
|
.then((rows) => rows[0]!);
|
||
|
|
expect(unchanged.membershipRole).toBe("owner");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("rejects role-only updates that would suspend the last active owner", async () => {
|
||
|
|
const { company, owner } = await createCompanyWithOwner(db);
|
||
|
|
const access = accessService(db);
|
||
|
|
|
||
|
|
await expect(
|
||
|
|
access.updateMember(company.id, owner.id, { status: "suspended" }),
|
||
|
|
).rejects.toThrow("Cannot remove the last active owner");
|
||
|
|
|
||
|
|
const unchanged = await db
|
||
|
|
.select()
|
||
|
|
.from(companyMemberships)
|
||
|
|
.where(eq(companyMemberships.id, owner.id))
|
||
|
|
.then((rows) => rows[0]!);
|
||
|
|
expect(unchanged.status).toBe("active");
|
||
|
|
});
|
||
|
|
});
|