1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as stream from "@distilled.cloud/cloudflare/stream";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: Cloudflare.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18/**
19 * A small, stable, publicly hosted PNG for Cloudflare to download
20 * (watermark images must be PNG, <= 2 MiB).
21 */
22const PNG_URL =
23 "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png";
24
25// Ride out 403 blips (`Forbidden`) from scoped-token propagation on the
26// test's own out-of-band verification calls.
27const getWatermark = (accountId: string, watermarkId: string) =>
28 stream.getWatermark({ accountId, identifier: watermarkId }).pipe(
29 Effect.retry({
30 while: (e) => e._tag === "Forbidden",
31 schedule: Schedule.exponential("500 millis"),
32 times: 8,
33 }),
34 );
35
36const expectGone = (accountId: string, watermarkId: string) =>
37 getWatermark(accountId, watermarkId).pipe(
38 Effect.flatMap(() => Effect.fail({ _tag: "WatermarkNotDeleted" } as const)),
39 // A missing watermark surfaces as `WatermarkNotFound` (Cloudflare
40 // error code 10003) — that's the success condition here.
41 Effect.catchTag("WatermarkNotFound", () => Effect.void),
42 Effect.retry({
43 while: (e) => e._tag === "WatermarkNotDeleted",
44 schedule: Schedule.max([
45 Schedule.exponential("500 millis"),
46 Schedule.recurs(10),
47 ]),
48 }),
49 );
50
51test.provider(
52 "create and delete a watermark with default name",
53 (stack) =>
54 Effect.gen(function* () {
55 const { accountId } = yield* yield* CloudflareEnvironment;
56
57 yield* stack.destroy();
58
59 const watermark = yield* stack.deploy(
60 Cloudflare.Stream.Watermark("DefaultWatermark", {
61 url: PNG_URL,
62 }),
63 );
64
65 expect(watermark.watermarkId).toBeTruthy();
66 expect(watermark.accountId).toEqual(accountId);
67 expect(watermark.downloadedFrom).toEqual(PNG_URL);
68 expect(watermark.position).toEqual("upperRight");
69 expect(watermark.opacity).toEqual(1.0);
70 expect(watermark.width).toBeGreaterThan(0);
71
72 const live = yield* getWatermark(accountId, watermark.watermarkId);
73 expect(live.uid).toEqual(watermark.watermarkId);
74 expect(live.name).toEqual(watermark.name);
75
76 yield* stack.destroy();
77
78 yield* expectGone(accountId, watermark.watermarkId);
79 }).pipe(logLevel),
80 {
81 tags: ["provider:cloudflare", "provider:cloudflare:stream", "live"],
82 timeout: 120_000,
83 },
84);
85
86test.provider(
87 "prop change replaces the watermark (create-only resource)",
88 (stack) =>
89 Effect.gen(function* () {
90 const { accountId } = yield* yield* CloudflareEnvironment;
91
92 yield* stack.destroy();
93
94 const initial = yield* stack.deploy(
95 Cloudflare.Stream.Watermark("ReplaceWatermark", {
96 name: "alchemy-stream-wm-replace",
97 url: PNG_URL,
98 }),
99 );
100
101 expect(initial.position).toEqual("upperRight");
102
103 // No update endpoint — changing any prop must replace (new uid).
104 const replaced = yield* stack.deploy(
105 Cloudflare.Stream.Watermark("ReplaceWatermark", {
106 name: "alchemy-stream-wm-replace",
107 url: PNG_URL,
108 position: "center",
109 opacity: 0.5,
110 }),
111 );
112
113 expect(replaced.watermarkId).not.toEqual(initial.watermarkId);
114 expect(replaced.position).toEqual("center");
115 expect(replaced.opacity).toEqual(0.5);
116
117 const live = yield* getWatermark(accountId, replaced.watermarkId);
118 expect(live.position).toEqual("center");
119 expect(live.opacity).toEqual(0.5);
120
121 // The old watermark must be gone after the replacement.
122 yield* expectGone(accountId, initial.watermarkId);
123
124 // Redeploying identical props is a no-op (same uid).
125 const noop = yield* stack.deploy(
126 Cloudflare.Stream.Watermark("ReplaceWatermark", {
127 name: "alchemy-stream-wm-replace",
128 url: PNG_URL,
129 position: "center",
130 opacity: 0.5,
131 }),
132 );
133 expect(noop.watermarkId).toEqual(replaced.watermarkId);
134
135 yield* stack.destroy();
136
137 yield* expectGone(accountId, replaced.watermarkId);
138 }).pipe(logLevel),
139 {
140 tags: ["provider:cloudflare", "provider:cloudflare:stream", "live"],
141 timeout: 120_000,
142 },
143);
144
145// Canonical `list()` test (account collection): deploy a watermark, then
146// enumerate every watermark profile in the account via the provider's
147// `list()` and assert the deployed uid is present in the exhaustively
148// paginated result.
149test.provider(
150 "list enumerates the deployed watermark",
151 (stack) =>
152 Effect.gen(function* () {
153 yield* stack.destroy();
154
155 const deployed = yield* stack.deploy(
156 Cloudflare.Stream.Watermark("ListWatermark", {
157 name: "alchemy-stream-wm-list",
158 url: PNG_URL,
159 }),
160 );
161
162 const provider = yield* Provider.findProvider(
163 Cloudflare.Stream.Watermark,
164 );
165 const all = yield* provider.list();
166
167 expect(all.some((w) => w.watermarkId === deployed.watermarkId)).toBe(
168 true,
169 );
170
171 yield* stack.destroy();
172 }).pipe(logLevel),
173 {
174 tags: ["provider:cloudflare", "provider:cloudflare:stream", "live"],
175 timeout: 120_000,
176 },
177);