import { convexTest } from "convex-test"; import { anyApi } from "convex/server"; import { describe, expect, test } from "vitest"; import { requireOrganizationMember } from "./authz"; import schema from "./schema"; // `import.meta.glob` is provided by the Vitest/Vite test runner at runtime; it // has no type definition under the Convex tsconfig (which scopes `types` to // node), so declare the minimal shape the test relies on. declare global { interface ImportMeta { readonly glob: (pattern: string) => Record Promise>; } } const modules = import.meta.glob("./**/*.ts"); const api = anyApi; const ID_A = "https://convex.test|user-a"; const ID_B = "https://convex.test|user-b"; const identityA = { tokenIdentifier: ID_A }; const identityB = { tokenIdentifier: ID_B }; const newTest = () => convexTest({ modules, schema }); describe("organizations", () => { test("first ensure creates one organization and owner membership", async () => { const t = newTest(); const org = await t .withIdentity(identityA) .mutation(api.organizations.ensurePersonalOrganization, {}); expect(org.kind).toBe("personal"); expect(org.name).toBe("Personal"); expect(org.createdBy).toBe(ID_A); expect(org._id).toBeTruthy(); // Idempotent shape: a second ensure returns the same organization. const again = await t .withIdentity(identityA) .mutation(api.organizations.ensurePersonalOrganization, {}); expect(again._id).toBe(org._id); // The current-organization query reflects the ensured org. const current = await t .withIdentity(identityA) .query(api.organizations.getCurrent, {}); expect(current?._id).toBe(org._id); }); test("repeated ensure returns the same organization without duplicates", async () => { const t = newTest(); const first = await t .withIdentity(identityA) .mutation(api.organizations.ensurePersonalOrganization, {}); // Many subsequent ensures must all resolve to the single org. const repeats = await Promise.all( Array.from({ length: 5 }, () => t .withIdentity(identityA) .mutation(api.organizations.ensurePersonalOrganization, {}) ) ); for (const org of repeats) { expect(org._id).toBe(first._id); } // The current-organization query still reports the single org. const allForA = await t .withIdentity(identityA) .query(api.organizations.getCurrent, {}); expect(allForA?._id).toBe(first._id); }); test("unauthenticated access is denied", async () => { const t = newTest(); await expect(t.query(api.organizations.getCurrent, {})).rejects.toThrow( /Authentication required/u ); await expect( t.mutation(api.organizations.ensurePersonalOrganization, {}) ).rejects.toThrow(/Authentication required/u); }); test("a second identity cannot read or mutate the first organization", async () => { const t = newTest(); const orgA = await t .withIdentity(identityA) .mutation(api.organizations.ensurePersonalOrganization, {}); // The second identity sees none of the first identity's organizations. const currentForB = await t .withIdentity(identityB) .query(api.organizations.getCurrent, {}); expect(currentForB).toBeNull(); // The second identity gets its own distinct organization. const orgB = await t .withIdentity(identityB) .mutation(api.organizations.ensurePersonalOrganization, {}); expect(orgB._id).not.toBe(orgA._id); // The membership boundary denies the second identity access to org A. await expect( t .withIdentity(identityB) .query((ctx) => requireOrganizationMember(ctx, orgA._id)) ).rejects.toThrow(/Organization membership required/u); // But the first identity is a confirmed member of its own organization. await expect( t .withIdentity(identityA) .query((ctx) => requireOrganizationMember(ctx, orgA._id)) ).resolves.toBe(ID_A); }); });