1import * as AWS from "@/AWS";
2import { HttpNamespace } 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";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12const findNamespace = (namespaceId: string) =>
13 sd.getNamespace({ Id: namespaceId }).pipe(
14 Effect.map((r) => r.Namespace),
15 Effect.catchTag("NamespaceNotFound", () => Effect.succeed(undefined)),
16 );
17
18class NamespaceStillExists extends Data.TaggedError("NamespaceStillExists")<{
19 readonly namespaceId: string;
20}> {}
21
22const assertNamespaceDeleted = (namespaceId: string) =>
23 findNamespace(namespaceId).pipe(
24 Effect.flatMap((namespace) =>
25 namespace === undefined
26 ? Effect.void
27 : Effect.fail(new NamespaceStillExists({ namespaceId })),
28 ),
29 Effect.retry({
30 while: (e) => e._tag === "NamespaceStillExists",
31 schedule: Schedule.max([
32 Schedule.spaced("3 seconds"),
33 Schedule.recurs(20),
34 ]),
35 }),
36 );
37
38test.provider(
39 "create, update description, delete HTTP namespace",
40 (stack) =>
41 Effect.gen(function* () {
42 yield* stack.destroy();
43
44 const namespace = yield* stack.deploy(
45 Effect.gen(function* () {
46 return yield* HttpNamespace("TestHttpNamespace", {
47 description: "initial description",
48 tags: { Environment: "test" },
49 });
50 }),
51 );
52
53 expect(namespace.namespaceId).toBeDefined();
54 expect(namespace.namespaceArn).toContain(":namespace/");
55 expect(namespace.httpName).toBeDefined();
56
57 // out-of-band verification via distilled
58 const created = yield* findNamespace(namespace.namespaceId);
59 expect(created?.Type).toBe("HTTP");
60 expect(created?.Description).toBe("initial description");
61 const tags = yield* sd
62 .listTagsForResource({ ResourceARN: namespace.namespaceArn })
63 .pipe(
64 Effect.map((r) =>
65 Object.fromEntries((r.Tags ?? []).map((t) => [t.Key, t.Value])),
66 ),
67 );
68 expect(tags.Environment).toBe("test");
69 expect(tags["alchemy::id"]).toBe("TestHttpNamespace");
70
71 // update the description (async namespace update operation)
72 const updated = yield* stack.deploy(
73 Effect.gen(function* () {
74 return yield* HttpNamespace("TestHttpNamespace", {
75 description: "updated description",
76 tags: { Environment: "test", Extra: "yes" },
77 });
78 }),
79 );
80 expect(updated.namespaceId).toBe(namespace.namespaceId);
81 expect(updated.namespaceArn).toBe(namespace.namespaceArn);
82
83 const afterUpdate = yield* findNamespace(namespace.namespaceId);
84 expect(afterUpdate?.Description).toBe("updated description");
85 const tagsAfter = yield* sd
86 .listTagsForResource({ ResourceARN: namespace.namespaceArn })
87 .pipe(
88 Effect.map((r) =>
89 Object.fromEntries((r.Tags ?? []).map((t) => [t.Key, t.Value])),
90 ),
91 );
92 expect(tagsAfter.Extra).toBe("yes");
93
94 yield* stack.destroy();
95 yield* assertNamespaceDeleted(namespace.namespaceId);
96 }),
97 { tags: ["provider:aws", "provider:aws:cloudmap", "live"], timeout: 240_000 },
98);
99
100test.provider(
101 "explicit name and replacement on rename",
102 (stack) =>
103 Effect.gen(function* () {
104 yield* stack.destroy();
105
106 const first = yield* stack.deploy(
107 Effect.gen(function* () {
108 return yield* HttpNamespace("NamedHttpNamespace", {
109 name: "alchemy-test-cloudmap-http-a",
110 });
111 }),
112 );
113 expect(first.namespaceName).toBe("alchemy-test-cloudmap-http-a");
114
115 // renaming triggers a replacement: new physical namespace, old gone
116 const second = yield* stack.deploy(
117 Effect.gen(function* () {
118 return yield* HttpNamespace("NamedHttpNamespace", {
119 name: "alchemy-test-cloudmap-http-b",
120 });
121 }),
122 );
123 expect(second.namespaceName).toBe("alchemy-test-cloudmap-http-b");
124 expect(second.namespaceId).not.toBe(first.namespaceId);
125
126 yield* assertNamespaceDeleted(first.namespaceId);
127
128 yield* stack.destroy();
129 yield* assertNamespaceDeleted(second.namespaceId);
130 }),
131 { tags: ["provider:aws", "provider:aws:cloudmap", "live"], timeout: 240_000 },
132);