1import * as AWS from "@/AWS";
2import { VpcOrigin } from "@/AWS/CloudFront";
3import { Network } from "@/AWS/EC2/Network";
4import { SecurityGroup } from "@/AWS/EC2/SecurityGroup";
5import { LoadBalancer } from "@/AWS/ELBv2/LoadBalancer";
6import * as Provider from "@/Provider";
7import * as Test from "@/Test/Alchemy";
8import * as cloudfront from "@distilled.cloud/aws/cloudfront";
9import { describe, expect } from "alchemy-test";
10import * as Effect from "effect/Effect";
11import * as Schedule from "effect/Schedule";
12
13const { test } = Test.make({ providers: AWS.providers() });
14
15// The full lifecycle provisions a real internal ALB and a CloudFront VPC
16// origin. CloudFront VPC-origin deploy + delete is very slow (global
17// propagation, ~20-25 min for create alone), so the whole create -> Deployed
18// -> delete -> gone cycle runs ~35-40 min end to end. It is gated behind an
19// env var and given a generous timeout; the probe + list below run cheaply and
20// cover the wiring + typed-error surface in CI.
21const runLifecycle = process.env.CLOUDFRONT_TEST_VPC_ORIGIN === "1";
22
23describe(
24 "AWS.CloudFront.VpcOrigin",
25 { tags: ["provider:aws", "provider:aws:cloudfront", "live"] },
26 () => {
27 // Fast probe (no deploy): creating a VPC origin against a bogus ARN must
28 // surface a typed `InvalidArgument` (or `EntityNotFound`), proving the error
29 // typing for the create op without provisioning any infrastructure.
30 test.provider(
31 "createVpcOrigin rejects a bogus ARN with a typed error",
32 () =>
33 Effect.gen(function* () {
34 const result = yield* cloudfront
35 .createVpcOrigin({
36 VpcOriginEndpointConfig: {
37 Name: "alchemy-vpc-origin-probe",
38 Arn: "arn:aws:elasticloadbalancing:us-east-1:000000000000:loadbalancer/app/does-not-exist/0000000000000000",
39 HTTPPort: 80,
40 HTTPSPort: 443,
41 OriginProtocolPolicy: "https-only",
42 },
43 })
44 .pipe(Effect.flip);
45
46 expect([
47 "InvalidArgument",
48 "EntityNotFound",
49 "AccessDenied",
50 ]).toContain(result._tag);
51 }),
52 );
53
54 test.provider.skipIf(!runLifecycle)(
55 "create, update, and delete a VPC origin for an internal ALB",
56 (stack) =>
57 Effect.gen(function* () {
58 yield* stack.destroy();
59
60 // CloudFront VPC origins require the target's VPC to have an internet
61 // gateway attached. `Network` provisions a production-shaped VPC (VPC +
62 // attached IGW + public/private subnets across 2 AZs + route tables), so
63 // the networking + ALB is deployed in a first phase, then the VPC origin
64 // in a second — the IGW must be attached before `createVpcOrigin` runs.
65 const network = Effect.gen(function* () {
66 const net = yield* Network("VpcOriginNet", {
67 cidrBlock: "10.40.0.0/16",
68 });
69 const sg = yield* SecurityGroup("VpcOriginSg", {
70 vpcId: net.vpcId,
71 description: "alchemy vpc origin alb",
72 ingress: [
73 {
74 ipProtocol: "tcp",
75 fromPort: 80,
76 toPort: 80,
77 cidrIpv4: "0.0.0.0/0",
78 },
79 ],
80 });
81 const alb = yield* LoadBalancer("VpcOriginAlb", {
82 scheme: "internal",
83 type: "application",
84 subnets: net.publicSubnetIds,
85 securityGroups: [sg.groupId],
86 });
87 return { albArn: alb.loadBalancerArn };
88 });
89
90 // Phase 1: networking (incl. attached IGW) + ALB.
91 yield* stack.deploy(network);
92
93 // Phase 2: the VPC origin, now that the IGW is attached.
94 const deployed = yield* stack.deploy(
95 Effect.gen(function* () {
96 const { albArn } = yield* network;
97 const vpcOrigin = yield* VpcOrigin("AppVpcOrigin", {
98 arn: albArn,
99 httpPort: 80,
100 originProtocolPolicy: "http-only",
101 });
102 return { vpcOrigin };
103 }),
104 );
105
106 // Out-of-band: confirm it deployed.
107 const got = yield* cloudfront.getVpcOrigin({
108 Id: deployed.vpcOrigin.vpcOriginId,
109 });
110 expect(got.VpcOrigin?.Status).toEqual("Deployed");
111 expect(got.VpcOrigin?.VpcOriginEndpointConfig.HTTPPort).toEqual(80);
112 expect(
113 got.VpcOrigin?.VpcOriginEndpointConfig.OriginProtocolPolicy,
114 ).toEqual("http-only");
115
116 yield* stack.destroy();
117 yield* assertVpcOriginDeleted(deployed.vpcOrigin.vpcOriginId);
118 }),
119 // CloudFront VPC origin deploy + delete each take many minutes (global
120 // propagation), on top of the ALB/VPC provisioning and teardown — budget
121 // 45 min for the full create -> Deployed -> delete -> gone cycle.
122 { tags: ["provider:aws:ec2", "provider:aws:elbv2"], timeout: 2_700_000 },
123 );
124
125 test.provider.skipIf(!runLifecycle)(
126 "list enumerates account VPC origins",
127 () =>
128 Effect.gen(function* () {
129 const provider = yield* Provider.findProvider(VpcOrigin);
130 const all = yield* provider.list();
131 expect(Array.isArray(all)).toBe(true);
132 for (const item of all) {
133 expect(item.vpcOriginId).toBeDefined();
134 expect(item.vpcOriginArn).toBeDefined();
135 }
136 }),
137 );
138 },
139);
140
141const assertVpcOriginDeleted = (id: string) =>
142 cloudfront.getVpcOrigin({ Id: id }).pipe(
143 Effect.flatMap((result) =>
144 result.VpcOrigin
145 ? Effect.fail(new Error("VpcOriginStillExists"))
146 : Effect.void,
147 ),
148 Effect.catchTag("EntityNotFound", () => Effect.void),
149 Effect.retry({
150 while: (error) =>
151 error instanceof Error && error.message === "VpcOriginStillExists",
152 schedule: Schedule.max([
153 Schedule.fixed("10 seconds"),
154 Schedule.recurs(30),
155 ]),
156 }),
157 );