1import * as GCP from "@/GCP";
2import { GcpEnvironment } from "@/GCP/Environment";
3import * as Test from "@/Test/Alchemy";
4import * as Core from "@/Test/Core";
5import * as bqdt from "@distilled.cloud/gcp/bigquerydatatransfer_v1";
6import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
7import { describe, expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { dockerAvailable, expectProbe } from "../bindingHost.ts";
10import BigQueryDataTransferBindingsHost, {
11 Nightly,
12} from "./fixtures/bindings-host.ts";
13
14const testOptions = { providers: GCP.providers() };
15const { test, beforeAll, afterAll } = Test.make(testOptions);
16const sharedStack = Core.scratchStack(
17 testOptions,
18 "BigQueryDataTransferBindings",
19);
20
21let baseUrl: string;
22let configName: string;
23let hostAccount: string;
24
25/** Every project-level role (and its IAM Condition) `account` holds. */
26const projectGrantsOf = (account: string) =>
27 Effect.gen(function* () {
28 const { project } = yield* GcpEnvironment.current;
29 const policy = yield* resourcemanager.getIamPolicyProjects({
30 resource: `projects/${project}`,
31 body: { options: { requestedPolicyVersion: 3 } },
32 });
33 return (policy.bindings ?? [])
34 .filter((binding) =>
35 (binding.members ?? []).includes(`serviceAccount:${account}`),
36 )
37 .map((binding) => ({
38 role: binding.role,
39 condition: binding.condition?.expression,
40 }));
41 });
42
43describe.skipIf(!dockerAvailable)(
44 "BigQueryDataTransfer Bindings",
45 {
46 tags: [
47 "provider:gcp",
48 "provider:gcp:bigquerydatatransfer",
49 "provider:gcp:run",
50 "live",
51 ],
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* BigQueryDataTransferBindingsHost;
60 const config = yield* Nightly;
61 return {
62 uri: host.uri,
63 serviceAccount: host.serviceAccount,
64 config: config.name,
65 };
66 }),
67 );
68 baseUrl = out.uri!;
69 configName = out.config;
70 hostAccount = out.serviceAccount!;
71 }),
72 { timeout: 900_000 },
73 );
74
75 afterAll(sharedStack.destroy(), { timeout: 600_000 });
76
77 describe("StartManualRuns", () => {
78 test.provider(
79 "starts a run as the host's service account",
80 (_stack) =>
81 Effect.gen(function* () {
82 const out =
83 yield* expectProbe<bqdt.StartManualTransferRunsResponse>(
84 baseUrl,
85 "startManualRuns",
86 );
87 expect(out.runs).toHaveLength(1);
88 const runName = out.runs?.[0]?.name ?? "";
89 expect(runName.startsWith(`${configName}/runs/`)).toEqual(true);
90
91 const run = yield* bqdt.getProjectsLocationsTransferConfigsRuns({
92 name: runName,
93 });
94 expect(run.name).toEqual(runName);
95 expect(run.dataSourceId).toEqual("scheduled_query");
96 expect(run.runTime).toEqual("2020-01-01T00:00:00Z");
97
98 // Transfer configs have no resource-level IAM or IAM Conditions
99 // support, so the binding documents a project grant.
100 expect(yield* projectGrantsOf(hostAccount)).toEqual([
101 { role: "roles/bigquery.admin", condition: undefined },
102 ]);
103 }),
104 {
105 tags: ["provider:gcp", "provider:gcp:bigquerydatatransfer", "live"],
106 timeout: 600_000,
107 },
108 );
109 });
110 },
111);