1import * as AWS from "@/AWS";
2import { PlaybackKeyPair } from "@/AWS/IVS";
3import * as Test from "@/Test/Alchemy";
4import * as ivs from "@distilled.cloud/aws/ivs";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11// Generated once with `openssl ecparam -name secp384r1 -genkey | openssl
12// ec -pubout` and checked in as constants (never generated at test time).
13const PUBLIC_KEY_A = `-----BEGIN PUBLIC KEY-----
14MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAESErCxbkHXtts2QDGbIfjMpUjNtnBtHwm
15vIu3tC3Rqdv3NAcDzBadv045/QzYLFYpW1qqAMBGCASZKl+fvk+oI6ES+wV6aT1i
16vGQdcQN88zA1DeJLu2CEA5dRXmSKhP4C
17-----END PUBLIC KEY-----`;
18
19const PUBLIC_KEY_B = `-----BEGIN PUBLIC KEY-----
20MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEiS2HkhQsxtJG4i++/Na99ZwNEugpI+M/
21N0EKZtiBKnt5QhZMcyeRjcwY3zJBtrZSd8hgdyaW0HQoGtyNVNZNi54XPRq3BQUf
22r4NJCuts5JUtFVRM6ynCdqdi78ZxNUBj
23-----END PUBLIC KEY-----`;
24
25const assertKeyPairGone = (arn: string) =>
26 Effect.gen(function* () {
27 const keyPair = yield* ivs.getPlaybackKeyPair({ arn }).pipe(
28 Effect.map((r) => r.keyPair),
29 Effect.catchTag("ResourceNotFoundException", () =>
30 Effect.succeed(undefined),
31 ),
32 );
33 if (keyPair !== undefined) {
34 return yield* Effect.fail(
35 new Error(`playback key pair '${arn}' still exists`),
36 );
37 }
38 }).pipe(
39 Effect.retry({
40 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(8)]),
41 }),
42 );
43
44// Key pairs are free and provision synchronously. Covers create, no-op,
45// and the replace path (key material is immutable).
46test.provider(
47 "import, replace, and destroy a playback key pair",
48 (stack) =>
49 Effect.gen(function* () {
50 yield* stack.destroy();
51
52 const props = {
53 playbackKeyPairName: "alchemy-test-ivs-keypair",
54 publicKeyMaterial: PUBLIC_KEY_A,
55 tags: { fixture: "ivs-playback-key-pair" },
56 };
57
58 // Create (import).
59 const created = yield* stack.deploy(PlaybackKeyPair("Viewer", props));
60 expect(created.playbackKeyPairName).toBe("alchemy-test-ivs-keypair");
61 expect(created.playbackKeyPairArn).toContain(":playback-key/");
62 expect(created.fingerprint).toBeDefined();
63
64 // Out-of-band verification via distilled.
65 const observed = yield* ivs.getPlaybackKeyPair({
66 arn: created.playbackKeyPairArn,
67 });
68 expect(observed.keyPair?.name).toBe("alchemy-test-ivs-keypair");
69 expect(observed.keyPair?.fingerprint).toBe(created.fingerprint);
70 expect(observed.keyPair?.tags?.["alchemy::id"]).toBe("Viewer");
71
72 // No-op redeploy keeps the same key pair.
73 const noop = yield* stack.deploy(PlaybackKeyPair("Viewer", props));
74 expect(noop.playbackKeyPairArn).toBe(created.playbackKeyPairArn);
75 expect(noop.fingerprint).toBe(created.fingerprint);
76
77 // Changing the key material replaces the key pair — new ARN and
78 // fingerprint under the same name.
79 const replaced = yield* stack.deploy(
80 PlaybackKeyPair("Viewer", {
81 ...props,
82 publicKeyMaterial: PUBLIC_KEY_B,
83 }),
84 );
85 expect(replaced.playbackKeyPairArn).not.toBe(created.playbackKeyPairArn);
86 expect(replaced.fingerprint).not.toBe(created.fingerprint);
87 yield* assertKeyPairGone(created.playbackKeyPairArn);
88
89 // Destroy and verify out-of-band with a typed wait-until-gone.
90 yield* stack.destroy();
91 yield* assertKeyPairGone(replaced.playbackKeyPairArn);
92 }),
93 { tags: ["provider:aws", "provider:aws:ivs", "live"], timeout: 240_000 },
94);