1import * as Provider from "@/Provider";
2import * as Stripe from "@/Stripe";
3import * as Test from "@/Test/Alchemy";
4import { GetWebhookEndpoint } from "@distilled.cloud/stripe/stripe";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Redacted from "effect/Redacted";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10import { isMissingStripeResource } from "@/Stripe/missing.ts";
11
12const { test } = Test.make({ providers: Stripe.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19const TEST_URL = "https://example.com/alchemy-stripe-webhook";
20const UPDATED_URL = "https://example.com/alchemy-stripe-webhook/updated";
21
22const waitUntilGone = (id: string) =>
23 GetWebhookEndpoint({ webhook_endpoint: id }).pipe(
24 Effect.as("found" as const),
25 Effect.catchIf(isMissingStripeResource, () =>
26 Effect.succeed("gone" as const),
27 ),
28 Effect.repeat({
29 schedule: Schedule.spaced("1 second"),
30 until: (status) => status === "gone",
31 times: 10,
32 }),
33 );
34
35const expectSecret = (secret: Redacted.Redacted<string> | undefined) => {
36 expect(secret).toBeDefined();
37 expect(Redacted.isRedacted(secret)).toEqual(true);
38 expect(Redacted.value(secret!).length).toBeGreaterThan(0);
39};
40
41test.provider(
42 "create, update, and delete a webhook endpoint",
43 (stack) =>
44 Effect.gen(function* () {
45 yield* stack.destroy();
46
47 const created = yield* stack.deploy(
48 Effect.gen(function* () {
49 return yield* Stripe.WebhookEndpoint("ChargesWebhook", {
50 url: TEST_URL,
51 enabledEvents: ["charge.succeeded"],
52 description: "Alchemy charge webhook",
53 metadata: { purpose: "charges" },
54 });
55 }),
56 );
57
58 expect(created.id).toMatch(/^we_/);
59 expect(created.url).toEqual(TEST_URL);
60 expect(created.enabledEvents).toEqual(["charge.succeeded"]);
61 expect(created.description).toEqual("Alchemy charge webhook");
62 expect(created.status).toEqual("enabled");
63 expect(created.connect).toEqual(false);
64 expect(created.metadata).toMatchObject({ purpose: "charges" });
65 expect(created.created).toEqual(expect.any(Number));
66 expect(created.livemode).toEqual(false);
67 expectSecret(created.secret);
68
69 const fetched = yield* GetWebhookEndpoint({
70 webhook_endpoint: created.id,
71 });
72 expect(fetched.id).toEqual(created.id);
73 expect(fetched.url).toEqual(TEST_URL);
74 expect(fetched.enabled_events).toEqual(["charge.succeeded"]);
75 expect(fetched.description).toEqual("Alchemy charge webhook");
76 expect(fetched.status).toEqual("enabled");
77 expect(fetched.metadata?.purpose).toEqual("charges");
78 expect(
79 fetched.metadata?.[Stripe.alchemyMetadataKeys.stack],
80 ).toBeDefined();
81 expect(
82 fetched.metadata?.[Stripe.alchemyMetadataKeys.stage],
83 ).toBeDefined();
84 expect(fetched.metadata?.[Stripe.alchemyMetadataKeys.id]).toBeDefined();
85
86 const updated = yield* stack.deploy(
87 Effect.gen(function* () {
88 return yield* Stripe.WebhookEndpoint("ChargesWebhook", {
89 url: UPDATED_URL,
90 enabledEvents: ["charge.succeeded", "charge.failed"],
91 description: "Alchemy charge webhook updated",
92 disabled: true,
93 metadata: { purpose: "charges", env: "test" },
94 });
95 }),
96 );
97
98 expect(updated.id).toEqual(created.id);
99 expect(updated.url).toEqual(UPDATED_URL);
100 expect(updated.enabledEvents).toEqual([
101 "charge.succeeded",
102 "charge.failed",
103 ]);
104 expect(updated.description).toEqual("Alchemy charge webhook updated");
105 expect(updated.status).toEqual("disabled");
106 expect(updated.metadata).toEqual({ purpose: "charges", env: "test" });
107 expectSecret(updated.secret);
108
109 const refetched = yield* GetWebhookEndpoint({
110 webhook_endpoint: updated.id,
111 });
112 expect(refetched.url).toEqual(UPDATED_URL);
113 expect(refetched.enabled_events).toEqual([
114 "charge.succeeded",
115 "charge.failed",
116 ]);
117 expect(refetched.description).toEqual("Alchemy charge webhook updated");
118 expect(refetched.status).toEqual("disabled");
119 expect(refetched.metadata?.purpose).toEqual("charges");
120 expect(refetched.metadata?.env).toEqual("test");
121 expect(refetched.metadata?.[Stripe.alchemyMetadataKeys.id]).toBeDefined();
122
123 yield* stack.destroy();
124
125 const gone = yield* waitUntilGone(created.id);
126 expect(gone).toEqual("gone");
127 }).pipe(logLevel),
128 {
129 tags: ["provider:stripe", "provider:stripe:webhookendpoint", "live"],
130 timeout: 120_000,
131 },
132);
133
134test.provider(
135 "list enumerates the deployed webhook endpoint",
136 (stack) =>
137 Effect.gen(function* () {
138 yield* stack.destroy();
139
140 const deployed = yield* stack.deploy(
141 Effect.gen(function* () {
142 return yield* Stripe.WebhookEndpoint("ListWebhook", {
143 url: TEST_URL,
144 enabledEvents: ["charge.succeeded"],
145 metadata: { kind: "list" },
146 });
147 }),
148 );
149
150 const provider = yield* Provider.findProvider(Stripe.WebhookEndpoint);
151 const all = yield* provider.list();
152 const found = all.find((endpoint) => endpoint.id === deployed.id);
153 expect(found).toBeDefined();
154 expect(found?.url).toEqual(TEST_URL);
155 expect(found?.metadata).toMatchObject({ kind: "list" });
156
157 yield* stack.destroy();
158
159 const gone = yield* waitUntilGone(deployed.id);
160 expect(gone).toEqual("gone");
161 }).pipe(logLevel),
162 {
163 tags: ["provider:stripe", "provider:stripe:webhookendpoint", "live"],
164 timeout: 120_000,
165 },
166);