1import { adopt } from "@/AdoptPolicy.ts";
2import { Project } from "@/Neon/Project.ts";
3import {
4 ProjectVPCEndpoint,
5 validateProjectVPCEndpoint,
6} from "@/Neon/ProjectVPCEndpoint.ts";
7import { providers } from "@/Neon/Providers.ts";
8import * as Output from "@/Output.ts";
9import * as Test from "@/Test/Alchemy";
10import * as SDK from "@distilled.cloud/neon";
11import { expect } from "alchemy-test";
12import * as Effect from "effect/Effect";
13import * as Result from "effect/Result";
14
15const { test } = Test.make({ providers: providers() });
16const endpoint = {
17 orgId: "org-fixture",
18 regionId: "aws-us-east-2",
19 vpcEndpointId: "vpce-0123456789abcdef0",
20};
21const props = {
22 project: { projectId: "fixture-project" },
23 endpoint,
24 label: "Fixture",
25};
26const context = {
27 id: "Restriction",
28 fqn: "Restriction",
29 instanceId: "restriction-validation",
30 oldBindings: [],
31 newBindings: [],
32};
33
34test(
35 "project VPC restriction requires a project and valid organization endpoint scope",
36 Effect.gen(function* () {
37 yield* validateProjectVPCEndpoint(props);
38 expect(
39 Result.isFailure(
40 yield* validateProjectVPCEndpoint({
41 ...props,
42 project: { projectId: "" },
43 }).pipe(Effect.result),
44 ),
45 ).toBe(true);
46 expect(
47 Result.isFailure(
48 yield* validateProjectVPCEndpoint({
49 ...props,
50 endpoint: { ...endpoint, regionId: "azure-eastus2" },
51 }).pipe(Effect.result),
52 ),
53 ).toBe(true);
54 }),
55 { tags: ["provider:neon", "provider:neon:projectvpcendpoint", "live"] },
56);
57
58test.provider(
59 "project restriction replaces all changed or unresolved scopes with old-grant cleanup first",
60 () =>
61 Effect.gen(function* () {
62 const provider = yield* ProjectVPCEndpoint.Provider;
63 for (const patch of [
64 { project: { projectId: "other-project" } },
65 { project: { projectId: Output.literal("other-project") } },
66 { endpoint: { ...endpoint, orgId: "org-other" } },
67 { endpoint: { ...endpoint, regionId: "aws-us-west-2" } },
68 { endpoint: { ...endpoint, vpcEndpointId: "vpce-abcdef01234567890" } },
69 {
70 endpoint: { ...endpoint, regionId: Output.literal("aws-us-west-2") },
71 },
72 ]) {
73 expect(
74 yield* provider.diff!({
75 ...context,
76 olds: props,
77 news: { ...props, ...patch },
78 output: undefined,
79 }),
80 ).toEqual({ action: "replace", deleteFirst: true });
81 }
82 for (const news of [Output.literal(props), Effect.succeed(props)]) {
83 expect(
84 yield* provider.diff!({
85 ...context,
86 olds: props,
87 news,
88 output: undefined,
89 }),
90 ).toEqual({ action: "replace", deleteFirst: true });
91 }
92 expect(
93 yield* provider.diff!({
94 ...context,
95 olds: props,
96 news: { ...props, label: Output.literal("Updated") },
97 output: undefined,
98 }),
99 ).toBeUndefined();
100 }).pipe(
101 Effect.provideService(
102 SDK.Credentials,
103 Effect.die("Unexpected Neon request in a no-I/O guard test"),
104 ),
105 ),
106 { tags: ["provider:neon", "provider:neon:projectvpcendpoint", "live"] },
107);
108
109test.provider(
110 "project restriction rejects a cached grant from another project before I/O",
111 () =>
112 Effect.gen(function* () {
113 const provider = yield* ProjectVPCEndpoint.Provider;
114 expect(
115 yield* provider
116 .reconcile({
117 ...context,
118 bindings: [],
119 session: {
120 emit: () => Effect.void,
121 done: () => Effect.void,
122 note: () => Effect.void,
123 },
124 olds: undefined,
125 news: { ...props, project: { projectId: "other-project" } },
126 output: {
127 ...endpoint,
128 projectId: props.project.projectId,
129 label: props.label,
130 initialLabel: props.label,
131 managedLabel: props.label,
132 },
133 })
134 .pipe(
135 Effect.as(false),
136 Effect.catchTag("InvalidProjectVPCEndpoint", () =>
137 Effect.succeed(true),
138 ),
139 ),
140 ).toBe(true);
141 }).pipe(
142 Effect.provideService(
143 SDK.Credentials,
144 Effect.die("Unexpected Neon request in a no-I/O guard test"),
145 ),
146 ),
147 { tags: ["provider:neon", "provider:neon:projectvpcendpoint", "live"] },
148);
149
150const orgId = process.env.NEON_GOVERNANCE_TEST_ORG_ID;
151const regionId = process.env.NEON_GOVERNANCE_TEST_VPC_ENDPOINT_REGION;
152const vpcEndpointId = process.env.NEON_GOVERNANCE_TEST_VPC_ENDPOINT_ID;
153const region = (
154 [
155 "aws-us-east-1",
156 "aws-us-east-2",
157 "aws-us-west-2",
158 "aws-eu-central-1",
159 "aws-eu-west-2",
160 "aws-ap-southeast-1",
161 "aws-ap-southeast-2",
162 "aws-sa-east-1",
163 ] as const
164).find((value) => value === regionId);
165const enabled =
166 !!orgId &&
167 !!region &&
168 !!vpcEndpointId &&
169 process.env.NEON_GOVERNANCE_TEST_NETWORK === "1" &&
170 process.env.NEON_GOVERNANCE_TEST_VPC_UNREGISTER !== "1";
171
172test.provider.skipIf(!enabled)(
173 "dedicated network fixture cleans replaced project grants and preserves adopted restriction labels",
174 (stack) =>
175 Effect.gen(function* () {
176 yield* stack.destroy();
177 const scope = {
178 orgId: orgId!,
179 regionId: region!,
180 vpcEndpointId: vpcEndpointId!,
181 };
182 const organizationRequest = {
183 org_id: scope.orgId,
184 region_id: scope.regionId,
185 vpc_endpoint_id: scope.vpcEndpointId,
186 };
187 const organizationBaseline =
188 yield* SDK.getOrganizationVPCEndpointDetails(organizationRequest);
189 const base = Effect.gen(function* () {
190 const a = yield* Project("PrivateProjectA", {
191 orgId: scope.orgId,
192 region: region!,
193 });
194 const b = yield* Project("PrivateProjectB", {
195 orgId: scope.orgId,
196 region: region!,
197 });
198 return { a, b };
199 });
200 const application = (
201 second: boolean,
202 label: string,
203 takeOwnership = false,
204 ) =>
205 Effect.gen(function* () {
206 const projects = yield* base;
207 const restriction = yield* ProjectVPCEndpoint("Restriction", {
208 project: second ? projects.b : projects.a,
209 endpoint: scope,
210 label,
211 }).pipe(adopt(takeOwnership));
212 return { ...projects, restriction };
213 });
214 const first = yield* stack.deploy(
215 application(false, "Alchemy project fixture"),
216 );
217 const aRequest = {
218 project_id: first.a.projectId,
219 vpc_endpoint_id: scope.vpcEndpointId,
220 };
221 const bRequest = {
222 project_id: first.b.projectId,
223 vpc_endpoint_id: scope.vpcEndpointId,
224 };
225 expect(first.restriction.initialLabel).toBeNull();
226 expect(
227 (yield* SDK.listProjectVPCEndpoints(aRequest)).endpoints,
228 ).toContainEqual({
229 vpc_endpoint_id: scope.vpcEndpointId,
230 label: "Alchemy project fixture",
231 });
232 yield* stack.deploy(application(false, "Alchemy project updated"));
233 expect(
234 (yield* SDK.listProjectVPCEndpoints(aRequest)).endpoints,
235 ).toContainEqual({
236 vpc_endpoint_id: scope.vpcEndpointId,
237 label: "Alchemy project updated",
238 });
239 yield* stack.deploy(application(true, "Alchemy replacement fixture"));
240 expect((yield* SDK.listProjectVPCEndpoints(aRequest)).endpoints).toEqual(
241 [],
242 );
243 expect(
244 (yield* SDK.listProjectVPCEndpoints(bRequest)).endpoints,
245 ).toContainEqual({
246 vpc_endpoint_id: scope.vpcEndpointId,
247 label: "Alchemy replacement fixture",
248 });
249 yield* stack.deploy(base);
250 expect((yield* SDK.listProjectVPCEndpoints(bRequest)).endpoints).toEqual(
251 [],
252 );
253
254 yield* SDK.assignProjectVPCEndpoint({
255 ...aRequest,
256 label: "Original fixture restriction",
257 });
258 const baseline = (yield* SDK.listProjectVPCEndpoints(aRequest)).endpoints;
259 expect(
260 Result.isFailure(
261 yield* stack
262 .plan(application(false, "Adopted fixture"))
263 .pipe(Effect.result),
264 ),
265 ).toBe(true);
266 const adopted = yield* stack.deploy(
267 application(false, "Adopted fixture", true),
268 );
269 expect(adopted.restriction.initialLabel).toBe(
270 "Original fixture restriction",
271 );
272 yield* stack.deploy(application(false, "Updated adopted fixture"));
273 yield* stack.deploy(base);
274 expect((yield* SDK.listProjectVPCEndpoints(aRequest)).endpoints).toEqual(
275 baseline,
276 );
277 expect(
278 (yield* SDK.getOrganizationVPCEndpointDetails(organizationRequest))
279 .label,
280 ).toBe(organizationBaseline.label);
281 yield* stack.destroy();
282 for (const projectId of [first.a.projectId, first.b.projectId]) {
283 expect(
284 yield* SDK.getProject({ project_id: projectId }).pipe(
285 Effect.as(false),
286 Effect.catchTag("NotFound", () => Effect.succeed(true)),
287 ),
288 ).toBe(true);
289 }
290 yield* stack.destroy();
291 }),
292 {
293 tags: [
294 "provider:neon",
295 "provider:neon:project",
296 "provider:neon:projectvpcendpoint",
297 "live",
298 ],
299 timeout: 120_000,
300 exclusive: true,
301 },
302);