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