1import * as GCP from "@/GCP";
2import { GcpEnvironment } from "@/GCP/Environment";
3import * as Test from "@/Test/Alchemy";
4import * as Core from "@/Test/Core";
5import * as crm from "@distilled.cloud/gcp/cloudresourcemanager_v3";
6import * as cw from "@distilled.cloud/gcp/contentwarehouse_v1";
7import { describe, expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { dockerAvailable, expectProbe } from "../bindingHost.ts";
10import ContentWarehouseBindingsHost, {
11 Checks,
12 Note,
13 Sales,
14 Welcome,
15} from "./fixtures/bindings-host.ts";
16
17const testOptions = { providers: GCP.providers() };
18const { test, beforeAll, afterAll } = Test.make(testOptions);
19const sharedStack = Core.scratchStack(testOptions, "ContentWarehouseBindings");
20
21// Document AI Warehouse is disabled on the testing project: calls fail with
22// ServiceDisabled "Document AI Warehouse API has not been used in project ...
23// before or it is disabled" (the service also needs per-project
24// provisioning). Set GCP_TEST_CONTENTWAREHOUSE=1 on a provisioned project.
25const runLifecycle =
26 !process.env.FAST && !!process.env.GCP_TEST_CONTENTWAREHOUSE;
27
28let baseUrl: string;
29let hostAccount: string;
30let schemaName: string;
31let documentName: string;
32let ruleSetName: string;
33let synonymSetName: string;
34
35/**
36 * Document AI Warehouse has no per-resource IAM for these types, so every
37 * binding grants on the project: `documentSchemaViewer` (GetDocumentSchema),
38 * `documentViewer` (GetDocument) and `admin` (GetRuleSet / GetSynonymSet —
39 * `ruleSets.get` / `synonymSets.get` exist in no narrower role).
40 */
41const expectProjectGrants = Effect.gen(function* () {
42 const { project } = yield* GcpEnvironment.current;
43 const policy = yield* crm.getIamPolicyProjects({
44 resource: `projects/${project}`,
45 body: { options: { requestedPolicyVersion: 3 } },
46 });
47 const roles = (policy.bindings ?? [])
48 .filter((binding) =>
49 (binding.members ?? []).includes(`serviceAccount:${hostAccount}`),
50 )
51 .map((binding) => binding.role)
52 .sort();
53 expect(roles).toEqual([
54 "roles/contentwarehouse.admin",
55 "roles/contentwarehouse.documentSchemaViewer",
56 "roles/contentwarehouse.documentViewer",
57 ]);
58});
59
60describe.skipIf(!dockerAvailable || !runLifecycle)(
61 "ContentWarehouse Bindings",
62 {
63 tags: [
64 "provider:gcp",
65 "provider:gcp:contentwarehouse",
66 "provider:gcp:run",
67 "live",
68 ],
69 },
70 () => {
71 beforeAll(
72 Effect.gen(function* () {
73 yield* sharedStack.destroy();
74 const out = yield* sharedStack.deploy(
75 Effect.gen(function* () {
76 const host = yield* ContentWarehouseBindingsHost;
77 const schema = yield* Note;
78 const document = yield* Welcome;
79 const rules = yield* Checks;
80 const synonyms = yield* Sales;
81 return {
82 uri: host.uri,
83 serviceAccount: host.serviceAccount,
84 schema: schema.name,
85 document: document.name,
86 rules: rules.name,
87 synonyms: synonyms.name,
88 };
89 }),
90 );
91 baseUrl = out.uri!;
92 hostAccount = out.serviceAccount!;
93 schemaName = out.schema;
94 documentName = out.document;
95 ruleSetName = out.rules;
96 synonymSetName = out.synonyms;
97 }),
98 { timeout: 900_000 },
99 );
100
101 afterAll(sharedStack.destroy(), { timeout: 600_000 });
102
103 describe("GetDocumentSchema", () => {
104 test.provider(
105 "reads the schema as the host's service account",
106 (_stack) =>
107 Effect.gen(function* () {
108 const schema =
109 yield* expectProbe<cw.GoogleCloudContentwarehouseV1DocumentSchema>(
110 baseUrl,
111 "getDocumentSchema",
112 );
113 const live = yield* cw.getProjectsLocationsDocumentSchemas({
114 name: schemaName,
115 });
116 expect(schema.name).toEqual(schemaName);
117 expect(schema.displayName).toEqual("binding-note");
118 expect(schema.updateTime).toEqual(live.updateTime);
119 yield* expectProjectGrants;
120 }),
121 {
122 tags: ["provider:gcp", "provider:gcp:contentwarehouse", "live"],
123 timeout: 600_000,
124 },
125 );
126 });
127
128 describe("GetDocument", () => {
129 test.provider(
130 "reads the document as the host's service account",
131 (_stack) =>
132 Effect.gen(function* () {
133 const document =
134 yield* expectProbe<cw.GoogleCloudContentwarehouseV1Document>(
135 baseUrl,
136 "getDocument",
137 );
138 expect(document.name).toEqual(documentName);
139 expect(document.displayName).toEqual("binding-welcome");
140 expect(document.plainText).toEqual("hello binding");
141 yield* expectProjectGrants;
142 }),
143 {
144 tags: ["provider:gcp", "provider:gcp:contentwarehouse", "live"],
145 timeout: 600_000,
146 },
147 );
148 });
149
150 describe("GetRuleSet", () => {
151 test.provider(
152 "reads the rule set as the host's service account",
153 (_stack) =>
154 Effect.gen(function* () {
155 const rules =
156 yield* expectProbe<cw.GoogleCloudContentwarehouseV1RuleSet>(
157 baseUrl,
158 "getRuleSet",
159 );
160 const live = yield* cw.getProjectsLocationsRuleSets({
161 name: ruleSetName,
162 });
163 expect(rules.name).toEqual(ruleSetName);
164 expect(rules.description).toEqual("binding rules");
165 expect(rules.rules?.length).toEqual(live.rules?.length);
166 yield* expectProjectGrants;
167 }),
168 {
169 tags: ["provider:gcp", "provider:gcp:contentwarehouse", "live"],
170 timeout: 600_000,
171 },
172 );
173 });
174
175 describe("GetSynonymSet", () => {
176 test.provider(
177 "reads the synonym set as the host's service account",
178 (_stack) =>
179 Effect.gen(function* () {
180 const synonyms =
181 yield* expectProbe<cw.GoogleCloudContentwarehouseV1SynonymSet>(
182 baseUrl,
183 "getSynonymSet",
184 );
185 expect(synonyms.name).toEqual(synonymSetName);
186 expect(synonyms.synonyms?.[0]?.words).toEqual([
187 "sale",
188 "invoice",
189 "bill",
190 ]);
191 yield* expectProjectGrants;
192 }),
193 {
194 tags: ["provider:gcp", "provider:gcp:contentwarehouse", "live"],
195 timeout: 600_000,
196 },
197 );
198 });
199 },
200);