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 * as recaptchaenterprise from "@distilled.cloud/gcp/recaptchaenterprise_v1";
6import { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { dockerAvailable, expectProbe } from "../bindingHost.ts";
9import RecaptchaBindingsHost, { Signup } from "./fixtures/bindings-host.ts";
10
11const testOptions = { providers: GCP.providers() };
12const { test, beforeAll, afterAll } = Test.make(testOptions);
13const sharedStack = Core.scratchStack(testOptions, "RecaptchaBindings");
14
15let baseUrl: string;
16let hostAccount: string;
17let project: string;
18let keyName: string;
19let keyId: string;
20
21describe.skipIf(!dockerAvailable)(
22 "RecaptchaEnterprise Bindings",
23 {
24 tags: [
25 "provider:gcp",
26 "provider:gcp:recaptchaenterprise",
27 "provider:gcp:run",
28 "live",
29 ],
30 },
31 () => {
32 beforeAll(
33 Effect.gen(function* () {
34 yield* sharedStack.destroy();
35 const out = yield* sharedStack.deploy(
36 Effect.gen(function* () {
37 const host = yield* RecaptchaBindingsHost;
38 const key = yield* Signup;
39 return {
40 uri: host.uri,
41 serviceAccount: host.serviceAccount,
42 name: key.name,
43 keyId: key.keyId,
44 project: key.project,
45 };
46 }),
47 );
48 baseUrl = out.uri!;
49 hostAccount = out.serviceAccount!;
50 keyName = out.name;
51 keyId = out.keyId;
52 project = out.project;
53 }),
54 { timeout: 900_000 },
55 );
56
57 afterAll(sharedStack.destroy(), { timeout: 600_000 });
58
59 describe("CreateAssessment", () => {
60 test.provider(
61 "assesses a token against the key as the host's service account, granted recaptchaenterprise.agent on the project",
62 (_stack) =>
63 Effect.gen(function* () {
64 const assessment =
65 yield* expectProbe<recaptchaenterprise.GoogleCloudRecaptchaenterpriseV1Assessment>(
66 baseUrl,
67 "createAssessment",
68 );
69 expect(assessment.name).toEqual(
70 expect.stringContaining("/assessments/"),
71 );
72 expect(assessment.event?.siteKey).toEqual(keyId);
73 expect(assessment.event?.expectedAction).toEqual("login");
74 // The token is not a real reCAPTCHA token.
75 expect(assessment.tokenProperties?.valid).toEqual(false);
76
77 // The assessment exists server-side: annotating it succeeds.
78 yield* recaptchaenterprise.annotateProjectsAssessments({
79 name: assessment.name ?? "",
80 body: { annotation: "LEGITIMATE" },
81 });
82
83 // reCAPTCHA keys have no IAM policy of their own.
84 const policy = yield* crm.getIamPolicyProjects({
85 resource: `projects/${project}`,
86 body: { options: { requestedPolicyVersion: 3 } },
87 });
88 const roles = (policy.bindings ?? [])
89 .filter((binding) =>
90 (binding.members ?? []).includes(
91 `serviceAccount:${hostAccount}`,
92 ),
93 )
94 .map((binding) => ({
95 role: binding.role,
96 condition: binding.condition,
97 }));
98 expect(roles).toEqual([
99 { role: "roles/recaptchaenterprise.agent", condition: undefined },
100 ]);
101 }),
102 {
103 tags: ["provider:gcp", "provider:gcp:recaptchaenterprise", "live"],
104 timeout: 600_000,
105 },
106 );
107 });
108 },
109);