1import * as AWS from "@/AWS";
2import * as Test from "@/Test/Alchemy";
3import * as ElastiCache from "@distilled.cloud/aws/elasticache";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import {
8 assertReplicationGroupGone,
9 getProvisionedNetwork,
10 shareProvisionedNetwork,
11} from "./ProvisionedFixture.ts";
12
13const { test, beforeAll, afterAll } = Test.make({
14 providers: AWS.providers(),
15});
16shareProvisionedNetwork({ beforeAll, afterAll });
17const listTagsWhenAvailable = (resourceName: string) =>
18 ElastiCache.listTagsForResource({ ResourceName: resourceName }).pipe(
19 Effect.retry({
20 while: (error) =>
21 (error as { _tag?: string })._tag ===
22 "InvalidReplicationGroupStateFault",
23 schedule: Schedule.max([
24 Schedule.fixed("15 seconds"),
25 Schedule.recurs(100),
26 ]),
27 }),
28 );
29
30test.provider.skipIf(!process.env.AWS_TEST_SLOW)(
31 "updates replication-group tags and VPC security groups in place",
32 (stack) =>
33 Effect.gen(function* () {
34 yield* stack.destroy();
35 const deploy = (phase: "one" | "two") =>
36 stack.deploy(
37 Effect.gen(function* () {
38 const net = yield* getProvisionedNetwork;
39 const replacementSecurityGroup = yield* AWS.EC2.SecurityGroup(
40 "ReplacementSecurityGroup",
41 {
42 vpcId: net.vpcId,
43 description: "ElastiCache replacement integration-test access",
44 tags: { fixture: "elasticache-provisioned" },
45 },
46 );
47 const cache = yield* AWS.ElastiCache.ReplicationGroup("Cache", {
48 description: "alchemy configuration cache",
49 engine: "valkey",
50 nodeType: "cache.t4g.micro",
51 subnetGroupName: net.subnetGroupName,
52 securityGroupIds: [
53 phase === "one"
54 ? net.securityGroupId
55 : replacementSecurityGroup.groupId,
56 ],
57 replicasPerNodeGroup: 0,
58 transitEncryptionEnabled: true,
59 tags:
60 phase === "one"
61 ? {
62 fixture: "elasticache-provisioned",
63 phase: "one",
64 remove: "me",
65 }
66 : { fixture: "elasticache-provisioned", phase: "two" },
67 });
68 return {
69 cache,
70 activeSecurityGroupId:
71 phase === "one"
72 ? net.securityGroupId
73 : replacementSecurityGroup.groupId,
74 };
75 }),
76 );
77
78 const created = yield* deploy("one");
79 const createdTags = yield* listTagsWhenAvailable(
80 created.cache.replicationGroupArn,
81 );
82 expect(createdTags.TagList).toContainEqual({
83 Key: "phase",
84 Value: "one",
85 });
86 expect(createdTags.TagList).toContainEqual({
87 Key: "remove",
88 Value: "me",
89 });
90
91 const updated = yield* deploy("two");
92 expect(updated.cache.replicationGroupId).toBe(
93 created.cache.replicationGroupId,
94 );
95 const updatedTags = yield* listTagsWhenAvailable(
96 updated.cache.replicationGroupArn,
97 );
98 expect(updatedTags.TagList).toContainEqual({
99 Key: "phase",
100 Value: "two",
101 });
102 expect(updatedTags.TagList).not.toContainEqual({
103 Key: "remove",
104 Value: "me",
105 });
106
107 const group = yield* ElastiCache.describeReplicationGroups({
108 ReplicationGroupId: updated.cache.replicationGroupId,
109 });
110 const memberId = group.ReplicationGroups?.[0]?.MemberClusters?.[0];
111 if (!memberId)
112 return yield* Effect.fail(
113 new Error("replication group has no member cluster"),
114 );
115 const member = yield* ElastiCache.describeCacheClusters({
116 CacheClusterId: memberId,
117 });
118 expect(
119 member.CacheClusters?.[0]?.SecurityGroups?.map(
120 (sg) => sg.SecurityGroupId,
121 ),
122 ).toEqual([updated.activeSecurityGroupId]);
123
124 yield* stack.destroy();
125 yield* assertReplicationGroupGone(updated.cache.replicationGroupId);
126 }),
127 {
128 tags: [
129 "provider:aws",
130 "provider:aws:ec2",
131 "provider:aws:elasticache",
132 "live",
133 ],
134 timeout: 2_700_000,
135 },
136);