1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as kms from "@distilled.cloud/gcp/cloudkms_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import { GcpEnvironment } from "@/GCP/Environment";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17const INSTANCE_ID = "alchemy-sthsm-does-not-exist";
18const PROPOSAL_ID = "alchemy-test-sthsm-proposal";
19const instanceNameOf = (project: string) =>
20 `projects/${project}/locations/us-central1/singleTenantHsmInstances/${INSTANCE_ID}`;
21const missingProposalNameOf = (project: string) =>
22 `${instanceNameOf(project)}/proposals/alchemy-sthsm-proposal-does-not-exist`;
23
24// Creating a SingleTenantHsmInstance succeeds as an LRO then lands in
25// FAILED (`quorumAuth` without 2FA registration). Proposals against a
26// FAILED instance return BadRequest (`FAILED_PRECONDITION`: "Can not
27// create proposal for a FAILED SingleTenantHsmInstance."). A missing
28// parent returns NotFound. Set GCP_TEST_KMS_HSM=1 and
29// GCP_TEST_KMS_HSM_INSTANCE to a working instance to run the lifecycle.
30const runLifecycle =
31 !process.env.FAST &&
32 process.env.GCP_TEST_KMS_HSM === "1" &&
33 !!process.env.GCP_TEST_KMS_HSM_INSTANCE;
34const entitledInstance = process.env.GCP_TEST_KMS_HSM_INSTANCE ?? "";
35
36const waitUntilGone = (name: string) =>
37 kms.getProjectsLocationsSingleTenantHsmInstancesProposals({ name }).pipe(
38 Effect.as("found" as const),
39 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
40 Effect.repeat({
41 schedule: Schedule.spaced("1 second"),
42 until: (status) => status === "gone",
43 times: 10,
44 }),
45 );
46
47test.provider(
48 "getProjectsLocationsSingleTenantHsmInstancesProposals on a missing proposal fails with a typed tag",
49 (stack) =>
50 Effect.gen(function* () {
51 const { project } = yield* GcpEnvironment.current;
52 const missingProposalName = missingProposalNameOf(project);
53 yield* stack.destroy();
54
55 const error = yield* Effect.flip(
56 kms.getProjectsLocationsSingleTenantHsmInstancesProposals({
57 name: missingProposalName,
58 }),
59 );
60 expect(error._tag).toBe("NotFound");
61
62 const gone = yield* waitUntilGone(missingProposalName);
63 expect(gone).toEqual("gone");
64
65 yield* stack.destroy();
66 }).pipe(logLevel),
67 { tags: ["provider:gcp", "provider:gcp:kms", "live"], timeout: 90_000 },
68);
69
70test.provider.skipIf(runLifecycle)(
71 "create is rejected with NotFound when the parent HSM instance is missing",
72 (stack) =>
73 Effect.gen(function* () {
74 const { project } = yield* GcpEnvironment.current;
75 const instanceName = instanceNameOf(project);
76 yield* stack.destroy();
77
78 const error = yield* Effect.flip(
79 stack.deploy(
80 Effect.gen(function* () {
81 return yield* GCP.KMS.SingleTenantHsmInstanceProposal("Refresh", {
82 singleTenantHsmInstance: instanceName,
83 proposalId: PROPOSAL_ID,
84 refreshSingleTenantHsmInstance: true,
85 });
86 }),
87 ),
88 );
89 expect(error._tag).toBe("NotFound");
90
91 yield* stack.destroy();
92 }).pipe(logLevel),
93 { tags: ["provider:gcp", "provider:gcp:kms", "live"], timeout: 90_000 },
94);
95
96test.provider.skipIf(!runLifecycle)(
97 "create and delete a single-tenant HSM instance proposal",
98 (stack) =>
99 Effect.gen(function* () {
100 const { project } = yield* GcpEnvironment.current;
101 yield* stack.destroy();
102
103 const created = yield* stack.deploy(
104 Effect.gen(function* () {
105 return yield* GCP.KMS.SingleTenantHsmInstanceProposal("Refresh", {
106 singleTenantHsmInstance: entitledInstance,
107 proposalId: PROPOSAL_ID,
108 refreshSingleTenantHsmInstance: true,
109 });
110 }),
111 );
112
113 expect(created.proposalId).toEqual(PROPOSAL_ID);
114 expect(created.name).toContain(`/proposals/${PROPOSAL_ID}`);
115 expect(created.refreshSingleTenantHsmInstance).toEqual(true);
116 expect(created.project).toEqual(project);
117
118 const fetched =
119 yield* kms.getProjectsLocationsSingleTenantHsmInstancesProposals({
120 name: created.name,
121 });
122 expect(fetched.name).toEqual(created.name);
123
124 yield* stack.destroy();
125
126 const gone = yield* waitUntilGone(created.name);
127 expect(gone).toEqual("gone");
128 }).pipe(logLevel),
129 { tags: ["provider:gcp", "provider:gcp:kms", "live"], timeout: 90_000 },
130);