Open History to see the last commit
1import {
2 GovernanceRoleSafetyError,
3 governanceRoleTransition,
4 requireGovernanceBaseline,
5 validateGovernanceActor,
6 type GovernanceRoleBaseline,
7} from "@alchemy.run/neon/OrganizationMemberRole";
8import { Project } from "@alchemy.run/neon/Project";
9import {
10 ProjectMemberRole,
11 projectMemberDirectRole,
12 validateProjectGovernanceRole,
13 type ProjectGovernanceRole,
14} from "@alchemy.run/neon/ProjectMemberRole";
15import { providers } from "@alchemy.run/neon/Providers";
16import { expect, test as unit } from "@alchemy.run/test";
17import * as Test from "@alchemy.run/test/Alchemy";
18import * as SDK from "@distilled.cloud/neon";
19import { adopt } from "alchemy/AdoptPolicy";
20import * as Effect from "effect/Effect";
21
22const member = {
23 member_id: "authorized-member",
24 user_id: "authorized-user",
25 org_role: "editor" as const,
26};
27const refused = <A, R>(effect: Effect.Effect<A, GovernanceRoleSafetyError, R>) =>
28 effect.pipe(
29 Effect.as(false),
30 Effect.catchTag("NeonGovernanceRoleSafetyError", () => Effect.succeed(true)),
31 );
32
33unit.effect(
34 "project role state distinguishes explicit grants from inherited effective access",
35 () =>
36 Effect.gen(function* () {
37 expect(
38 yield* projectMemberDirectRole({
39 ...member,
40 grant_source: "org_role_default",
41 project_role: "editor",
42 effective_project_permission: "EDITOR",
43 org_default_project_permission: "EDITOR",
44 }),
45 ).toBeNull();
46 expect(yield* projectMemberDirectRole({ ...member, grant_source: "unassigned" })).toBeNull();
47 expect(
48 yield* projectMemberDirectRole({
49 ...member,
50 grant_source: "explicit",
51 explicit_project_permission: "VIEWER",
52 project_role: "editor",
53 effective_project_permission: "EDITOR",
54 }),
55 ).toBe("viewer");
56 expect(
57 yield* projectMemberDirectRole({
58 ...member,
59 grant_source: "explicit",
60 explicit_project_permission: "EDITOR",
61 }),
62 ).toBe("editor");
63 expect(
64 yield* projectMemberDirectRole({
65 ...member,
66 grant_source: "explicit",
67 explicit_project_permission: "ADMIN",
68 }),
69 ).toBe("admin");
70 }),
71 { tags: ["unit", "provider:neon", "provider:neon:projectmemberrole", "local"] },
72);
73
74unit.effect(
75 "project role state refuses missing, inconsistent and org-admin grant evidence",
76 () =>
77 Effect.gen(function* () {
78 expect(yield* refused(projectMemberDirectRole(member))).toBe(true);
79 expect(yield* refused(projectMemberDirectRole({ ...member, grant_source: "explicit" }))).toBe(
80 true,
81 );
82 expect(
83 yield* refused(
84 projectMemberDirectRole({
85 ...member,
86 grant_source: "org_role_default",
87 explicit_project_permission: "VIEWER",
88 }),
89 ),
90 ).toBe(true);
91 expect(
92 yield* refused(
93 projectMemberDirectRole({
94 ...member,
95 org_role: "admin",
96 grant_source: "org_admin_override",
97 project_role: "admin",
98 }),
99 ),
100 ).toBe(true);
101 expect(
102 yield* refused(
103 projectMemberDirectRole({
104 ...member,
105 grant_source: "org_admin_override",
106 explicit_project_permission: "VIEWER",
107 }),
108 ),
109 ).toBe(true);
110 }),
111 {
112 tags: [
113 "unit",
114 "provider:neon",
115 "provider:neon:organizationmemberrole",
116 "provider:neon:projectmemberrole",
117 "local",
118 ],
119 },
120);
121
122unit.effect(
123 "project restoration removes only an owned direct grant and preserves adopted roles",
124 () =>
125 Effect.gen(function* () {
126 const baseline: GovernanceRoleBaseline<ProjectGovernanceRole | null> = {
127 fqn: "ProjectRole",
128 instanceId: "generation",
129 userId: member.user_id,
130 originalRole: null,
131 managedRole: "editor",
132 };
133 expect(yield* governanceRoleTransition(baseline, "editor", null, true)).toBe(true);
134 expect(yield* governanceRoleTransition(baseline, null, null, true)).toBe(false);
135 expect(yield* refused(governanceRoleTransition(baseline, "viewer", null, true))).toBe(true);
136 const adopted = { ...baseline, originalRole: "viewer" as const };
137 expect(yield* governanceRoleTransition(adopted, "editor", "viewer", true)).toBe(true);
138 expect(yield* governanceRoleTransition(adopted, "viewer", "viewer", true)).toBe(false);
139 expect(yield* refused(governanceRoleTransition(adopted, null, "viewer", true))).toBe(true);
140 expect(
141 yield* refused(
142 requireGovernanceBaseline<ProjectGovernanceRole | null>(undefined, baseline),
143 ),
144 ).toBe(true);
145 }),
146 {
147 tags: [
148 "unit",
149 "provider:neon",
150 "provider:neon:organizationmemberrole",
151 "provider:neon:projectmemberrole",
152 "local",
153 ],
154 },
155);
156
157unit.effect(
158 "project role validation rejects unknown persisted permissions",
159 () =>
160 Effect.gen(function* () {
161 for (const role of [null, "viewer", "editor", "admin"] as const)
162 yield* validateProjectGovernanceRole(role);
163 // @ts-expect-error Runtime validation protects JavaScript callers and persisted state.
164 expect(yield* refused(validateProjectGovernanceRole("owner"))).toBe(true);
165 }),
166 {
167 tags: [
168 "unit",
169 "provider:neon",
170 "provider:neon:organizationmemberrole",
171 "provider:neon:projectmemberrole",
172 "local",
173 ],
174 },
175);
176
177const { test } = Test.make({ providers: providers() });
178
179const baseline: GovernanceRoleBaseline<ProjectGovernanceRole | null> = {
180 fqn: "ProjectRole",
181 instanceId: "project-role-generation",
182 userId: member.user_id,
183 originalRole: null,
184 managedRole: "viewer",
185};
186const context = {
187 id: "ProjectRole",
188 fqn: baseline.fqn,
189 instanceId: baseline.instanceId,
190 oldBindings: [],
191 newBindings: [],
192 bindings: [],
193 session: { note: () => Effect.void },
194};
195const noCloudCredentials = Effect.die("Safety-only governance tests must not issue Neon requests");
196
197test.provider(
198 "project grant identity changes are refused by diff before cloud I/O",
199 () =>
200 Effect.gen(function* () {
201 const provider = yield* ProjectMemberRole.Provider;
202 const olds = {
203 orgId: "org-original",
204 memberId: "member-original",
205 project: { projectId: "project-original" },
206 role: "viewer" as const,
207 };
208 const owned = {
209 orgId: olds.orgId,
210 memberId: olds.memberId,
211 projectId: olds.project.projectId,
212 role: olds.role,
213 baseline,
214 };
215 for (const output of [undefined, owned]) {
216 for (const news of [
217 { ...olds, orgId: "org-other" },
218 { ...olds, memberId: "member-other" },
219 { ...olds, project: { projectId: "project-other" } },
220 ]) {
221 expect(yield* refused(provider.diff!({ ...context, olds, news, output }))).toBe(true);
222 }
223 expect(
224 yield* provider.diff!({ ...context, olds, news: { ...olds, role: "editor" }, output }),
225 ).toBeUndefined();
226 }
227 }).pipe(Effect.provideService(SDK.Credentials, noCloudCredentials)),
228 {
229 tags: [
230 "provider:neon",
231 "provider:neon:organizationmemberrole",
232 "provider:neon:projectmemberrole",
233 "live",
234 ],
235 timeout: 120_000,
236 },
237);
238
239test.provider(
240 "project grant read reconcile and delete reject missing baselines before cloud I/O",
241 () =>
242 Effect.gen(function* () {
243 const provider = yield* ProjectMemberRole.Provider;
244 const news = {
245 orgId: "org-original",
246 memberId: "member-original",
247 project: { projectId: "project-original" },
248 role: "viewer" as const,
249 };
250 for (const olds of [undefined, news]) {
251 expect(
252 yield* refused(provider.reconcile({ ...context, news, olds, output: undefined })),
253 ).toBe(true);
254 }
255 const output = {
256 orgId: news.orgId,
257 memberId: news.memberId,
258 projectId: news.project.projectId,
259 role: news.role,
260 baseline,
261 };
262 yield* Effect.sync(() => Reflect.deleteProperty(output, "baseline"));
263 expect(yield* refused(provider.read!({ ...context, olds: news, output }))).toBe(true);
264 expect(yield* refused(provider.reconcile({ ...context, news, olds: news, output }))).toBe(
265 true,
266 );
267 expect(yield* refused(provider.delete({ ...context, olds: news, output }))).toBe(true);
268 }).pipe(Effect.provideService(SDK.Credentials, noCloudCredentials)),
269 {
270 tags: [
271 "provider:neon",
272 "provider:neon:organizationmemberrole",
273 "provider:neon:projectmemberrole",
274 "live",
275 ],
276 timeout: 120_000,
277 },
278);
279
280const orgId = process.env.NEON_GOVERNANCE_TEST_ORG_ID;
281const memberId = process.env.NEON_GOVERNANCE_TEST_MEMBER_ID;
282
283test.provider.skipIf(!orgId || !memberId)(
284 "live direct project grants restore absence and an adopted explicit grant on a test-owned project",
285 (stack) =>
286 Effect.gen(function* () {
287 yield* stack.destroy();
288 if (!orgId || !memberId)
289 return yield* new GovernanceRoleSafetyError({
290 message: "Explicit governance fixture authorization is required",
291 });
292 const membership = yield* SDK.getOrganizationMember({ org_id: orgId, member_id: memberId });
293 if (membership.role === "admin" || !membership.joined_at)
294 return yield* new GovernanceRoleSafetyError({
295 message: "Governance test fixture must be an active, non-admin member",
296 });
297 yield* validateGovernanceActor(membership.user_id, (yield* SDK.getCurrentUserInfo({})).id);
298 const base = Effect.gen(function* () {
299 const project = yield* Project("GovernanceProject", { orgId, region: "aws-us-east-2" });
300 return { project };
301 });
302 const initial = yield* stack.deploy(base);
303 const projectId = initial.project.projectId;
304 const request = { project_id: projectId, member_id: memberId };
305 const observeGrant = Effect.gen(function* () {
306 let cursor: string | undefined;
307 for (let page = 0; page < 10; page++) {
308 const response = yield* SDK.listProjectMembers({
309 project_id: projectId,
310 limit: 500,
311 cursor,
312 });
313 const current = response.project_members.find((entry) => entry.member_id === memberId);
314 if (current) return yield* projectMemberDirectRole(current);
315 if (!response.pagination?.next) break;
316 cursor = response.pagination.next;
317 }
318 return yield* new GovernanceRoleSafetyError({
319 message:
320 "Authorized fixture member is not visible; project-role management and ADMIN access are required",
321 });
322 });
323 expect(yield* observeGrant).toBeNull();
324 const application = (role: ProjectGovernanceRole, takeOwnership = false) =>
325 Effect.gen(function* () {
326 yield* base;
327 const access = yield* ProjectMemberRole("AuthorizedProjectGrant", {
328 orgId,
329 memberId,
330 project: { projectId },
331 role,
332 }).pipe(adopt(takeOwnership));
333 return { access };
334 });
335 const created = yield* stack.deploy(application("viewer"));
336 expect(created.access.baseline.originalRole).toBeNull();
337 expect(yield* observeGrant).toBe("viewer");
338 const updated = yield* stack.deploy(application("editor"));
339 expect(updated.access.baseline.originalRole).toBeNull();
340 expect(yield* observeGrant).toBe("editor");
341 yield* stack.deploy(base);
342 expect(yield* observeGrant).toBeNull();
343
344 yield* SDK.setProjectMemberRole({ ...request, role: "viewer", confirm_self_demotion: false });
345 expect(
346 yield* stack.deploy(application("editor")).pipe(
347 Effect.as(false),
348 Effect.catchTag("OwnedBySomeoneElse", () => Effect.succeed(true)),
349 ),
350 ).toBe(true);
351 const adopted = yield* stack.deploy(application("editor", true));
352 expect(adopted.access.baseline.originalRole).toBe("viewer");
353 expect(yield* observeGrant).toBe("editor");
354 yield* stack.deploy(base);
355 expect(yield* observeGrant).toBe("viewer");
356 expect((yield* SDK.getOrganizationMember({ org_id: orgId, member_id: memberId })).role).toBe(
357 membership.role,
358 );
359 yield* stack.destroy();
360 expect(
361 yield* SDK.getProject({ project_id: projectId }).pipe(
362 Effect.as(false),
363 Effect.catchTag("NotFound", () => Effect.succeed(true)),
364 ),
365 ).toBe(true);
366 yield* stack.destroy();
367 }),
368 {
369 tags: [
370 "provider:neon",
371 "provider:neon:organizationmemberrole",
372 "provider:neon:project",
373 "provider:neon:projectmemberrole",
374 "live",
375 ],
376 timeout: 120_000,
377 },
378);