1import * as Provider from "@/Provider";
2import * as Stripe from "@/Stripe";
3import * as Test from "@/Test/Alchemy";
4import { Credentials } from "@distilled.cloud/stripe";
5import {
6 GetFileLink,
7 GetFiles,
8 CreateFile,
9} from "@distilled.cloud/stripe/stripe";
10import { expect } from "alchemy-test";
11import * as Effect from "effect/Effect";
12import { MinimumLogLevel } from "effect/References";
13import * as Result from "effect/Result";
14import * as Schedule from "effect/Schedule";
15import { isMissingStripeResource } from "@/Stripe/missing.ts";
16
17const { test } = Test.make({ providers: Stripe.providers() });
18
19const logLevel = Effect.provideService(
20 MinimumLogLevel,
21 process.env.DEBUG ? "Debug" : "Info",
22);
23
24const FILES_API_BASE_URL = "https://files.stripe.com";
25
26/** 1×1 PNG — Stripe accepts PNG for `dispute_evidence`. */
27const PNG_1X1 = new Uint8Array([
28 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49,
29 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02,
30 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44,
31 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00, 0x00, 0x00, 0x03, 0x00,
32 0x01, 0x18, 0xdd, 0x8d, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44,
33 0xae, 0x42, 0x60, 0x82,
34]);
35
36/** 2030-03-17 — Stripe rejects expiries more than five years out. */
37const EXPIRES_AT = 1_900_000_000;
38/** 2030-11-01 */
39const EXPIRES_AT_UPDATED = 1_920_000_000;
40
41const isMissing = isMissingStripeResource;
42
43const waitUntilExpired = (id: string) =>
44 GetFileLink({ link: id }).pipe(
45 Effect.map((link) =>
46 link.expired ? ("expired" as const) : ("active" as const),
47 ),
48 Effect.catchIf(isMissing, () => Effect.succeed("gone" as const)),
49 Effect.repeat({
50 schedule: Schedule.spaced("1 second"),
51 until: (status) => status === "expired" || status === "gone",
52 times: 10,
53 }),
54 );
55
56const FILE_PURPOSE = "dispute_evidence" as const;
57const FILE_NAME = "alchemy-filelink.png";
58
59const postFile = (file: File) =>
60 Effect.gen(function* () {
61 const resolve = yield* Credentials;
62 const creds = yield* resolve;
63 return yield* CreateFile({
64 file: file as unknown as string,
65 purpose: FILE_PURPOSE,
66 }).pipe(
67 Effect.provideService(
68 Credentials,
69 Effect.succeed({
70 apiKey: creds.apiKey,
71 apiBaseUrl: FILES_API_BASE_URL,
72 }),
73 ),
74 );
75 });
76
77/**
78 * Files cannot be deleted. Reuse a prior Alchemy-uploaded PNG when present;
79 * otherwise upload a 1×1 PNG via distilled. Do not reuse arbitrary account
80 * files — Stripe-generated files often reject `file_links` creates.
81 */
82const ensureFile = Effect.gen(function* () {
83 const listed = yield* GetFiles({
84 purpose: FILE_PURPOSE,
85 limit: 100,
86 });
87 const existing = listed.data.find((file) => file.filename === FILE_NAME);
88 if (existing !== undefined) return existing;
89 const png = yield* Effect.sync(
90 () => new File([PNG_1X1], FILE_NAME, { type: "image/png" }),
91 );
92 return yield* postFile(png);
93});
94
95test.provider(
96 "file upload probe",
97 (stack) =>
98 Effect.gen(function* () {
99 yield* stack.destroy();
100
101 const result = yield* ensureFile.pipe(Effect.result);
102 if (Result.isSuccess(result)) {
103 expect(result.success.id).toMatch(/^file_/);
104 } else {
105 expect([
106 "InvalidRequestError",
107 "Forbidden",
108 "Unauthorized",
109 "BadRequest",
110 ]).toContain(result.failure._tag);
111 }
112
113 yield* stack.destroy();
114 }).pipe(logLevel),
115 {
116 tags: ["provider:stripe", "provider:stripe:filelink", "live"],
117 timeout: 60_000,
118 },
119);
120
121test.provider(
122 "create, update, and expire a file link",
123 (stack) =>
124 Effect.gen(function* () {
125 yield* stack.destroy();
126
127 const file = yield* ensureFile;
128
129 const created = yield* stack.deploy(
130 Effect.gen(function* () {
131 return yield* Stripe.FileLink("ReportLink", {
132 file: file.id,
133 metadata: { kind: "report" },
134 });
135 }),
136 );
137
138 expect(created.id).toMatch(/^link_/);
139 expect(created.file).toEqual(file.id);
140 expect(created.expired).toEqual(false);
141 expect(created.expiresAt).toBeUndefined();
142 expect(created.url).toEqual(expect.any(String));
143 expect(created.metadata).toMatchObject({ kind: "report" });
144 expect(created.created).toEqual(expect.any(Number));
145 expect(created.livemode).toEqual(false);
146
147 const fetched = yield* GetFileLink({ link: created.id });
148 expect(fetched.id).toEqual(created.id);
149 expect(
150 typeof fetched.file === "string" ? fetched.file : fetched.file.id,
151 ).toEqual(file.id);
152 expect(fetched.expired).toEqual(false);
153 expect(fetched.metadata?.kind).toEqual("report");
154 expect(
155 fetched.metadata?.[Stripe.alchemyMetadataKeys.stack],
156 ).toBeDefined();
157 expect(
158 fetched.metadata?.[Stripe.alchemyMetadataKeys.stage],
159 ).toBeDefined();
160 expect(fetched.metadata?.[Stripe.alchemyMetadataKeys.id]).toBeDefined();
161
162 const updated = yield* stack.deploy(
163 Effect.gen(function* () {
164 return yield* Stripe.FileLink("ReportLink", {
165 file: file.id,
166 expiresAt: EXPIRES_AT,
167 metadata: { kind: "report", env: "test" },
168 });
169 }),
170 );
171
172 expect(updated.id).toEqual(created.id);
173 expect(updated.file).toEqual(file.id);
174 expect(updated.expired).toEqual(false);
175 expect(updated.expiresAt).toEqual(EXPIRES_AT);
176 expect(updated.metadata).toEqual({ kind: "report", env: "test" });
177
178 const refetched = yield* GetFileLink({ link: updated.id });
179 expect(refetched.expires_at).toEqual(EXPIRES_AT);
180 expect(refetched.metadata?.kind).toEqual("report");
181 expect(refetched.metadata?.env).toEqual("test");
182 expect(refetched.metadata?.[Stripe.alchemyMetadataKeys.id]).toBeDefined();
183
184 const extended = yield* stack.deploy(
185 Effect.gen(function* () {
186 return yield* Stripe.FileLink("ReportLink", {
187 file: file.id,
188 expiresAt: EXPIRES_AT_UPDATED,
189 metadata: { kind: "invoice" },
190 });
191 }),
192 );
193
194 expect(extended.id).toEqual(created.id);
195 expect(extended.expiresAt).toEqual(EXPIRES_AT_UPDATED);
196 expect(extended.metadata).toEqual({ kind: "invoice" });
197
198 yield* stack.destroy();
199
200 const expired = yield* waitUntilExpired(created.id);
201 expect(expired).toEqual("expired");
202 }).pipe(logLevel),
203 {
204 tags: ["provider:stripe", "provider:stripe:filelink", "live"],
205 timeout: 120_000,
206 },
207);
208
209test.provider(
210 "list enumerates the deployed file link",
211 (stack) =>
212 Effect.gen(function* () {
213 yield* stack.destroy();
214
215 const file = yield* ensureFile;
216
217 const deployed = yield* stack.deploy(
218 Effect.gen(function* () {
219 return yield* Stripe.FileLink("ListLink", {
220 file: file.id,
221 metadata: { kind: "list" },
222 });
223 }),
224 );
225
226 const provider = yield* Provider.findProvider(Stripe.FileLink);
227 const all = yield* provider.list();
228 const found = all.find((link) => link.id === deployed.id);
229 expect(found).toBeDefined();
230 expect(found?.file).toEqual(file.id);
231 expect(found?.metadata).toMatchObject({ kind: "list" });
232
233 yield* stack.destroy();
234
235 const expired = yield* waitUntilExpired(deployed.id);
236 expect(expired).toEqual("expired");
237 }).pipe(logLevel),
238 {
239 tags: ["provider:stripe", "provider:stripe:filelink", "live"],
240 timeout: 120_000,
241 },
242);