1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as Core from "@/Test/Core";
4import * as spanner from "@distilled.cloud/gcp/spanner_v1";
5import { describe, expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { dockerAvailable, expectProbe } from "../bindingHost.ts";
8import SpannerBindingsHost, {
9 App,
10 Db,
11 ITEMS_DDL,
12 Schema,
13} from "./fixtures/bindings-host.ts";
14
15const testOptions = { providers: GCP.providers() };
16const { test, beforeAll, afterAll } = Test.make(testOptions);
17const sharedStack = Core.scratchStack(testOptions, "SpannerBindings");
18
19let baseUrl: string;
20let member: string;
21let names: { instance: string; schema: string; app: string };
22
23type Policy = { bindings?: { role?: string; members?: string[] }[] };
24
25const rolesOf = (policy: Policy) =>
26 (policy.bindings ?? [])
27 .filter((binding) => (binding.members ?? []).includes(member))
28 .map((binding) => binding.role)
29 .sort();
30
31const databaseRoles = (database: string) =>
32 spanner
33 .getIamPolicyProjectsInstancesDatabases({
34 resource: database,
35 body: { options: { requestedPolicyVersion: 3 } },
36 })
37 .pipe(Effect.map(rolesOf));
38
39const instanceRoles = Effect.suspend(() =>
40 spanner
41 .getIamPolicyProjectsInstances({
42 resource: names.instance,
43 body: { options: { requestedPolicyVersion: 3 } },
44 })
45 .pipe(Effect.map(rolesOf)),
46);
47
48describe.skipIf(!dockerAvailable)(
49 "Spanner Bindings",
50 {
51 tags: ["provider:gcp", "provider:gcp:spanner", "provider:gcp:run", "live"],
52 },
53 () => {
54 beforeAll(
55 Effect.gen(function* () {
56 yield* sharedStack.destroy();
57 const out = yield* sharedStack.deploy(
58 Effect.gen(function* () {
59 const host = yield* SpannerBindingsHost;
60 return {
61 uri: host.uri,
62 serviceAccount: host.serviceAccount,
63 instance: (yield* Db).name,
64 schema: (yield* Schema).name,
65 app: (yield* App).name,
66 };
67 }),
68 );
69 baseUrl = out.uri!;
70 member = `serviceAccount:${out.serviceAccount!}`;
71 names = out;
72 }),
73 { timeout: 1_200_000 },
74 );
75
76 afterAll(sharedStack.destroy(), { timeout: 900_000 });
77
78 describe("GetInstance", () => {
79 test.provider(
80 "reads the instance, granted viewer on the instance only",
81 (_stack) =>
82 Effect.gen(function* () {
83 const live = yield* expectProbe<spanner.Instance>(
84 baseUrl,
85 "getInstance",
86 );
87 expect(live.name).toEqual(names.instance);
88 expect(live.processingUnits).toEqual(100);
89 expect(yield* instanceRoles).toEqual(["roles/spanner.viewer"]);
90 }),
91 {
92 tags: ["provider:gcp", "provider:gcp:spanner", "live"],
93 timeout: 600_000,
94 },
95 );
96 });
97
98 describe("GetDdl", () => {
99 test.provider(
100 "reads the schema, granted databaseReader on the database only",
101 (_stack) =>
102 Effect.gen(function* () {
103 const ddl = yield* expectProbe<spanner.GetDatabaseDdlResponse>(
104 baseUrl,
105 "getDdl",
106 );
107 expect(ddl.statements).toEqual([ITEMS_DDL]);
108
109 const direct = yield* spanner.getDdlProjectsInstancesDatabases({
110 database: names.schema,
111 });
112 expect(ddl.statements).toEqual(direct.statements);
113
114 expect(yield* databaseRoles(names.schema)).toEqual([
115 "roles/spanner.databaseReader",
116 ]);
117 }),
118 {
119 tags: ["provider:gcp", "provider:gcp:spanner", "live"],
120 timeout: 600_000,
121 },
122 );
123 });
124
125 describe("ExecuteSql", () => {
126 test.provider(
127 "queries the database, granted databaseUser on the database only",
128 (_stack) =>
129 Effect.gen(function* () {
130 const result = yield* expectProbe<spanner.ResultSet>(
131 baseUrl,
132 "executeSql",
133 );
134 expect(result.metadata?.rowType?.fields?.[0]?.name).toEqual("n");
135 expect(result.rows).toEqual([["0"]]);
136
137 expect(yield* databaseRoles(names.app)).toEqual([
138 "roles/spanner.databaseUser",
139 ]);
140 // Nothing leaks onto the other database or the instance.
141 expect(yield* databaseRoles(names.schema)).toEqual([
142 "roles/spanner.databaseReader",
143 ]);
144 expect(yield* instanceRoles).toEqual(["roles/spanner.viewer"]);
145 }),
146 {
147 tags: ["provider:gcp", "provider:gcp:spanner", "live"],
148 timeout: 600_000,
149 },
150 );
151 });
152 },
153);