1import * as AWS from "@/AWS";
2import * as Core from "@/Test/Core";
3import * as Test from "@/Test/Alchemy";
4import * as mwaa from "@distilled.cloud/aws/mwaa";
5import { describe, expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8import * as HttpClient from "effect/http/HttpClient";
9import MWAATestFunctionLive, { MWAATestFunction } from "./bindings-handler.ts";
10
11const testOptions = { providers: AWS.providers() };
12const { test } = Test.make(testOptions);
13
14// A well-formed-but-nonexistent environment name the ungated probes are
15// driven against.
16const NONEXISTENT_ENVIRONMENT = "alchemy-mwaa-nonexistent-probe";
17
18// ---------------------------------------------------------------------------
19// Ungated typed-error probes: every data-plane operation the four bindings
20// wrap is exercised directly through distilled against a nonexistent
21// environment, and must answer with the typed not-found tag. These prove the
22// distilled error unions (and request serialization) at near-zero cost on
23// every CI pass, while the full runtime fixture below is gated behind the
24// 20-30 minute environment provisioning.
25// ---------------------------------------------------------------------------
26
27describe(
28 "MWAA data-plane operations (typed-error probes)",
29 { tags: ["provider:aws", "provider:aws:mwaa", "live"] },
30 () => {
31 test.provider(
32 "createCliToken on a nonexistent environment fails with ResourceNotFoundException",
33 () =>
34 Effect.gen(function* () {
35 const error = yield* Effect.flip(
36 mwaa.createCliToken({ Name: NONEXISTENT_ENVIRONMENT }),
37 );
38 expect(error._tag).toBe("ResourceNotFoundException");
39 }),
40 );
41
42 test.provider(
43 "createWebLoginToken on a nonexistent environment fails with ResourceNotFoundException",
44 () =>
45 Effect.gen(function* () {
46 const error = yield* Effect.flip(
47 mwaa.createWebLoginToken({ Name: NONEXISTENT_ENVIRONMENT }),
48 );
49 expect(error._tag).toBe("ResourceNotFoundException");
50 }),
51 );
52
53 test.provider(
54 "invokeRestApi on a nonexistent environment fails with ResourceNotFoundException",
55 () =>
56 Effect.gen(function* () {
57 const error = yield* Effect.flip(
58 mwaa.invokeRestApi({
59 Name: NONEXISTENT_ENVIRONMENT,
60 Method: "GET",
61 Path: "/dags",
62 }),
63 );
64 expect(error._tag).toBe("ResourceNotFoundException");
65 }),
66 );
67 },
68);
69
70// ---------------------------------------------------------------------------
71// Full runtime fixture: a Lambda bound to all four MWAA bindings against a
72// live Airflow environment. An environment takes ~20-30 minutes to provision
73// and is billed hourly while it exists (and needs real private-subnet
74// networking supplied out of band), so this is gated behind AWS_TEST_SLOW=1
75// (same gate as the Environment lifecycle test) and always destroys what it
76// created.
77// ---------------------------------------------------------------------------
78
79const sharedStack = Core.scratchStack(testOptions, "MWAABindings");
80
81test.provider.skipIf(!process.env.AWS_TEST_SLOW)(
82 "token, REST API, and describe bindings against a live environment",
83 () =>
84 Effect.gen(function* () {
85 yield* sharedStack.destroy();
86
87 yield* Effect.gen(function* () {
88 const { functionUrl } = yield* sharedStack.deploy(
89 Effect.gen(function* () {
90 return yield* MWAATestFunction;
91 }).pipe(Effect.provide(MWAATestFunctionLive)),
92 );
93 expect(functionUrl).toBeTruthy();
94 const baseUrl = functionUrl!.replace(/\/+$/, "");
95
96 const getJson = (path: string) =>
97 HttpClient.get(`${baseUrl}${path}`).pipe(
98 Effect.flatMap((response) =>
99 response.status >= 500
100 ? Effect.fail(
101 new Error(`transient upstream ${response.status}`),
102 )
103 : Effect.succeed(response),
104 ),
105 Effect.retry({
106 schedule: Schedule.max([
107 Schedule.exponential("500 millis"),
108 Schedule.recurs(10),
109 ]),
110 }),
111 Effect.flatMap((r) => r.json),
112 );
113
114 // All four capabilities initialized in the runtime.
115 const bindings = (yield* getJson("/bindings")) as { bound: string[] };
116 expect(bindings.bound).toHaveLength(4);
117
118 // GetEnvironment — proves the environment-ARN grant + Name injection.
119 const env = (yield* getJson("/environment")) as {
120 status?: string;
121 errorTag?: string;
122 };
123 expect(env.errorTag).toBeUndefined();
124 expect(env.status).toBe("AVAILABLE");
125
126 // CreateCliToken — 60-second Airflow CLI token.
127 const cli = (yield* getJson("/cli-token")) as {
128 hasToken?: boolean;
129 hostname?: string;
130 errorTag?: string;
131 };
132 expect(cli.errorTag).toBeUndefined();
133 expect(cli.hasToken).toBe(true);
134 expect(cli.hostname).toBeTruthy();
135
136 // CreateWebLoginToken — proves the Airflow-role-ARN grant (Admin).
137 const web = (yield* getJson("/web-login-token")) as {
138 hasToken?: boolean;
139 hostname?: string;
140 errorTag?: string;
141 };
142 expect(web.errorTag).toBeUndefined();
143 expect(web.hasToken).toBe(true);
144 expect(web.hostname).toBeTruthy();
145
146 // InvokeRestApi — GET /dags through the Airflow REST API.
147 const dags = (yield* getJson("/dags")) as {
148 statusCode?: number;
149 errorTag?: string;
150 };
151 expect(dags.errorTag).toBeUndefined();
152 expect(dags.statusCode).toBe(200);
153 }).pipe(Effect.ensuring(sharedStack.destroy().pipe(Effect.orDie)));
154 }),
155 // environment create (~20-30 min) + binding checks + destroy initiation.
156 {
157 tags: [
158 "provider:aws",
159 "provider:aws:ec2",
160 "provider:aws:iam",
161 "provider:aws:lambda",
162 "provider:aws:mwaa",
163 "provider:aws:s3",
164 "live",
165 ],
166 timeout: 5_400_000,
167 },
168);