1import * as AWS from "@/AWS";
2import { PrivateDnsNamespace } from "@/AWS/CloudMap";
3import * as Test from "@/Test/Alchemy";
4import * as sd from "@distilled.cloud/aws/servicediscovery";
5import { expect } from "alchemy-test";
6import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9import { getDefaultVpc } from "../DefaultVpc.ts";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13const findNamespace = (namespaceId: string) =>
14 sd.getNamespace({ Id: namespaceId }).pipe(
15 Effect.map((r) => r.Namespace),
16 Effect.catchTag("NamespaceNotFound", () => Effect.succeed(undefined)),
17 );
18
19class NamespaceStillExists extends Data.TaggedError("NamespaceStillExists")<{
20 readonly namespaceId: string;
21}> {}
22
23const assertNamespaceDeleted = (namespaceId: string) =>
24 findNamespace(namespaceId).pipe(
25 Effect.flatMap((namespace) =>
26 namespace === undefined
27 ? Effect.void
28 : Effect.fail(new NamespaceStillExists({ namespaceId })),
29 ),
30 Effect.retry({
31 while: (e) => e._tag === "NamespaceStillExists",
32 schedule: Schedule.max([
33 Schedule.spaced("3 seconds"),
34 Schedule.recurs(20),
35 ]),
36 }),
37 );
38
39test.provider(
40 "create, update description + SOA TTL, delete private DNS namespace",
41 (stack) =>
42 Effect.gen(function* () {
43 yield* stack.destroy();
44 const vpc = yield* getDefaultVpc;
45
46 const namespace = yield* stack.deploy(
47 Effect.gen(function* () {
48 return yield* PrivateDnsNamespace("TestPrivateNamespace", {
49 name: "alchemy-test-cloudmap-private.local",
50 vpc: vpc.vpcId,
51 description: "initial description",
52 ttl: "60 seconds",
53 tags: { Environment: "test" },
54 });
55 }),
56 );
57
58 expect(namespace.namespaceId).toBeDefined();
59 expect(namespace.namespaceArn).toContain(":namespace/");
60 expect(namespace.namespaceName).toBe(
61 "alchemy-test-cloudmap-private.local",
62 );
63 // a private DNS namespace creates a Route 53 private hosted zone
64 expect(namespace.hostedZoneId).toBeDefined();
65
66 // out-of-band verification via distilled
67 const created = yield* findNamespace(namespace.namespaceId);
68 expect(created?.Type).toBe("DNS_PRIVATE");
69 expect(created?.Description).toBe("initial description");
70 expect(created?.Properties?.DnsProperties?.SOA?.TTL).toBe(60);
71 const tags = yield* sd
72 .listTagsForResource({ ResourceARN: namespace.namespaceArn })
73 .pipe(
74 Effect.map((r) =>
75 Object.fromEntries((r.Tags ?? []).map((t) => [t.Key, t.Value])),
76 ),
77 );
78 expect(tags.Environment).toBe("test");
79 expect(tags["alchemy::id"]).toBe("TestPrivateNamespace");
80
81 // update description + SOA TTL (async namespace update operation)
82 const updated = yield* stack.deploy(
83 Effect.gen(function* () {
84 return yield* PrivateDnsNamespace("TestPrivateNamespace", {
85 name: "alchemy-test-cloudmap-private.local",
86 vpc: vpc.vpcId,
87 description: "updated description",
88 ttl: "120 seconds",
89 tags: { Environment: "test" },
90 });
91 }),
92 );
93 expect(updated.namespaceId).toBe(namespace.namespaceId);
94
95 const afterUpdate = yield* findNamespace(namespace.namespaceId);
96 expect(afterUpdate?.Description).toBe("updated description");
97 expect(afterUpdate?.Properties?.DnsProperties?.SOA?.TTL).toBe(120);
98
99 yield* stack.destroy();
100 yield* assertNamespaceDeleted(namespace.namespaceId);
101 }),
102 {
103 tags: ["provider:aws", "provider:aws:cloudmap", "provider:aws:ec2", "live"],
104 timeout: 240_000,
105 },
106);