1import * as AWS from "@/AWS";
2import { SignalingChannel } from "@/AWS/KinesisVideo";
3import * as Test from "@/Test/Alchemy";
4import * as kv from "@distilled.cloud/aws/kinesis-video";
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 findChannel = (channelName: string) =>
13 kv.describeSignalingChannel({ ChannelName: channelName }).pipe(
14 Effect.map((r) => r.ChannelInfo),
15 Effect.catchTag("ResourceNotFoundException", () =>
16 Effect.succeed(undefined),
17 ),
18 );
19
20class ChannelStillExists extends Data.TaggedError("ChannelStillExists")<{
21 readonly channelName: string;
22}> {}
23
24// DeleteSignalingChannel is asynchronous: the channel lingers in DELETING
25// for a while — treat DELETING as deleted.
26const assertChannelDeleted = (channelName: string) =>
27 findChannel(channelName).pipe(
28 Effect.flatMap((info) =>
29 info === undefined || info.ChannelStatus === "DELETING"
30 ? Effect.void
31 : Effect.fail(new ChannelStillExists({ channelName })),
32 ),
33 Effect.retry({
34 while: (e) => e._tag === "ChannelStillExists",
35 schedule: Schedule.max([
36 Schedule.spaced("3 seconds"),
37 Schedule.recurs(20),
38 ]),
39 }),
40 );
41
42test.provider(
43 "create, update message TTL and tags, delete signaling channel",
44 (stack) =>
45 Effect.gen(function* () {
46 yield* stack.destroy();
47
48 const channel = yield* stack.deploy(
49 Effect.gen(function* () {
50 return yield* SignalingChannel("TestSignalingChannel", {
51 tags: { Environment: "test" },
52 });
53 }),
54 );
55
56 expect(channel.channelName).toBeDefined();
57 expect(channel.channelArn).toContain(":channel/");
58
59 // out-of-band verification via distilled
60 const created = yield* findChannel(channel.channelName);
61 expect(created?.ChannelStatus).toBe("ACTIVE");
62 expect(created?.ChannelType).toBe("SINGLE_MASTER");
63 const tags = yield* kv
64 .listTagsForResource({ ResourceARN: channel.channelArn })
65 .pipe(Effect.map((r) => r.Tags ?? {}));
66 expect(tags.Environment).toBe("test");
67 expect(tags["alchemy::id"]).toBe("TestSignalingChannel");
68
69 // update the message TTL in place
70 const updated = yield* stack.deploy(
71 Effect.gen(function* () {
72 return yield* SignalingChannel("TestSignalingChannel", {
73 messageTtl: "30 seconds",
74 tags: { Environment: "test", Extra: "yes" },
75 });
76 }),
77 );
78 expect(updated.channelName).toBe(channel.channelName);
79 expect(updated.channelArn).toBe(channel.channelArn);
80
81 const afterUpdate = yield* findChannel(channel.channelName);
82 expect(afterUpdate?.SingleMasterConfiguration?.MessageTtlSeconds).toBe(
83 30,
84 );
85 const tagsAfter = yield* kv
86 .listTagsForResource({ ResourceARN: channel.channelArn })
87 .pipe(Effect.map((r) => r.Tags ?? {}));
88 expect(tagsAfter.Extra).toBe("yes");
89
90 yield* stack.destroy();
91 yield* assertChannelDeleted(channel.channelName);
92 }),
93 {
94 tags: ["provider:aws", "provider:aws:kinesisvideo", "live"],
95 timeout: 240_000,
96 },
97);
98
99test.provider(
100 "explicit name and replacement on rename",
101 (stack) =>
102 Effect.gen(function* () {
103 yield* stack.destroy();
104
105 const first = yield* stack.deploy(
106 Effect.gen(function* () {
107 return yield* SignalingChannel("NamedSignalingChannel", {
108 channelName: "alchemy-test-kvs-channel-a",
109 });
110 }),
111 );
112 expect(first.channelName).toBe("alchemy-test-kvs-channel-a");
113
114 // renaming triggers a replacement: new physical channel, old gone
115 const second = yield* stack.deploy(
116 Effect.gen(function* () {
117 return yield* SignalingChannel("NamedSignalingChannel", {
118 channelName: "alchemy-test-kvs-channel-b",
119 });
120 }),
121 );
122 expect(second.channelName).toBe("alchemy-test-kvs-channel-b");
123 expect(second.channelArn).not.toBe(first.channelArn);
124
125 yield* assertChannelDeleted(first.channelName);
126
127 yield* stack.destroy();
128 yield* assertChannelDeleted(second.channelName);
129 }),
130 {
131 tags: ["provider:aws", "provider:aws:kinesisvideo", "live"],
132 timeout: 240_000,
133 },
134);