1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8
9const { test } = Test.make({ providers: Cloudflare.providers() });
10
11const logLevel = Effect.provideService(
12 MinimumLogLevel,
13 process.env.DEBUG ? "Debug" : "Info",
14);
15
16// Resource Sharing writes are permission-gated and require a *second* account
17// or organization as recipient. On the standard testing account every write
18// under /accounts/{id}/shares fails with the typed `Forbidden` error, but the
19// read path (list shares + list recipients per share) succeeds. The deploy
20// test is therefore gated behind an env var pointing at a recipient account;
21// the read-only `list()` assertion always runs.
22const recipientAccountId =
23 process.env.CLOUDFLARE_TEST_SHARE_RECIPIENT_ACCOUNT_ID;
24
25test.provider(
26 "list() enumerates share recipients across the account's sent shares",
27 (stack) =>
28 Effect.gen(function* () {
29 yield* stack.destroy();
30
31 // Parent fan-out: list() enumerates the account's sent shares and then
32 // every recipient within each share. On a write-blocked account there
33 // may be zero shares — the result is still a well-typed array.
34 const provider = yield* Provider.findProvider(
35 Cloudflare.ResourceSharing.ShareRecipient,
36 );
37 const all = yield* provider.list();
38
39 expect(Array.isArray(all)).toBe(true);
40 for (const r of all) {
41 expect(typeof r.recipientId).toBe("string");
42 expect(typeof r.shareId).toBe("string");
43 expect(typeof r.recipientAccountId).toBe("string");
44 expect(r.associationStatus).not.toEqual("disassociated");
45 }
46
47 yield* stack.destroy();
48 }).pipe(logLevel),
49 {
50 tags: [
51 "provider:cloudflare",
52 "provider:cloudflare:resourcesharing",
53 "live",
54 ],
55 timeout: 120_000,
56 },
57);
58
59test.provider.skipIf(!recipientAccountId)(
60 "list() includes a freshly deployed ShareRecipient",
61 (stack) =>
62 Effect.gen(function* () {
63 const { accountId } = yield* yield* CloudflareEnvironment;
64 const recipient = recipientAccountId!;
65
66 yield* stack.destroy();
67
68 const policy = yield* stack.deploy(
69 Cloudflare.Gateway.Rule("RecipientListPolicy", {
70 action: "block",
71 traffic: 'dns.fqdn == "recipient-list.alchemy-test.example"',
72 filters: ["dns"],
73 enabled: false,
74 }),
75 );
76
77 const share = yield* stack.deploy(
78 Cloudflare.ResourceSharing.Share("RecipientListShare", {
79 name: "alchemy-recipient-list-share",
80 recipients: [],
81 resources: [
82 { resourceType: "gateway-policy", resourceId: policy.ruleId },
83 ],
84 }),
85 );
86
87 const deployed = yield* stack.deploy(
88 Cloudflare.ResourceSharing.ShareRecipient("RecipientListEntry", {
89 shareId: share.shareId,
90 accountId: recipient,
91 }),
92 );
93 expect(deployed.recipientId).toBeTruthy();
94 expect(deployed.accountId).toEqual(accountId);
95
96 const provider = yield* Provider.findProvider(
97 Cloudflare.ResourceSharing.ShareRecipient,
98 );
99 const all = yield* provider.list();
100
101 expect(all.some((r) => r.recipientId === deployed.recipientId)).toBe(
102 true,
103 );
104
105 yield* stack.destroy();
106 }).pipe(logLevel),
107 {
108 tags: [
109 "provider:cloudflare",
110 "provider:cloudflare:gateway",
111 "provider:cloudflare:resourcesharing",
112 "live",
113 ],
114 timeout: 180_000,
115 },
116);