1import * as AWS from "@/AWS";
2import { Vpc } from "@/AWS/EC2";
3import {
4 HostedZone,
5 VpcAssociationAuthorization,
6 ZoneVpcAssociation,
7} from "@/AWS/Route53";
8import * as Test from "@/Test/Alchemy";
9import * as route53 from "@distilled.cloud/aws/route-53";
10import { expect } from "alchemy-test";
11import * as Effect from "effect/Effect";
12import * as Schedule from "effect/Schedule";
13
14const { test } = Test.make({ providers: AWS.providers() });
15
16const VPC_REGION = "us-west-2";
17
18const assertZoneGone = (id: string) =>
19 route53.getHostedZone({ Id: id }).pipe(
20 Effect.flatMap(() => Effect.fail(new Error("hosted zone still exists"))),
21 Effect.catchTag("NoSuchHostedZone", () => Effect.void),
22 Effect.retry({
23 while: (e) => e instanceof Error,
24 schedule: Schedule.max([
25 Schedule.fixed("2 seconds"),
26 Schedule.recurs(10),
27 ]),
28 }),
29 );
30
31test.provider(
32 "associate a second VPC with a private zone and authorize it",
33 (stack) =>
34 Effect.gen(function* () {
35 yield* stack.destroy();
36
37 const deployed = yield* stack.deploy(
38 Effect.gen(function* () {
39 // Route 53 requires DNS support + hostnames on associated VPCs.
40 const primary = yield* Vpc("PrimaryVpc", {
41 cidrBlock: "10.61.0.0/24",
42 enableDnsSupport: true,
43 enableDnsHostnames: true,
44 });
45 const secondary = yield* Vpc("SecondaryVpc", {
46 cidrBlock: "10.62.0.0/24",
47 enableDnsSupport: true,
48 enableDnsHostnames: true,
49 });
50 const zone = yield* HostedZone("PrivateZone", {
51 name: "alchemy-test-assoc.internal",
52 privateZone: true,
53 vpc: { vpcId: primary.vpcId, vpcRegion: VPC_REGION },
54 forceDestroy: true,
55 });
56 const association = yield* ZoneVpcAssociation("SecondaryAssoc", {
57 hostedZoneId: zone.id,
58 vpcId: secondary.vpcId,
59 vpcRegion: VPC_REGION,
60 });
61 const authorization = yield* VpcAssociationAuthorization(
62 "SecondaryAuth",
63 {
64 hostedZoneId: zone.id,
65 vpcId: secondary.vpcId,
66 vpcRegion: VPC_REGION,
67 },
68 );
69 return { zone, association, authorization, secondary };
70 }),
71 );
72
73 expect(deployed.association.hostedZoneId).toBe(deployed.zone.id);
74 expect(deployed.association.vpcId).toBe(deployed.secondary.vpcId);
75 expect(deployed.authorization.vpcId).toBe(deployed.secondary.vpcId);
76
77 // Out-of-band: the zone reports both VPCs.
78 const detail = yield* route53.getHostedZone({ Id: deployed.zone.id });
79 const vpcIds = (detail.VPCs ?? []).map((vpc) => vpc.VPCId);
80 expect(vpcIds).toContain(deployed.secondary.vpcId);
81 expect(vpcIds).toHaveLength(2);
82
83 // Out-of-band: the authorization is listed for the zone.
84 const authorizations = yield* route53.listVPCAssociationAuthorizations({
85 HostedZoneId: deployed.zone.id,
86 });
87 expect((authorizations.VPCs ?? []).map((vpc) => vpc.VPCId)).toContain(
88 deployed.secondary.vpcId,
89 );
90
91 // Removing just the association (keeping zone + VPCs) disassociates.
92 const remaining = yield* stack.deploy(
93 Effect.gen(function* () {
94 const primary = yield* Vpc("PrimaryVpc", {
95 cidrBlock: "10.61.0.0/24",
96 enableDnsSupport: true,
97 enableDnsHostnames: true,
98 });
99 const secondary = yield* Vpc("SecondaryVpc", {
100 cidrBlock: "10.62.0.0/24",
101 enableDnsSupport: true,
102 enableDnsHostnames: true,
103 });
104 const zone = yield* HostedZone("PrivateZone", {
105 name: "alchemy-test-assoc.internal",
106 privateZone: true,
107 vpc: { vpcId: primary.vpcId, vpcRegion: VPC_REGION },
108 forceDestroy: true,
109 });
110 return { zone, secondary };
111 }),
112 );
113
114 const detailAfter = yield* route53.getHostedZone({
115 Id: remaining.zone.id,
116 });
117 const vpcIdsAfter = (detailAfter.VPCs ?? []).map((vpc) => vpc.VPCId);
118 expect(vpcIdsAfter).not.toContain(remaining.secondary.vpcId);
119 expect(vpcIdsAfter).toHaveLength(1);
120
121 const authorizationsAfter =
122 yield* route53.listVPCAssociationAuthorizations({
123 HostedZoneId: remaining.zone.id,
124 });
125 expect(authorizationsAfter.VPCs ?? []).toHaveLength(0);
126
127 yield* stack.destroy();
128 yield* assertZoneGone(deployed.zone.id);
129 }),
130 {
131 tags: ["provider:aws", "provider:aws:ec2", "provider:aws:route53", "live"],
132 timeout: 180_000,
133 },
134);