1import * as AWS from "@/AWS";
2import { Resource as CloudControlResource } from "@/AWS/CloudControl";
3import * as Test from "@/Test/Alchemy";
4import * as CloudControl from "@distilled.cloud/aws/cloudcontrol";
5import { expect } from "alchemy-test";
6import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Redacted from "effect/Redacted";
10import * as Schedule from "effect/Schedule";
11
12const { test } = Test.make({ providers: AWS.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19class ResourceStillExists extends Data.TaggedError("ResourceStillExists") {}
20
21const readValue = (
22 properties: string | Redacted.Redacted<string> | undefined,
23): string | undefined => {
24 const raw =
25 properties === undefined || typeof properties === "string"
26 ? properties
27 : Redacted.value(properties);
28 if (raw === undefined) return undefined;
29 return (JSON.parse(raw) as { Value?: string }).Value;
30};
31
32const assertDeleted = Effect.fn(function* (name: string) {
33 yield* CloudControl.getResource({
34 TypeName: "AWS::SSM::Parameter",
35 Identifier: name,
36 }).pipe(
37 Effect.flatMap(() => Effect.fail(new ResourceStillExists())),
38 Effect.retry({
39 while: (e) => e._tag === "ResourceStillExists",
40 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
41 }),
42 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
43 );
44});
45
46// A deterministic, free SSM parameter name unique to this test.
47const paramName = "/alchemy-test/cloudcontrol/greeting";
48
49const resourceDef = (value: string) =>
50 Effect.gen(function* () {
51 const param = yield* CloudControlResource("CcParam", {
52 typeName: "AWS::SSM::Parameter",
53 desiredState: { Name: paramName, Type: "String", Value: value },
54 });
55 return { param };
56 });
57
58test.provider(
59 "create, update (JSON patch), delete an SSM parameter via Cloud Control",
60 (stack) =>
61 Effect.gen(function* () {
62 yield* stack.destroy();
63
64 // Create.
65 const { param: created } = yield* stack.deploy(resourceDef("hello"));
66 expect(created.identifier).toBe(paramName);
67 expect(created.typeName).toBe("AWS::SSM::Parameter");
68 expect(created.properties.Value).toBe("hello");
69
70 // Verify out-of-band.
71 const described = yield* CloudControl.getResource({
72 TypeName: "AWS::SSM::Parameter",
73 Identifier: paramName,
74 });
75 expect(readValue(described.ResourceDescription?.Properties)).toBe(
76 "hello",
77 );
78
79 // Update the value — a JSON Patch is computed over just the Value key.
80 const { param: updated } = yield* stack.deploy(resourceDef("world"));
81 expect(updated.identifier).toBe(paramName);
82 expect(updated.properties.Value).toBe("world");
83
84 const reDescribed = yield* CloudControl.getResource({
85 TypeName: "AWS::SSM::Parameter",
86 Identifier: paramName,
87 });
88 expect(readValue(reDescribed.ResourceDescription?.Properties)).toBe(
89 "world",
90 );
91
92 // Delete + wait gone.
93 yield* stack.destroy();
94 yield* assertDeleted(paramName);
95 }).pipe(logLevel),
96 {
97 tags: ["provider:aws", "provider:aws:cloudcontrol", "live"],
98 timeout: 240_000,
99 },
100);