1import { Auth, AuthProvider, removedAuthSettings } from "@alchemy.run/neon/Auth";
2import { AuthOAuthProvider, AuthOAuthProviderProvider } from "@alchemy.run/neon/AuthOAuthProvider";
3import { AuthTrustedDomain, AuthTrustedDomainProvider } from "@alchemy.run/neon/AuthTrustedDomain";
4import { Branch } from "@alchemy.run/neon/Branch";
5import { DataApi, DataApiProvider } from "@alchemy.run/neon/DataApi";
6import { runSql, withPgClient } from "@alchemy.run/neon/Migrations";
7import { Project } from "@alchemy.run/neon/Project";
8import { providers } from "@alchemy.run/neon/Providers";
9import { makePgMigrationExecutor } from "@alchemy.run/sql/Migrations";
10import { expect } from "@alchemy.run/test";
11import * as Test from "@alchemy.run/test/Alchemy";
12import * as SDK from "@distilled.cloud/neon";
13import { adopt, Unowned } from "alchemy/AdoptPolicy";
14import * as Output from "alchemy/Output";
15import * as Provider from "alchemy/Provider";
16import { Resource } from "alchemy/Resource";
17import { State } from "alchemy/State";
18import * as Effect from "effect/Effect";
19import * as HttpClient from "effect/http/HttpClient";
20import * as HttpClientRequest from "effect/http/HttpClientRequest";
21import * as HttpClientResponse from "effect/http/HttpClientResponse";
22import * as Layer from "effect/Layer";
23import * as Redacted from "effect/Redacted";
24import * as Result from "effect/Result";
25
26const { test } = Test.make({ providers: providers() });
27
28const context = {
29 id: "IdentityGuard",
30 fqn: "IdentityGuard",
31 instanceId: "identity-guard",
32 oldBindings: [],
33 newBindings: [],
34 bindings: [],
35 session: { note: () => Effect.void },
36};
37
38test.provider(
39 "Auth recovery treats incomplete uncreated identities as absent and preserves existing ownership",
40 () =>
41 Effect.gen(function* () {
42 const auth = yield* Auth.Provider;
43 const domain = yield* AuthTrustedDomain.Provider;
44 const scope = { projectId: "recovery-project", branchId: "recovery-branch" };
45 const origin = "https://recovery.example.com";
46 const incompleteAuth = { branch: { ...scope } };
47 const partialAuth = { branch: { ...scope } };
48 const incompleteDomain = { auth: { ...scope }, domain: origin };
49 const partialDomain = { auth: { ...scope }, domain: origin };
50 const missingOrigin = { auth: { ...scope }, domain: origin };
51 // Interrupted state can omit unresolved reference fields.
52 yield* Effect.sync(() => {
53 Reflect.deleteProperty(incompleteAuth, "branch");
54 Reflect.deleteProperty(partialAuth.branch, "branchId");
55 Reflect.deleteProperty(incompleteDomain, "auth");
56 Reflect.deleteProperty(partialDomain.auth, "projectId");
57 Reflect.deleteProperty(missingOrigin, "domain");
58 });
59 const requests: string[] = [];
60 const http = HttpClient.make((request) =>
61 Effect.sync(() => {
62 requests.push(request.method);
63 expect(request.method).toBe("GET");
64 return HttpClientResponse.fromWeb(
65 request,
66 Response.json(
67 request.url.endsWith("/domains")
68 ? { domains: [{ domain: origin, auth_provider: "better_auth" }] }
69 : {
70 auth_provider: "better_auth",
71 auth_provider_project_id: "recovery-auth",
72 branch_id: scope.branchId,
73 db_name: "neondb",
74 created_at: "2026-01-01T00:00:00Z",
75 owned_by: "neon",
76 jwks_url: "https://auth.example.com/jwks",
77 base_url: "https://auth.example.com",
78 },
79 ),
80 );
81 }),
82 );
83 yield* Effect.gen(function* () {
84 for (const olds of [
85 incompleteAuth,
86 partialAuth,
87 { branch: { projectId: scope.projectId, branchId: "" } },
88 { project: { projectId: "" } },
89 ]) {
90 expect(yield* auth.read!({ ...context, olds, output: undefined })).toBeUndefined();
91 }
92 for (const olds of [
93 incompleteDomain,
94 partialDomain,
95 missingOrigin,
96 { auth: { projectId: "", branchId: scope.branchId }, domain: origin },
97 ]) {
98 expect(yield* domain.read!({ ...context, olds, output: undefined })).toBeUndefined();
99 }
100 expect(requests).toEqual([]);
101 const authOutput = yield* auth.read!({
102 ...context,
103 olds: { branch: scope },
104 output: undefined,
105 });
106 const domainOutput = yield* domain.read!({
107 ...context,
108 olds: { auth: scope, domain: origin },
109 output: undefined,
110 });
111 expect(Unowned.is(authOutput)).toBe(true);
112 expect(Unowned.is(domainOutput)).toBe(true);
113 if (!authOutput || !domainOutput)
114 return yield* Effect.fail(new Error("Expected observed Auth identities"));
115 expect(
116 Unowned.is(yield* auth.read!({ ...context, olds: incompleteAuth, output: authOutput })),
117 ).toBe(false);
118 expect(
119 Unowned.is(
120 yield* domain.read!({ ...context, olds: incompleteDomain, output: domainOutput }),
121 ),
122 ).toBe(false);
123 expect(
124 yield* auth
125 .reconcile({ ...context, olds: undefined, news: { branch: scope }, output: undefined })
126 .pipe(
127 Effect.as(false),
128 Effect.catchTag("OwnedBySomeoneElse", () => Effect.succeed(true)),
129 ),
130 ).toBe(true);
131 expect(
132 yield* domain
133 .reconcile({
134 ...context,
135 olds: undefined,
136 news: { auth: scope, domain: origin },
137 output: undefined,
138 })
139 .pipe(
140 Effect.as(false),
141 Effect.catchTag("OwnedBySomeoneElse", () => Effect.succeed(true)),
142 ),
143 ).toBe(true);
144 expect(requests).toHaveLength(6);
145 }).pipe(
146 Effect.provideService(HttpClient.HttpClient, http),
147 Effect.provideService(
148 SDK.Credentials,
149 Effect.succeed({
150 apiKey: Redacted.make("recovery-test"),
151 apiBaseUrl: "https://neon.example.com",
152 }),
153 ),
154 );
155 }).pipe(Effect.provide(Layer.mergeAll(AuthProvider(), AuthTrustedDomainProvider()))),
156 { tags: ["provider:neon", "provider:neon:auth", "provider:neon:authtrusteddomain", "live"] },
157);
158
159test.provider(
160 "Auth children plan unresolved identities as replacements and reject mismatched cached scope",
161 () =>
162 Effect.gen(function* () {
163 const auth = yield* Auth.Provider;
164 const oauth = yield* AuthOAuthProvider.Provider;
165 const domain = yield* AuthTrustedDomain.Provider;
166 const data = yield* DataApi.Provider;
167 const scope = { projectId: "owned-project", branchId: "owned-branch" };
168 const changed = { ...scope, branchId: "other-branch" };
169 const unresolved = { ...scope, branchId: Output.literal("other-branch") };
170 const authOutput = {
171 ...scope,
172 database: "neondb",
173 baseUrl: "https://auth.example.com",
174 jwksUrl: "https://auth.example.com/jwks",
175 name: undefined,
176 };
177 const oauthOutput = {
178 ...scope,
179 provider: "github" as const,
180 type: "standard" as const,
181 clientId: "client",
182 };
183 const domainOutput = { ...scope, domain: "https://app.example.com" };
184 const dataOutput = {
185 ...scope,
186 database: "neondb",
187 url: "https://data.example.com",
188 status: "active",
189 settings: undefined,
190 };
191 expect(
192 yield* auth.diff!({
193 ...context,
194 olds: { branch: scope },
195 news: { branch: unresolved },
196 output: authOutput,
197 }),
198 ).toMatchObject({ action: "replace" });
199 expect(
200 yield* oauth.diff!({
201 ...context,
202 olds: { auth: scope, provider: "github" },
203 news: { auth: unresolved, provider: "github" },
204 output: oauthOutput,
205 }),
206 ).toMatchObject({ action: "replace" });
207 expect(
208 yield* domain.diff!({
209 ...context,
210 olds: { auth: scope, domain: domainOutput.domain },
211 news: { auth: unresolved, domain: domainOutput.domain },
212 output: domainOutput,
213 }),
214 ).toMatchObject({ action: "replace" });
215 expect(
216 yield* data.diff!({
217 ...context,
218 olds: { branch: scope, database: "neondb" },
219 news: { branch: unresolved, database: "neondb" },
220 output: dataOutput,
221 }),
222 ).toMatchObject({ action: "replace" });
223 expect(
224 yield* auth.diff!({
225 ...context,
226 olds: { branch: scope },
227 news: { branch: scope, database: Output.literal("neondb") },
228 output: authOutput,
229 }),
230 ).toBeUndefined();
231 expect(
232 yield* auth
233 .reconcile({
234 ...context,
235 olds: { branch: scope },
236 news: { branch: changed },
237 output: authOutput,
238 })
239 .pipe(
240 Effect.as(false),
241 Effect.catchTag("InvalidManagedAuth", () => Effect.succeed(true)),
242 ),
243 ).toBe(true);
244 expect(
245 yield* oauth
246 .reconcile({
247 ...context,
248 olds: { auth: scope, provider: "github" },
249 news: { auth: changed, provider: "github" },
250 output: oauthOutput,
251 })
252 .pipe(
253 Effect.as(false),
254 Effect.catchTag("InvalidManagedAuth", () => Effect.succeed(true)),
255 ),
256 ).toBe(true);
257 expect(
258 yield* domain
259 .reconcile({
260 ...context,
261 olds: { auth: scope, domain: domainOutput.domain },
262 news: { auth: changed, domain: domainOutput.domain },
263 output: domainOutput,
264 })
265 .pipe(
266 Effect.as(false),
267 Effect.catchTag("InvalidManagedAuth", () => Effect.succeed(true)),
268 ),
269 ).toBe(true);
270 expect(
271 yield* data
272 .reconcile({
273 ...context,
274 olds: { branch: scope, database: "neondb" },
275 news: { branch: changed, database: "neondb" },
276 output: dataOutput,
277 })
278 .pipe(
279 Effect.as(false),
280 Effect.catchTag("InvalidDataApiConfiguration", () => Effect.succeed(true)),
281 ),
282 ).toBe(true);
283 expect(
284 yield* auth
285 .reconcile({
286 ...context,
287 olds: { branch: scope, allowLocalhost: false },
288 news: { branch: scope, name: "must-not-apply" },
289 output: authOutput,
290 })
291 .pipe(
292 Effect.as(false),
293 Effect.catchTag("InvalidManagedAuth", () => Effect.succeed(true)),
294 ),
295 ).toBe(true);
296 expect(
297 yield* oauth
298 .reconcile({
299 ...context,
300 olds: { auth: scope, provider: "github", clientId: "client" },
301 news: { auth: scope, provider: "github" },
302 output: oauthOutput,
303 })
304 .pipe(
305 Effect.as(false),
306 Effect.catchTag("InvalidManagedAuth", () => Effect.succeed(true)),
307 ),
308 ).toBe(true);
309 expect(
310 yield* data
311 .reconcile({
312 ...context,
313 olds: { branch: scope, database: "neondb", settings: { db_max_rows: 10 } },
314 news: { branch: scope, database: "neondb", settings: {} },
315 output: dataOutput,
316 })
317 .pipe(
318 Effect.as(false),
319 Effect.catchTag("InvalidDataApiConfiguration", () => Effect.succeed(true)),
320 ),
321 ).toBe(true);
322 }).pipe(
323 Effect.provide(
324 Layer.mergeAll(
325 AuthProvider(),
326 AuthOAuthProviderProvider(),
327 AuthTrustedDomainProvider(),
328 DataApiProvider(),
329 ),
330 ),
331 ),
332 {
333 tags: [
334 "provider:neon",
335 "provider:neon:auth",
336 "provider:neon:authoauthprovider",
337 "provider:neon:authtrusteddomain",
338 "provider:neon:dataapi",
339 "live",
340 ],
341 },
342);
343
344test(
345 "managed field removal checks nested settings and permits an explicit email provider switch",
346 Effect.sync(() => {
347 expect(
348 removedAuthSettings(
349 { magicLink: { enabled: true, expires_in: 10 } },
350 { magicLink: { enabled: false } },
351 ["magicLink"],
352 ),
353 ).toEqual(["magicLink.expires_in"]);
354 expect(
355 removedAuthSettings(
356 { emailProvider: { type: "standard", host: "smtp.example.com" } },
357 { emailProvider: { type: "shared" } },
358 ["emailProvider"],
359 ),
360 ).toEqual([]);
361 }),
362 { tags: ["unit", "provider:neon", "provider:neon:auth", "local"] },
363);
364
365test.provider(
366 "managed Auth requires adoption and preserves user data after external disable",
367 (stack) =>
368 Effect.gen(function* () {
369 yield* stack.destroy();
370 const base = Effect.gen(function* () {
371 const project = yield* Project("AuthOwnershipProject", { region: "aws-us-east-2" });
372 const branch = yield* Branch("AuthOwnershipBranch", { project });
373 return { project, branch };
374 });
375 const existing = yield* stack.deploy(base);
376 const request = {
377 project_id: existing.branch.projectId,
378 branch_id: existing.branch.branchId,
379 };
380 yield* SDK.createNeonAuth({ ...request, auth_provider: "better_auth" });
381 const application = (takeOwnership: boolean) =>
382 Effect.gen(function* () {
383 const { branch } = yield* base;
384 const auth = yield* Auth("OwnedAuth", {
385 branch,
386 name: "Owned managed auth",
387 allowLocalhost: false,
388 }).pipe(adopt(takeOwnership));
389 return { auth };
390 });
391 const refusal = yield* stack.deploy(application(false)).pipe(Effect.result);
392 expect(Result.isFailure(refusal)).toBe(true);
393 yield* stack.deploy(application(true));
394 expect((yield* SDK.getNeonAuth(request)).name).toBe("Owned managed auth");
395 expect((yield* SDK.getNeonAuthAllowLocalhost(request)).allow_localhost).toBe(false);
396 yield* SDK.disableNeonAuth(request);
397 const refused = yield* stack.deploy(application(false)).pipe(
398 Effect.as(undefined),
399 Effect.catchTag("Conflict", (error) => Effect.succeed(error.message)),
400 );
401 expect(refused).toBe(
402 "The `neon_auth` schema already exists and cannot be automatically provisioned. Please drop the existing `neon_auth` schema before provisioning Neon Auth.",
403 );
404 yield* stack.destroy();
405 expect(
406 yield* SDK.getNeonAuth(request).pipe(
407 Effect.as(false),
408 Effect.catchTag("NotFound", () => Effect.succeed(true)),
409 ),
410 ).toBe(true);
411 yield* stack.destroy();
412 }),
413 {
414 tags: [
415 "provider:neon",
416 "provider:neon:auth",
417 "provider:neon:branch",
418 "provider:neon:project",
419 "live",
420 ],
421 timeout: 120_000,
422 },
423);
424
425interface PreviewScope extends Resource<
426 "Test.AuthPreviewScope",
427 {},
428 { projectId: string; branchId: string }
429> {}
430const PreviewScope = Resource<PreviewScope>("Test.AuthPreviewScope");
431const { test: retryTest } = Test.make({
432 providers: Layer.mergeAll(
433 AuthProvider(),
434 Provider.succeed(PreviewScope, {
435 reconcile: () => Effect.succeed({ projectId: "retry-project", branchId: "retry-child" }),
436 delete: () => Effect.void,
437 }),
438 ).pipe(
439 Layer.provideMerge(
440 Layer.succeed(
441 SDK.Credentials,
442 Effect.succeed({
443 apiKey: Redacted.make("ownership-retry-test"),
444 apiBaseUrl: "https://neon.example.com",
445 }),
446 ),
447 ),
448 ),
449});
450
451retryTest.provider(
452 "actual Auth refusal retries scoped adoption with complete branch identity",
453 (stack) => {
454 const requests: string[] = [];
455 const http = HttpClient.make((request) =>
456 Effect.sync(() => {
457 requests.push(request.method);
458 expect(request.url).toContain("/projects/retry-project/branches/retry-child/auth");
459 expect(["GET", "DELETE"]).toContain(request.method);
460 return HttpClientResponse.fromWeb(
461 request,
462 Response.json(
463 request.method === "DELETE"
464 ? {}
465 : {
466 auth_provider: "better_auth",
467 auth_provider_project_id: "inherited-auth",
468 branch_id: "retry-child",
469 db_name: "neondb",
470 created_at: "2026-01-01T00:00:00Z",
471 owned_by: "neon",
472 jwks_url: "https://child.example.com/jwks",
473 base_url: "https://child.example.com",
474 },
475 ),
476 );
477 }),
478 );
479 const app = (enabled: boolean) =>
480 Effect.gen(function* () {
481 const branch = yield* PreviewScope("Branch", {});
482 return yield* Auth("Auth", { branch }).pipe(adopt(enabled));
483 });
484 return Effect.gen(function* () {
485 yield* stack.destroy();
486 const refusal = yield* stack.deploy(app(false)).pipe(
487 Effect.as(false),
488 Effect.catchTag("OwnedBySomeoneElse", () => Effect.succeed(true)),
489 );
490 expect(refusal).toBe(true);
491 expect(requests.every((method) => method === "GET")).toBe(true);
492 const state = yield* yield* State;
493 const refused = yield* state.get({ stack: stack.name, stage: stack.stage, fqn: "Auth" });
494 if (!refused || refused.kind === "action")
495 return yield* Effect.fail(new Error("Expected an Auth resource checkpoint"));
496 expect(refused.attr).toBeUndefined();
497 expect(
498 yield* stack.deploy(app(false)).pipe(
499 Effect.as(false),
500 Effect.catchTag("OwnedBySomeoneElse", () => Effect.succeed(true)),
501 ),
502 ).toBe(true);
503 const adopted = yield* stack.deploy(app(true));
504 expect(adopted.branchId).toBe("retry-child");
505 expect(adopted.baseUrl).toBe("https://child.example.com");
506 expect(refused?.props).toEqual({
507 branch: { projectId: "retry-project", branchId: "retry-child" },
508 });
509 expect(requests.every((method) => method === "GET")).toBe(true);
510 yield* stack.destroy();
511 expect(requests.filter((method) => method === "DELETE")).toHaveLength(1);
512 }).pipe(
513 Effect.ensuring(stack.destroy().pipe(Effect.orDie)),
514 Effect.provideService(HttpClient.HttpClient, http),
515 );
516 },
517 { tags: ["unit", "provider:neon", "provider:neon:auth", "local"], timeout: 10_000 },
518);
519
520const { test: previewTest } = Test.make({
521 providers: providers(),
522 stage: "test-neon-auth-deferred-adoption",
523});
524
525previewTest.provider(
526 "new child Auth explicitly adopts inherited integration and preserves parent identities and data",
527 (stack) =>
528 Effect.gen(function* () {
529 yield* stack.destroy();
530 const parent = Effect.gen(function* () {
531 const project = yield* Project("DeferredAuthProject", { region: "aws-us-east-2" });
532 const auth = yield* Auth("ParentAuth", {
533 project,
534 name: "Deferred adoption parent",
535 allowLocalhost: true,
536 emailAndPassword: {
537 enabled: true,
538 require_email_verification: false,
539 send_verification_email_on_sign_up: false,
540 },
541 });
542 return { project, auth };
543 });
544 const first = yield* stack.deploy(parent);
545 const parentRequest = { project_id: first.auth.projectId, branch_id: first.auth.branchId };
546 const parentUri = Redacted.make(first.project.connectionUri);
547 const query = (uri: Redacted.Redacted<string>, sql: string) =>
548 withPgClient(uri, (client) => makePgMigrationExecutor(client).query(sql));
549 yield* runSql(
550 parentUri,
551 "CREATE TABLE auth_preview_probe (id integer PRIMARY KEY, value text NOT NULL); INSERT INTO auth_preview_probe VALUES (1, 'parent');",
552 );
553 const http = yield* HttpClient.HttpClient;
554 const baseUrl = first.auth.baseUrl.replace(/\/$/, "");
555 const authPath = baseUrl.endsWith("/auth") ? baseUrl : `${baseUrl}/auth`;
556 const signup = yield* http.execute(
557 HttpClientRequest.post(`${authPath}/sign-up/email`).pipe(
558 HttpClientRequest.setHeader("origin", "http://localhost:4318"),
559 HttpClientRequest.bodyJsonUnsafe({
560 email: "parent-auth-preview@example.com",
561 password: "Alchemy-Auth-Preview-Test-Only-73!",
562 name: "Parent preview user",
563 }),
564 ),
565 );
566 expect(signup.status).toBe(200);
567 const usersSql = 'SELECT id, email FROM neon_auth."user" ORDER BY id';
568 const parentUsers = yield* query(parentUri, usersSql);
569 expect(parentUsers.length).toBe(1);
570 const parentAuth = yield* SDK.getNeonAuth(parentRequest);
571 const parentSettings = yield* SDK.getNeonAuthEmailAndPasswordConfig(parentRequest);
572 const parentLocalhost = yield* SDK.getNeonAuthAllowLocalhost(parentRequest);
573 const verifyParent = Effect.gen(function* () {
574 const survivingAuth = yield* SDK.getNeonAuth(parentRequest);
575 expect(survivingAuth.base_url).toBe(parentAuth.base_url);
576 expect(survivingAuth.name).toBe(parentAuth.name);
577 expect(yield* SDK.getNeonAuthEmailAndPasswordConfig(parentRequest)).toEqual(parentSettings);
578 expect(yield* SDK.getNeonAuthAllowLocalhost(parentRequest)).toEqual(parentLocalhost);
579 expect(yield* query(parentUri, usersSql)).toEqual(parentUsers);
580 expect(yield* query(parentUri, "SELECT * FROM auth_preview_probe")).toEqual([
581 { id: 1, value: "parent" },
582 ]);
583 });
584 const preview = Effect.gen(function* () {
585 const resources = yield* parent;
586 const branch = yield* Branch("AuthPreview", {
587 project: resources.project,
588 parentBranch: { branchId: resources.auth.branchId },
589 initSource: "parent-data",
590 });
591 const auth = yield* Auth("PreviewAuth", {
592 branch,
593 name: "Deferred adoption child",
594 allowLocalhost: false,
595 }).pipe(adopt());
596 return { branch, auth };
597 });
598 const child = yield* stack.deploy(preview);
599 expect(child.branch.parentBranchId).toBe(first.auth.branchId);
600 expect(child.auth.branchId).toBe(child.branch.branchId);
601 expect(child.auth.branchId).not.toBe(first.auth.branchId);
602 expect(child.auth.baseUrl).not.toBe(first.auth.baseUrl);
603 const childRequest = { project_id: child.auth.projectId, branch_id: child.auth.branchId };
604 expect((yield* SDK.getNeonAuth(childRequest)).name).toBe("Deferred adoption child");
605 expect((yield* SDK.getNeonAuthAllowLocalhost(childRequest)).allow_localhost).toBe(false);
606 const childUri = Redacted.make(child.branch.connectionUri);
607 expect(yield* query(childUri, usersSql)).toEqual(parentUsers);
608 expect(yield* query(childUri, "SELECT * FROM auth_preview_probe")).toEqual([
609 { id: 1, value: "parent" },
610 ]);
611 yield* runSql(childUri, "UPDATE auth_preview_probe SET value = 'child' WHERE id = 1");
612 expect(yield* query(childUri, "SELECT * FROM auth_preview_probe")).toEqual([
613 { id: 1, value: "child" },
614 ]);
615 yield* verifyParent;
616 const unchanged = yield* stack.deploy(preview);
617 expect(unchanged.auth.baseUrl).toBe(child.auth.baseUrl);
618 yield* stack.deploy(parent);
619 expect(
620 yield* SDK.getNeonAuth(childRequest).pipe(
621 Effect.as(false),
622 Effect.catchTag("NotFound", () => Effect.succeed(true)),
623 ),
624 ).toBe(true);
625 expect(
626 yield* SDK.getProjectBranch(childRequest).pipe(
627 Effect.as(false),
628 Effect.catchTag("NotFound", () => Effect.succeed(true)),
629 ),
630 ).toBe(true);
631 yield* verifyParent;
632 yield* stack.destroy();
633 expect(
634 yield* SDK.getNeonAuth(parentRequest).pipe(
635 Effect.as(false),
636 Effect.catchTag("NotFound", () => Effect.succeed(true)),
637 ),
638 ).toBe(true);
639 expect(
640 yield* SDK.getProject({ project_id: first.project.projectId }).pipe(
641 Effect.as(false),
642 Effect.catchTag("NotFound", () => Effect.succeed(true)),
643 ),
644 ).toBe(true);
645 }),
646 {
647 tags: [
648 "provider:neon",
649 "provider:neon:auth",
650 "provider:neon:branch",
651 "provider:neon:project",
652 "live",
653 ],
654 timeout: 120_000,
655 },
656);
657
658test.provider(
659 "Auth scope replacement retains both branches and leaves the old branch disabled",
660 (stack) =>
661 Effect.gen(function* () {
662 yield* stack.destroy();
663 const application = (second: boolean) =>
664 Effect.gen(function* () {
665 const project = yield* Project("AuthReplacementProject", { region: "aws-us-east-2" });
666 const a = yield* Branch("AuthBranchA", { project });
667 const b = second ? yield* Branch("AuthBranchB", { project }) : undefined;
668 const auth = yield* Auth("ReplacingAuth", { branch: b ?? a });
669 const domain = yield* AuthTrustedDomain("ScopeOrigin", {
670 auth,
671 domain: "https://scope.example.com",
672 });
673 return { a, b, auth, domain };
674 });
675 const first = yield* stack.deploy(application(false));
676 const replaced = yield* stack.deploy(application(true));
677 expect(replaced.auth.branchId).toBe(replaced.b!.branchId);
678 expect(replaced.domain.branchId).toBe(replaced.b!.branchId);
679 expect(replaced.a.branchId).toBe(first.a.branchId);
680 expect(
681 yield* SDK.getNeonAuth({
682 project_id: first.auth.projectId,
683 branch_id: first.auth.branchId,
684 }).pipe(
685 Effect.as(false),
686 Effect.catchTag("NotFound", () => Effect.succeed(true)),
687 ),
688 ).toBe(true);
689 yield* stack.destroy();
690 }),
691 {
692 tags: [
693 "provider:neon",
694 "provider:neon:auth",
695 "provider:neon:authtrusteddomain",
696 "provider:neon:branch",
697 "provider:neon:project",
698 "live",
699 ],
700 timeout: 120_000,
701 },
702);