1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as Core from "@/Test/Core";
4import * as crm from "@distilled.cloud/gcp/cloudresourcemanager_v3";
5import type * as retail from "@distilled.cloud/gcp/retail_v2";
6import { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { dockerAvailable, expectProbe } from "../bindingHost.ts";
9import RetailBindingsHost, {
10 retailEnabled,
11 Serving,
12 Shirt,
13} from "./fixtures/bindings-host.ts";
14
15const testOptions = { providers: GCP.providers() };
16const { test, beforeAll, afterAll } = Test.make(testOptions);
17const sharedStack = Core.scratchStack(testOptions, "RetailBindings");
18
19let baseUrl: string;
20let hostAccount: string;
21let project: string;
22
23/** Serving configs have no IAM policy: both bindings grant on the project. */
24const expectProjectGrants = Effect.gen(function* () {
25 const policy = yield* crm.getIamPolicyProjects({
26 resource: `projects/${project}`,
27 body: { options: { requestedPolicyVersion: 3 } },
28 });
29 const roles = (policy.bindings ?? [])
30 .filter((binding) =>
31 (binding.members ?? []).includes(`serviceAccount:${hostAccount}`),
32 )
33 .map((binding) => ({ role: binding.role, condition: binding.condition }));
34 expect(roles).toEqual([
35 { role: "roles/retail.viewer", condition: undefined },
36 ]);
37});
38
39describe.skipIf(!dockerAvailable || !retailEnabled)(
40 "Retail Bindings",
41 {
42 tags: ["provider:gcp", "provider:gcp:retail", "provider:gcp:run", "live"],
43 },
44 () => {
45 beforeAll(
46 Effect.gen(function* () {
47 yield* sharedStack.destroy();
48 const out = yield* sharedStack.deploy(
49 Effect.gen(function* () {
50 const host = yield* RetailBindingsHost;
51 const serving = yield* Serving;
52 yield* Shirt;
53 return {
54 uri: host.uri,
55 serviceAccount: host.serviceAccount,
56 project: serving.project,
57 };
58 }),
59 );
60 baseUrl = out.uri!;
61 hostAccount = out.serviceAccount!;
62 project = out.project;
63 }),
64 { timeout: 900_000 },
65 );
66
67 afterAll(sharedStack.destroy(), { timeout: 600_000 });
68
69 describe("Search", () => {
70 test.provider(
71 "searches the catalog as the host's service account",
72 (_stack) =>
73 Effect.gen(function* () {
74 const searched =
75 yield* expectProbe<retail.GoogleCloudRetailV2SearchResponse>(
76 baseUrl,
77 "search",
78 );
79 expect(searched.attributionToken).toEqual(expect.any(String));
80 yield* expectProjectGrants;
81 }),
82 {
83 tags: ["provider:gcp", "provider:gcp:retail", "live"],
84 timeout: 600_000,
85 },
86 );
87 });
88
89 describe("Predict", () => {
90 test.provider(
91 "validates a prediction request as the host's service account",
92 (_stack) =>
93 Effect.gen(function* () {
94 const predicted =
95 yield* expectProbe<retail.GoogleCloudRetailV2PredictResponse>(
96 baseUrl,
97 "predict",
98 );
99 expect(predicted.validateOnly).toEqual(true);
100 yield* expectProjectGrants;
101 }),
102 {
103 tags: ["provider:gcp", "provider:gcp:retail", "live"],
104 timeout: 600_000,
105 },
106 );
107 });
108 },
109);