1import * as AWS from "@/AWS";
2import * as Core from "@/Test/Core";
3import * as Test from "@/Test/Alchemy";
4import * as glacier from "@distilled.cloud/aws/glacier";
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 GlacierBindingsFunctionLive, {
10 GlacierBindingsFunction,
11} from "./bindings-handler";
12
13const testOptions = { providers: AWS.providers() };
14const { test, beforeAll, afterAll } = Test.make(testOptions);
15
16// The vault-based S3 Glacier API rejects accounts created after its 2025
17// closure to new customers with the typed NoLongerSupportedException (see
18// the ungated probes below and in Vault.test.ts). The live Lambda E2E needs
19// a real vault, so it is gated behind AWS_TEST_GLACIER=1 for entitled
20// (grandfathered) accounts.
21const RUN_LIVE = !!process.env.AWS_TEST_GLACIER;
22
23// Ungated typed-error probes: prove the distilled error unions the bindings
24// depend on are typed on every account, entitled or not, at near-zero cost.
25test.provider(
26 "listJobs on a nonexistent vault fails with a typed tag",
27 () =>
28 Effect.gen(function* () {
29 const error = yield* Effect.flip(
30 glacier.listJobs({
31 accountId: "-",
32 vaultName: "alchemy-nonexistent-glacier-vault-probe",
33 }),
34 );
35 expect([
36 "ResourceNotFoundException",
37 "NoLongerSupportedException",
38 ]).toContain(error._tag);
39 }),
40 { tags: ["provider:aws", "provider:aws:glacier", "live"] },
41);
42
43test.provider(
44 "uploadArchive on a nonexistent vault fails with a typed tag",
45 () =>
46 Effect.gen(function* () {
47 const error = yield* Effect.flip(
48 glacier.uploadArchive({
49 accountId: "-",
50 vaultName: "alchemy-nonexistent-glacier-vault-probe",
51 body: "alchemy-glacier-probe",
52 }),
53 );
54 expect([
55 "ResourceNotFoundException",
56 "NoLongerSupportedException",
57 "MissingParameterValueException",
58 "InvalidParameterValueException",
59 ]).toContain(error._tag);
60 }),
61 { tags: ["provider:aws", "provider:aws:glacier", "live"] },
62);
63
64const sharedStack = Core.scratchStack(testOptions, "GlacierBindings");
65
66let baseUrl: string;
67
68const get = (path: string) =>
69 HttpClient.get(`${baseUrl}${path}`).pipe(Effect.flatMap((r) => r.json));
70const post = (path: string) =>
71 HttpClient.post(`${baseUrl}${path}`).pipe(Effect.flatMap((r) => r.json));
72
73describe(
74 "Glacier Bindings (E2E)",
75 {
76 tags: [
77 "provider:aws",
78 "provider:aws:glacier",
79 "provider:aws:lambda",
80 "live",
81 ],
82 },
83 () => {
84 beforeAll(
85 Effect.gen(function* () {
86 if (!RUN_LIVE) return;
87 yield* Effect.logInfo("Glacier E2E setup: destroying previous run");
88 yield* sharedStack.destroy();
89
90 yield* Effect.logInfo("Glacier E2E setup: deploying vault + Lambda");
91 const { functionUrl } = yield* sharedStack.deploy(
92 Effect.gen(function* () {
93 return yield* GlacierBindingsFunction;
94 }).pipe(Effect.provide(GlacierBindingsFunctionLive)),
95 );
96
97 expect(functionUrl).toBeTruthy();
98 baseUrl = functionUrl!.replace(/\/+$/, "");
99
100 // Readiness probe — fresh function URLs take seconds to serve 200s.
101 yield* HttpClient.get(`${baseUrl}/bindings`).pipe(
102 Effect.flatMap((response) =>
103 response.status === 200
104 ? Effect.succeed(response)
105 : Effect.fail(
106 new Error(`Function not ready: ${response.status}`),
107 ),
108 ),
109 Effect.retry({
110 schedule: Schedule.max([
111 Schedule.fixed("2 seconds"),
112 Schedule.recurs(60),
113 ]),
114 }),
115 );
116 }),
117 { timeout: 300_000 },
118 );
119 afterAll(
120 Effect.gen(function* () {
121 if (!RUN_LIVE) return;
122 yield* sharedStack.destroy();
123 }),
124 { timeout: 300_000 },
125 );
126
127 test.provider.skipIf(!RUN_LIVE)(
128 "all 13 capabilities initialize in the runtime",
129 () =>
130 Effect.gen(function* () {
131 const response = (yield* get("/bindings")) as any;
132 expect(response.bound).toHaveLength(13);
133 }),
134 );
135
136 test.provider.skipIf(!RUN_LIVE)(
137 "DescribeVault reads the bound vault's stats",
138 () =>
139 Effect.gen(function* () {
140 const response = (yield* get("/vault")) as any;
141 expect(typeof response.vaultName).toBe("string");
142 expect(response.numberOfArchives).toBe(0);
143 }),
144 );
145
146 test.provider.skipIf(!RUN_LIVE)(
147 "ListJobs + ListMultipartUploads round-trip against the empty vault",
148 () =>
149 Effect.gen(function* () {
150 const jobs = (yield* get("/jobs")) as any;
151 expect(jobs.count).toBe(0);
152 const uploads = (yield* get("/uploads")) as any;
153 expect(uploads.count).toBe(0);
154 }),
155 );
156
157 test.provider.skipIf(!RUN_LIVE)(
158 "DescribeJob, GetJobOutput, and ListParts surface the typed not-found",
159 () =>
160 Effect.gen(function* () {
161 const job = (yield* get("/jobs/typed-not-found")) as any;
162 expect(job.typed).toBe(true);
163 const output = (yield* get("/job-output/typed-not-found")) as any;
164 expect(output.typed).toBe(true);
165 const parts = (yield* get("/parts/typed-not-found")) as any;
166 expect(parts.typed).toBe(true);
167 }),
168 );
169
170 test.provider.skipIf(!RUN_LIVE)(
171 "multipart mutation bindings surface the typed not-found",
172 () =>
173 Effect.gen(function* () {
174 const response = (yield* post("/multipart/typed-not-found")) as any;
175 expect(response.upload).toBe(true);
176 expect(response.complete).toBe(true);
177 expect(response.abort).toBe(true);
178 }),
179 );
180
181 test.provider.skipIf(!RUN_LIVE)(
182 "DeleteArchive and UploadArchive round-trip typed rejections",
183 () =>
184 Effect.gen(function* () {
185 const deleted = (yield* post("/archive/typed-not-found")) as any;
186 expect(deleted.typed).toBe(true);
187 // Wrong checksum — rejected before storing, vault stays deletable.
188 const uploaded = (yield* post("/archive/probe")) as any;
189 expect(uploaded.typed).toBe(true);
190 const multipart = (yield* post("/multipart/probe")) as any;
191 expect(multipart.typed).toBe(true);
192 }),
193 );
194
195 test.provider.skipIf(!RUN_LIVE)(
196 "InitiateJob starts (or typed-rejects) an inventory retrieval",
197 () =>
198 Effect.gen(function* () {
199 const response = (yield* post("/jobs/inventory")) as any;
200 // A fresh vault has no inventory yet — either the job starts or the
201 // typed 400 comes back; both prove the grant end-to-end.
202 if (response.started) {
203 expect(typeof response.jobId).toBe("string");
204 } else {
205 expect([
206 "InvalidParameterValueException",
207 "ResourceNotFoundException",
208 ]).toContain(response.error);
209 }
210 }),
211 );
212 },
213);