Skip to content

Commit a4a15d9

Browse files
committed
test(webapp): cover the SSO entitlement gate on directory sync
Locks in the three branches of the new gate: effects apply for an entitled org, are skipped for one without the entitlement (provision and deprovision alike, so revocation can't remove members), and an unreadable entitlement throws a warn-level retryable error rather than silently dropping a directory change. Also covers per-org memoisation across a batch and that one unentitled org doesn't block another. Picks up @trigger.dev/platform 1.3.0, which is what makes limits.hasSso visible to the webapp at all.
1 parent d6f9224 commit a4a15d9

2 files changed

Lines changed: 135 additions & 5 deletions

File tree

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import type { DirectorySyncEffect } from "@trigger.dev/plugins";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
4+
vi.mock("~/db.server", () => ({ prisma: {}, $replica: {} }));
5+
vi.mock("~/services/platformNotifications.server", () => ({
6+
createPlatformNotification: vi.fn(),
7+
}));
8+
9+
const getSsoEntitlement = vi.fn();
10+
vi.mock("~/services/platform.v3.server", async (importOriginal) => {
11+
const actual = (await importOriginal()) as Record<string, unknown>;
12+
return { ...actual, getSsoEntitlement: (orgId: string) => getSsoEntitlement(orgId) };
13+
});
14+
15+
const setUserRole = vi.fn();
16+
vi.mock("~/services/rbac.server", () => ({
17+
rbac: { setUserRole: (a: unknown) => setUserRole(a) },
18+
}));
19+
20+
const ensureOrgMember = vi.fn();
21+
const ensureUserForDirectory = vi.fn();
22+
const removeOrgMemberForDirectory = vi.fn();
23+
vi.mock("~/models/orgMember.server", () => ({
24+
ensureOrgMember: (a: unknown) => ensureOrgMember(a),
25+
ensureUserForDirectory: (a: unknown) => ensureUserForDirectory(a),
26+
removeOrgMemberForDirectory: (a: unknown) => removeOrgMemberForDirectory(a),
27+
}));
28+
29+
import { applyDirectorySyncEffects } from "~/services/directorySyncEffects.server";
30+
31+
const ENTITLED_ORG = "org_entitled";
32+
const UNENTITLED_ORG = "org_unentitled";
33+
34+
function provision(organizationId: string, email = "someone@acme.com"): DirectorySyncEffect {
35+
return {
36+
kind: "provision",
37+
userId: "user_1",
38+
email,
39+
firstName: null,
40+
lastName: null,
41+
organizationId,
42+
roleId: null,
43+
};
44+
}
45+
46+
function deprovision(organizationId: string): DirectorySyncEffect {
47+
return { kind: "deprovision", userId: "user_1", organizationId };
48+
}
49+
50+
describe("applyDirectorySyncEffects — SSO entitlement gate", () => {
51+
beforeEach(() => {
52+
vi.clearAllMocks();
53+
ensureOrgMember.mockResolvedValue(undefined);
54+
removeOrgMemberForDirectory.mockResolvedValue({ removed: true });
55+
setUserRole.mockResolvedValue({ ok: true });
56+
});
57+
58+
it("applies effects for an entitled org", async () => {
59+
getSsoEntitlement.mockResolvedValue("entitled");
60+
61+
await applyDirectorySyncEffects([provision(ENTITLED_ORG)]);
62+
63+
expect(ensureOrgMember).toHaveBeenCalledTimes(1);
64+
expect(ensureOrgMember).toHaveBeenCalledWith(
65+
expect.objectContaining({ organizationId: ENTITLED_ORG, source: "directory_sync" })
66+
);
67+
});
68+
69+
it("skips provisioning for an org without the entitlement", async () => {
70+
getSsoEntitlement.mockResolvedValue("not_entitled");
71+
72+
await applyDirectorySyncEffects([provision(UNENTITLED_ORG)]);
73+
74+
expect(ensureOrgMember).not.toHaveBeenCalled();
75+
expect(ensureUserForDirectory).not.toHaveBeenCalled();
76+
});
77+
78+
it("skips deprovisioning too, so revocation cannot remove members", async () => {
79+
getSsoEntitlement.mockResolvedValue("not_entitled");
80+
81+
await applyDirectorySyncEffects([deprovision(UNENTITLED_ORG)]);
82+
83+
expect(removeOrgMemberForDirectory).not.toHaveBeenCalled();
84+
});
85+
86+
it("throws on an unreadable entitlement so the worker retries", async () => {
87+
getSsoEntitlement.mockResolvedValue("unknown");
88+
89+
await expect(applyDirectorySyncEffects([provision(ENTITLED_ORG)])).rejects.toThrow(
90+
/could not read the SSO entitlement/
91+
);
92+
93+
expect(ensureOrgMember).not.toHaveBeenCalled();
94+
});
95+
96+
it("marks the retry as a warning rather than a pageable error", async () => {
97+
getSsoEntitlement.mockResolvedValue("unknown");
98+
99+
await applyDirectorySyncEffects([provision(ENTITLED_ORG)]).then(
100+
() => expect.unreachable("should have thrown"),
101+
(error) => expect(error).toMatchObject({ logLevel: "warn" })
102+
);
103+
});
104+
105+
it("resolves the entitlement once per org across a batch", async () => {
106+
getSsoEntitlement.mockResolvedValue("entitled");
107+
108+
await applyDirectorySyncEffects([
109+
provision(ENTITLED_ORG, "a@acme.com"),
110+
provision(ENTITLED_ORG, "b@acme.com"),
111+
provision(ENTITLED_ORG, "c@acme.com"),
112+
]);
113+
114+
expect(getSsoEntitlement).toHaveBeenCalledTimes(1);
115+
expect(ensureOrgMember).toHaveBeenCalledTimes(3);
116+
});
117+
118+
it("gates per org, so one unentitled org does not block another", async () => {
119+
getSsoEntitlement.mockImplementation(async (orgId: string) =>
120+
orgId === ENTITLED_ORG ? "entitled" : "not_entitled"
121+
);
122+
123+
await applyDirectorySyncEffects([provision(UNENTITLED_ORG), provision(ENTITLED_ORG)]);
124+
125+
expect(ensureOrgMember).toHaveBeenCalledTimes(1);
126+
expect(ensureOrgMember).toHaveBeenCalledWith(
127+
expect.objectContaining({ organizationId: ENTITLED_ORG })
128+
);
129+
});
130+
});

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)