1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as Core from "@/Test/Core";
4import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
5import type * as recommendationengine from "@distilled.cloud/gcp/recommendationengine_v1beta1";
6import { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { dockerAvailable, expectProbe } from "../bindingHost.ts";
9import { currentProject, entitled } from "./common.ts";
10import RecommendationEngineBindingsHost, {
11 Shirt,
12 TITLE,
13} from "./fixtures/bindings-host.ts";
14
15const testOptions = { providers: GCP.providers() };
16const { test, beforeAll, afterAll } = Test.make(testOptions);
17const sharedStack = Core.scratchStack(
18 testOptions,
19 "RecommendationEngineBindings",
20);
21
22let baseUrl: string;
23let member: string;
24let itemId: string;
25
26// Recommendations AI (Beta) is not enabled in the testing project; see
27// common.ts (GCP_TEST_RECOMMENDATION_ENGINE=1).
28describe.skipIf(!dockerAvailable || !entitled || !!process.env.FAST)(
29 "RecommendationEngine Bindings",
30 {
31 tags: [
32 "provider:gcp",
33 "provider:gcp:recommendationengine",
34 "provider:gcp:run",
35 "live",
36 ],
37 },
38 () => {
39 beforeAll(
40 Effect.gen(function* () {
41 yield* sharedStack.destroy();
42 const out = yield* sharedStack.deploy(
43 Effect.gen(function* () {
44 const host = yield* RecommendationEngineBindingsHost;
45 return {
46 uri: host.uri,
47 serviceAccount: host.serviceAccount,
48 itemId: (yield* Shirt).catalogItemId,
49 };
50 }),
51 );
52 baseUrl = out.uri!;
53 member = `serviceAccount:${out.serviceAccount!}`;
54 itemId = out.itemId;
55 }),
56 { timeout: 900_000 },
57 );
58
59 afterAll(sharedStack.destroy(), { timeout: 600_000 });
60
61 describe("GetCatalogItem", () => {
62 test.provider(
63 "reads the catalog item, granted viewer on the project",
64 (_stack) =>
65 Effect.gen(function* () {
66 const live =
67 yield* expectProbe<recommendationengine.GoogleCloudRecommendationengineV1beta1CatalogItem>(
68 baseUrl,
69 "getCatalogItem",
70 );
71 expect(live.id).toEqual(itemId);
72 expect(live.title).toEqual(TITLE);
73
74 // Recommendations AI has no resource-level IAM or IAM
75 // Conditions, so the viewer role is granted on the project.
76 const project = yield* currentProject;
77 const policy = yield* resourcemanager.getIamPolicyProjects({
78 resource: `projects/${project}`,
79 body: { options: { requestedPolicyVersion: 3 } },
80 });
81 expect(
82 (policy.bindings ?? [])
83 .filter((binding) => (binding.members ?? []).includes(member))
84 .map((binding) => [binding.role, binding.condition]),
85 ).toEqual([["roles/automlrecommendations.viewer", undefined]]);
86 }),
87 {
88 tags: ["provider:gcp", "provider:gcp:recommendationengine", "live"],
89 timeout: 600_000,
90 },
91 );
92 });
93 },
94);