1import * as AWS from "@/AWS";
2import { Profile, ProfileAssociation } from "@/AWS/Route53Profiles";
3import * as Test from "@/Test/Alchemy";
4import * as ec2 from "@distilled.cloud/aws/ec2";
5import * as profiles from "@distilled.cloud/aws/route53profiles";
6import { expect } from "alchemy-test";
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 assertAssociationGone = (profileAssociationId: string) =>
14 profiles
15 .getProfileAssociation({ ProfileAssociationId: profileAssociationId })
16 .pipe(
17 Effect.flatMap((r) =>
18 r.ProfileAssociation?.Status === "DELETING" ||
19 r.ProfileAssociation?.Status === "DELETED"
20 ? Effect.void
21 : Effect.fail(
22 new Error(`association still ${r.ProfileAssociation?.Status}`),
23 ),
24 ),
25 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
26 Effect.retry({
27 while: (e) => e instanceof Error,
28 schedule: Schedule.max([
29 Schedule.fixed("3 seconds"),
30 Schedule.recurs(10),
31 ]),
32 }),
33 );
34
35const assertProfileGone = (profileId: string) =>
36 profiles.getProfile({ ProfileId: profileId }).pipe(
37 Effect.flatMap((r) =>
38 r.Profile?.Status === "DELETING" || r.Profile?.Status === "DELETED"
39 ? Effect.void
40 : Effect.fail(new Error(`profile still ${r.Profile?.Status}`)),
41 ),
42 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
43 Effect.retry({
44 while: (e) => e instanceof Error,
45 schedule: Schedule.max([
46 Schedule.fixed("3 seconds"),
47 Schedule.recurs(10),
48 ]),
49 }),
50 );
51
52test.provider(
53 "associate a Profile with a VPC and destroy",
54 (stack) =>
55 Effect.gen(function* () {
56 yield* stack.destroy();
57
58 // A Profile association accepts any VPC in the Region. Use the
59 // standing default VPC instead of consuming a scarce VPC quota slot;
60 // because it is resolved outside the stack, destroy cannot delete it.
61 const defaultVpc = yield* getDefaultVpc;
62
63 const deployed = yield* stack.deploy(
64 Effect.gen(function* () {
65 const profile = yield* Profile("VpcDnsProfile", {});
66 const association = yield* ProfileAssociation("VpcDns", {
67 profileId: profile.profileId,
68 resourceId: defaultVpc.vpcId,
69 });
70 return { profile, association };
71 }),
72 );
73
74 expect(deployed.association.profileAssociationId).toMatch(/^rpassoc-/);
75 expect(deployed.association.profileId).toBe(deployed.profile.profileId);
76 expect(deployed.association.resourceId).toBe(defaultVpc.vpcId);
77
78 // Out-of-band: the association exists for the (profile, VPC) pair.
79 const live = yield* profiles.getProfileAssociation({
80 ProfileAssociationId: deployed.association.profileAssociationId,
81 });
82 expect(live.ProfileAssociation?.ProfileId).toBe(
83 deployed.profile.profileId,
84 );
85 expect(live.ProfileAssociation?.ResourceId).toBe(defaultVpc.vpcId);
86 expect(["CREATING", "COMPLETE"]).toContain(
87 live.ProfileAssociation?.Status,
88 );
89
90 // Re-deploying the same stack is a no-op (idempotent reconcile).
91 const again = yield* stack.deploy(
92 Effect.gen(function* () {
93 const profile = yield* Profile("VpcDnsProfile", {});
94 const association = yield* ProfileAssociation("VpcDns", {
95 profileId: profile.profileId,
96 resourceId: defaultVpc.vpcId,
97 });
98 return { association };
99 }),
100 );
101 expect(again.association.profileAssociationId).toBe(
102 deployed.association.profileAssociationId,
103 );
104
105 // Destroy tears down the association before the profile. The standing
106 // default VPC is deliberately outside the stack and must remain.
107 yield* stack.destroy();
108 yield* assertAssociationGone(deployed.association.profileAssociationId);
109 yield* assertProfileGone(deployed.profile.profileId);
110 const vpcAfter = yield* ec2.describeVpcs({
111 VpcIds: [defaultVpc.vpcId],
112 });
113 expect(vpcAfter.Vpcs?.[0]?.IsDefault).toBe(true);
114 }),
115 {
116 tags: [
117 "provider:aws",
118 "provider:aws:ec2",
119 "provider:aws:route53profiles",
120 "live",
121 ],
122 timeout: 360_000,
123 },
124);