1import {
2 scopeIdentity,
3 usesInjectedCredentials,
4} from "@/Neon/CredentialScope.ts";
5import * as Neon from "@/Neon/index.ts";
6import * as Output from "@/Output.ts";
7import { expect, test } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";
10
11const branch = { projectId: "project", branchId: "branch" };
12const host = {
13 Type: "Neon.Function",
14 Props: { branch },
15 Mode: undefined,
16};
17
18test(
19 "HTTP credentials use injection only for a known same-branch live Function",
20 () => {
21 expect(usesInjectedCredentials(host, { branch }, "live")).toBe(true);
22 expect(
23 usesInjectedCredentials(host, { branch: { ...branch } }, "live"),
24 ).toBe(true);
25 expect(
26 usesInjectedCredentials(
27 host,
28 { branch: { ...branch, branchId: "other" } },
29 "live",
30 ),
31 ).toBe(false);
32 expect(
33 usesInjectedCredentials(
34 host,
35 { branch: { ...branch, projectId: "other" } },
36 "live",
37 ),
38 ).toBe(false);
39 expect(usesInjectedCredentials(host, { branch }, "local")).toBe(false);
40 expect(
41 usesInjectedCredentials({ ...host, Mode: "live" }, { branch }, "local"),
42 ).toBe(true);
43 expect(
44 usesInjectedCredentials({ ...host, Mode: "local" }, { branch }, "live"),
45 ).toBe(false);
46 for (const Type of ["Cloudflare.Worker", "AWS.Lambda.Function"]) {
47 expect(
48 usesInjectedCredentials({ ...host, Type }, { branch }, "live"),
49 ).toBe(false);
50 }
51 expect(usesInjectedCredentials(undefined, { branch }, "live")).toBe(false);
52 },
53 { tags: ["unit", "provider:neon", "local"] },
54);
55
56test(
57 "HTTP credentials preserve project scope and reject ambiguous unresolved identities",
58 () => {
59 const project = { projectId: "project" };
60 expect(
61 usesInjectedCredentials(
62 { ...host, Props: { project } },
63 { project: { ...project } },
64 "live",
65 ),
66 ).toBe(true);
67 expect(usesInjectedCredentials(host, { project }, "live")).toBe(false);
68 const unresolved = {
69 projectId: Output.literal("project"),
70 branchId: Output.literal("branch"),
71 };
72 expect(scopeIdentity({ branch: unresolved })).toBeUndefined();
73 expect(
74 usesInjectedCredentials(
75 { ...host, Props: { branch: unresolved } },
76 { branch },
77 "live",
78 ),
79 ).toBe(false);
80 expect(
81 usesInjectedCredentials(
82 { ...host, Props: Effect.succeed({ branch }) },
83 { branch },
84 "live",
85 ),
86 ).toBe(false);
87 expect(
88 usesInjectedCredentials(
89 { ...host, Props: undefined },
90 { branch },
91 "live",
92 ),
93 ).toBe(false);
94 expect(
95 usesInjectedCredentials(
96 { ...host, Props: { branch: unresolved } },
97 { branch: unresolved },
98 "live",
99 ),
100 ).toBe(true);
101 },
102 { tags: ["unit", "provider:neon", "local"] },
103);
104
105test(
106 "Neon exposes only HTTP implementation layers",
107 () => {
108 expect("ConnectDataApi" in Neon).toBe(false);
109 expect("ConnectDataApiHttp" in Neon).toBe(false);
110 expect("ConnectAIGateway" in Neon).toBe(false);
111 expect("ConnectAIGatewayHttp" in Neon).toBe(false);
112 for (const name of [
113 "Connect",
114 "ConnectAuth",
115 "QueryDataApi",
116 "QueryAIGateway",
117 "ReadBucket",
118 "WriteBucket",
119 "ReadWriteBucket",
120 "ReadObject",
121 "WriteObject",
122 "InvokeFunction",
123 "CronEventSource",
124 "BucketEventSource",
125 ]) {
126 expect(`${name}Binding` in Neon).toBe(false);
127 expect(Layer.isLayer(Reflect.get(Neon, `${name}Http`))).toBe(true);
128 }
129 },
130 { tags: ["unit", "provider:neon", "local"] },
131);