@efaj/pulumi-dynamic-stripe 0.0.2 → 0.1.0-252fa04

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,322 +1,891 @@
1
- import { StripePaymentLinkProvider, StripeProductProvider, StripePriceProvider } from "../src/index";
2
1
  import Stripe from "stripe";
3
- import { describe, it, expect, jest, beforeEach } from '@jest/globals';
2
+ import type { Mock } from "vitest";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import {
5
+ StripePaymentLinkProvider,
6
+ StripePriceProvider,
7
+ StripeProductProvider,
8
+ } from "../src/index";
4
9
 
5
10
  // Mock the Stripe SDK
6
- jest.mock("stripe");
11
+ vi.mock("stripe");
7
12
 
8
- const MockedStripe = Stripe as jest.MockedClass<typeof Stripe>;
13
+ const MockedStripe = Stripe as any;
9
14
 
10
15
  describe("Stripe Providers", () => {
11
- let mockPaymentLinksCreate: jest.Mock<any>;
12
- let mockPaymentLinksUpdate: jest.Mock<any>;
13
- let mockProductsCreate: jest.Mock<any>;
14
- let mockProductsUpdate: jest.Mock<any>;
15
- let mockPricesCreate: jest.Mock<any>;
16
- let mockPricesUpdate: jest.Mock<any>;
17
-
18
- beforeEach(() => {
19
- // Reset mocks before each test
20
- jest.clearAllMocks();
21
-
22
- mockPaymentLinksCreate = jest.fn<any>().mockResolvedValue({ id: "plink_123", url: "https://stripe.com/plink_123" } as any);
23
- mockPaymentLinksUpdate = jest.fn<any>().mockResolvedValue({ id: "plink_123" } as any);
24
-
25
- mockProductsCreate = jest.fn<any>().mockResolvedValue({ id: "prod_123" } as any);
26
- mockProductsUpdate = jest.fn<any>().mockResolvedValue({ id: "prod_123" } as any);
27
-
28
- mockPricesCreate = jest.fn<any>().mockResolvedValue({ id: "price_123" } as any);
29
- mockPricesUpdate = jest.fn<any>().mockResolvedValue({ id: "price_123" } as any);
30
-
31
- // Setup the mocked Stripe instance's internal resource objects
32
- MockedStripe.mockImplementation(() => {
33
- return {
34
- paymentLinks: {
35
- create: mockPaymentLinksCreate,
36
- update: mockPaymentLinksUpdate,
37
- },
38
- products: {
39
- create: mockProductsCreate,
40
- update: mockProductsUpdate,
41
- },
42
- prices: {
43
- create: mockPricesCreate,
44
- update: mockPricesUpdate,
45
- },
46
- } as any;
47
- });
48
- });
49
-
50
- describe("StripePaymentLinkProvider", () => {
51
- const provider = new StripePaymentLinkProvider();
52
-
53
- describe("create", () => {
54
- const createCases = [
55
- {
56
- name: "all properties provided",
57
- inputs: {
58
- apiKey: "sk_test_123",
59
- priceId: "price_abc",
60
- sparksAmount: "100",
61
- redirectUrl: "https://example.com/success",
62
- allowPromotionCodes: true,
63
- managedPayments: true,
64
- },
65
- expectedPayload: {
66
- line_items: [{
67
- price: "price_abc",
68
- quantity: 1,
69
- adjustable_quantity: { enabled: true, minimum: 1, maximum: 100 },
70
- }],
71
- after_completion: { type: 'redirect', redirect: { url: "https://example.com/success" } },
72
- allow_promotion_codes: true,
73
- managed_payments: { enabled: true },
74
- metadata: { sparks: "100" }
75
- }
76
- },
77
- {
78
- name: "only required properties provided",
79
- inputs: {
80
- apiKey: "sk_test_123",
81
- priceId: "price_abc",
82
- sparksAmount: "50",
83
- redirectUrl: "https://example.com/success",
84
- },
85
- expectedPayload: {
86
- line_items: [{
87
- price: "price_abc",
88
- quantity: 1,
89
- adjustable_quantity: { enabled: true, minimum: 1, maximum: 100 },
90
- }],
91
- after_completion: { type: 'redirect', redirect: { url: "https://example.com/success" } },
92
- allow_promotion_codes: undefined,
93
- metadata: { sparks: "50" }
94
- }
95
- }
96
- ];
97
-
98
- it.each(createCases)("should correctly map Pulumi inputs to Stripe API payload when $name", async ({ inputs, expectedPayload }) => {
99
- const result = await provider.create(inputs as any);
100
-
101
- expect(result.id).toBe("plink_123");
102
- expect(result.outs?.paymentLinkId).toBe("plink_123");
103
- expect(result.outs?.url).toBe("https://stripe.com/plink_123");
104
-
105
- expect(MockedStripe).toHaveBeenCalledWith(inputs.apiKey, { apiVersion: '2022-11-15' as any });
106
- expect(mockPaymentLinksCreate).toHaveBeenCalledWith(expectedPayload);
107
- });
108
- });
109
-
110
- describe("diff", () => {
111
- const diffCases = [
112
- {
113
- name: "all properties change",
114
- olds: { priceId: "old", sparksAmount: "10", redirectUrl: "url1", allowPromotionCodes: false, managedPayments: false, apiKey: "old_key" },
115
- news: { priceId: "new", sparksAmount: "20", redirectUrl: "url2", allowPromotionCodes: true, managedPayments: true, apiKey: "new_key" },
116
- expectedChanges: true,
117
- expectedReplaces: ["priceId", "sparksAmount", "redirectUrl", "allowPromotionCodes", "managedPayments", "apiKey"]
118
- },
119
- {
120
- name: "some properties change",
121
- olds: { priceId: "old", sparksAmount: "10", redirectUrl: "url1" },
122
- news: { priceId: "old", sparksAmount: "20", redirectUrl: "url1" },
123
- expectedChanges: true,
124
- expectedReplaces: ["sparksAmount"]
125
- },
126
- {
127
- name: "no properties change",
128
- olds: { priceId: "same", sparksAmount: "10" },
129
- news: { priceId: "same", sparksAmount: "10" },
130
- expectedChanges: false,
131
- expectedReplaces: []
132
- }
133
- ];
134
-
135
- it.each(diffCases)("should correctly detect diff when $name", async ({ olds, news, expectedChanges, expectedReplaces }) => {
136
- const diffResult = await provider.diff("plink_123", olds as any, news as any);
137
- expect(diffResult.changes).toBe(expectedChanges);
138
- expect(diffResult.replaces).toEqual(expect.arrayContaining(expectedReplaces));
139
- expect(diffResult.replaces?.length).toBe(expectedReplaces.length);
140
- });
141
- });
142
-
143
- describe("delete", () => {
144
- it("should deactivate the payment link", async () => {
145
- await provider.delete("plink_123", { apiKey: "sk_test_123" } as any);
146
- expect(mockPaymentLinksUpdate).toHaveBeenCalledWith("plink_123", { active: false });
147
- });
148
- });
149
- });
150
-
151
- describe("StripeProductProvider", () => {
152
- const provider = new StripeProductProvider();
153
-
154
- describe("create", () => {
155
- const createCases = [
156
- {
157
- name: "all properties provided",
158
- inputs: {
159
- apiKey: "sk_test_123",
160
- name: "Full Product",
161
- description: "A complete product",
162
- taxCode: "txcd_123",
163
- },
164
- expectedPayload: {
165
- name: "Full Product",
166
- description: "A complete product",
167
- tax_code: "txcd_123",
168
- }
169
- },
170
- {
171
- name: "only required properties provided",
172
- inputs: {
173
- apiKey: "sk_test_123",
174
- name: "Minimal Product",
175
- },
176
- expectedPayload: {
177
- name: "Minimal Product",
178
- description: undefined,
179
- tax_code: undefined,
180
- }
181
- }
182
- ];
183
-
184
- it.each(createCases)("should correctly create a product when $name", async ({ inputs, expectedPayload }) => {
185
- const result = await provider.create(inputs as any);
186
-
187
- expect(result.id).toBe("prod_123");
188
- expect(result.outs?.productId).toBe("prod_123");
189
-
190
- expect(MockedStripe).toHaveBeenCalledWith(inputs.apiKey, { apiVersion: '2022-11-15' as any });
191
- expect(mockProductsCreate).toHaveBeenCalledWith(expectedPayload);
192
- });
193
- });
194
-
195
- describe("diff", () => {
196
- const diffCases = [
197
- {
198
- name: "all properties change",
199
- olds: { name: "old name", description: "old desc", taxCode: "tx_1", apiKey: "old_key" },
200
- news: { name: "new name", description: "new desc", taxCode: "tx_2", apiKey: "new_key" },
201
- expectedChanges: true,
202
- },
203
- {
204
- name: "some properties change",
205
- olds: { name: "old name", description: "old desc" },
206
- news: { name: "old name", description: "new desc" },
207
- expectedChanges: true,
208
- },
209
- {
210
- name: "no properties change",
211
- olds: { name: "same name", description: "same desc" },
212
- news: { name: "same name", description: "same desc" },
213
- expectedChanges: false,
214
- }
215
- ];
216
-
217
- it.each(diffCases)("should correctly detect diff when $name", async ({ olds, news, expectedChanges }) => {
218
- const diffResult = await provider.diff("prod_123", olds as any, news as any);
219
- expect(diffResult.changes).toBe(expectedChanges);
220
- expect(diffResult.replaces).toHaveLength(0);
221
- });
222
- });
223
-
224
- describe("update", () => {
225
- it("should correctly update a product", async () => {
226
- const result = await provider.update("prod_123",
227
- { apiKey: "sk_test_123", name: "Old Name" } as any,
228
- { apiKey: "sk_test_123", name: "New Name", description: "New Desc", taxCode: "txcd_999" } as any
229
- );
230
-
231
- expect(result.outs?.productId).toBe("prod_123");
232
- expect(result.outs?.name).toBe("New Name");
233
-
234
- expect(mockProductsUpdate).toHaveBeenCalledWith("prod_123", {
235
- name: "New Name",
236
- description: "New Desc",
237
- tax_code: "txcd_999",
238
- });
239
- });
240
- });
241
-
242
- describe("delete", () => {
243
- it("should deactivate the product on delete", async () => {
244
- await provider.delete("prod_123", { apiKey: "sk_test_123" } as any);
245
- expect(mockProductsUpdate).toHaveBeenCalledWith("prod_123", { active: false });
246
- });
247
- });
248
- });
249
-
250
- describe("StripePriceProvider", () => {
251
- const provider = new StripePriceProvider();
252
-
253
- describe("create", () => {
254
- const createCases = [
255
- {
256
- name: "required properties provided",
257
- inputs: {
258
- apiKey: "sk_test_123",
259
- productId: "prod_123",
260
- unitAmount: 1000,
261
- currency: "usd",
262
- },
263
- expectedPayload: {
264
- product: "prod_123",
265
- unit_amount: 1000,
266
- currency: "usd",
267
- }
268
- }
269
- ];
270
-
271
- it.each(createCases)("should correctly create a price when $name", async ({ inputs, expectedPayload }) => {
272
- const result = await provider.create(inputs as any);
273
-
274
- expect(result.id).toBe("price_123");
275
- expect(result.outs?.priceId).toBe("price_123");
276
-
277
- expect(MockedStripe).toHaveBeenCalledWith(inputs.apiKey, { apiVersion: '2022-11-15' as any });
278
- expect(mockPricesCreate).toHaveBeenCalledWith(expectedPayload);
279
- });
280
- });
281
-
282
- describe("diff", () => {
283
- const diffCases = [
284
- {
285
- name: "all properties change",
286
- olds: { productId: "prod_1", unitAmount: 100, currency: "usd", apiKey: "old_key" },
287
- news: { productId: "prod_2", unitAmount: 200, currency: "eur", apiKey: "new_key" },
288
- expectedChanges: true,
289
- expectedReplaces: ["productId", "unitAmount", "currency", "apiKey"]
290
- },
291
- {
292
- name: "some properties change",
293
- olds: { productId: "prod_1", unitAmount: 100, currency: "usd" },
294
- news: { productId: "prod_1", unitAmount: 200, currency: "usd" },
295
- expectedChanges: true,
296
- expectedReplaces: ["unitAmount"]
297
- },
298
- {
299
- name: "no properties change",
300
- olds: { productId: "prod_1", unitAmount: 100, currency: "usd" },
301
- news: { productId: "prod_1", unitAmount: 100, currency: "usd" },
302
- expectedChanges: false,
303
- expectedReplaces: []
304
- }
305
- ];
306
-
307
- it.each(diffCases)("should correctly detect diff when $name", async ({ olds, news, expectedChanges, expectedReplaces }) => {
308
- const diffResult = await provider.diff("price_123", olds as any, news as any);
309
- expect(diffResult.changes).toBe(expectedChanges);
310
- expect(diffResult.replaces).toEqual(expect.arrayContaining(expectedReplaces));
311
- expect(diffResult.replaces?.length).toBe(expectedReplaces.length);
312
- });
313
- });
314
-
315
- describe("delete", () => {
316
- it("should deactivate the price on delete", async () => {
317
- await provider.delete("price_123", { apiKey: "sk_test_123" } as any);
318
- expect(mockPricesUpdate).toHaveBeenCalledWith("price_123", { active: false });
319
- });
320
- });
321
- });
16
+ let mockPaymentLinksCreate: Mock<any>;
17
+ let mockPaymentLinksUpdate: Mock<any>;
18
+ let mockProductsCreate: Mock<any>;
19
+ let mockProductsUpdate: Mock<any>;
20
+ let mockPricesCreate: Mock<any>;
21
+ let mockPricesUpdate: Mock<any>;
22
+
23
+ beforeEach(() => {
24
+ // Reset mocks before each test
25
+ vi.clearAllMocks();
26
+
27
+ mockPaymentLinksCreate = vi.fn<any>().mockResolvedValue({
28
+ id: "plink_123",
29
+ url: "https://stripe.com/plink_123",
30
+ } as any);
31
+ mockPaymentLinksUpdate = vi
32
+ .fn<any>()
33
+ .mockResolvedValue({ id: "plink_123" } as any);
34
+
35
+ mockProductsCreate = vi
36
+ .fn<any>()
37
+ .mockResolvedValue({ id: "prod_123" } as any);
38
+ mockProductsUpdate = vi
39
+ .fn<any>()
40
+ .mockResolvedValue({ id: "prod_123" } as any);
41
+
42
+ mockPricesCreate = vi
43
+ .fn<any>()
44
+ .mockResolvedValue({ id: "price_123" } as any);
45
+ mockPricesUpdate = vi
46
+ .fn<any>()
47
+ .mockResolvedValue({ id: "price_123" } as any);
48
+
49
+ // Setup the mocked Stripe instance's internal resource objects
50
+ MockedStripe.mockImplementation(() => {
51
+ return {
52
+ paymentLinks: {
53
+ create: mockPaymentLinksCreate,
54
+ update: mockPaymentLinksUpdate,
55
+ },
56
+ products: {
57
+ create: mockProductsCreate,
58
+ update: mockProductsUpdate,
59
+ },
60
+ prices: {
61
+ create: mockPricesCreate,
62
+ update: mockPricesUpdate,
63
+ },
64
+ } as any;
65
+ });
66
+ });
67
+
68
+ describe("StripePaymentLinkProvider", () => {
69
+ const provider = new StripePaymentLinkProvider();
70
+
71
+ describe("create", () => {
72
+ const createCases = [
73
+ {
74
+ name: "all properties provided",
75
+ inputs: {
76
+ apiKey: "sk_test_123",
77
+ priceId: "price_abc",
78
+ sparksAmount: "100",
79
+ redirectUrl: "https://example.com/success",
80
+ allowPromotionCodes: true,
81
+ managedPayments: true,
82
+ automaticTax: true,
83
+ },
84
+ expectedPayload: {
85
+ line_items: [
86
+ {
87
+ price: "price_abc",
88
+ quantity: 1,
89
+ adjustable_quantity: {
90
+ enabled: true,
91
+ minimum: 1,
92
+ maximum: 100,
93
+ },
94
+ },
95
+ ],
96
+ after_completion: {
97
+ type: "redirect",
98
+ redirect: { url: "https://example.com/success" },
99
+ },
100
+ allow_promotion_codes: true,
101
+ managed_payments: { enabled: true },
102
+ automatic_tax: { enabled: true },
103
+ metadata: { sparks: "100" },
104
+ },
105
+ },
106
+ {
107
+ name: "only required properties provided",
108
+ inputs: {
109
+ apiKey: "sk_test_123",
110
+ priceId: "price_abc",
111
+ sparksAmount: "50",
112
+ redirectUrl: "https://example.com/success",
113
+ },
114
+ expectedPayload: {
115
+ line_items: [
116
+ {
117
+ price: "price_abc",
118
+ quantity: 1,
119
+ adjustable_quantity: {
120
+ enabled: true,
121
+ minimum: 1,
122
+ maximum: 100,
123
+ },
124
+ },
125
+ ],
126
+ after_completion: {
127
+ type: "redirect",
128
+ redirect: { url: "https://example.com/success" },
129
+ },
130
+ allow_promotion_codes: undefined,
131
+ metadata: { sparks: "50" },
132
+ },
133
+ },
134
+ {
135
+ name: "custom quantity and adjustableQuantity provided",
136
+ inputs: {
137
+ apiKey: "sk_test_123",
138
+ priceId: "price_abc",
139
+ sparksAmount: "100",
140
+ redirectUrl: "https://example.com/success",
141
+ quantity: 5,
142
+ adjustableQuantity: { enabled: true, maximum: 5 },
143
+ },
144
+ expectedPayload: {
145
+ line_items: [
146
+ {
147
+ price: "price_abc",
148
+ quantity: 5,
149
+ adjustable_quantity: { enabled: true, maximum: 5 },
150
+ },
151
+ ],
152
+ after_completion: {
153
+ type: "redirect",
154
+ redirect: { url: "https://example.com/success" },
155
+ },
156
+ allow_promotion_codes: undefined,
157
+ metadata: { sparks: "100" },
158
+ },
159
+ },
160
+ ];
161
+
162
+ it.each(createCases)(
163
+ "should correctly map Pulumi inputs to Stripe API payload when $name",
164
+ async ({ inputs, expectedPayload }) => {
165
+ const result = await provider.create(inputs as any);
166
+
167
+ expect(result.id).toBe("plink_123");
168
+ expect(result.outs?.paymentLinkId).toBe("plink_123");
169
+ expect(result.outs?.url).toBe("https://stripe.com/plink_123");
170
+
171
+ expect(MockedStripe).toHaveBeenCalledWith(inputs.apiKey);
172
+ expect(mockPaymentLinksCreate).toHaveBeenCalledWith(expectedPayload);
173
+ },
174
+ );
175
+ });
176
+
177
+ describe("diff", () => {
178
+ const diffCases = [
179
+ {
180
+ name: "all properties change",
181
+ olds: {
182
+ priceId: "old",
183
+ sparksAmount: "10",
184
+ redirectUrl: "url1",
185
+ allowPromotionCodes: false,
186
+ managedPayments: false,
187
+ automaticTax: false,
188
+ quantity: 1,
189
+ adjustableQuantity: { enabled: true },
190
+ apiKey: "old_key",
191
+ },
192
+ news: {
193
+ priceId: "new",
194
+ sparksAmount: "20",
195
+ redirectUrl: "url2",
196
+ allowPromotionCodes: true,
197
+ managedPayments: true,
198
+ automaticTax: true,
199
+ quantity: 2,
200
+ adjustableQuantity: { enabled: false },
201
+ apiKey: "new_key",
202
+ },
203
+ expectedChanges: true,
204
+ expectedReplaces: [
205
+ "priceId",
206
+ "sparksAmount",
207
+ "redirectUrl",
208
+ "allowPromotionCodes",
209
+ "managedPayments",
210
+ "automaticTax",
211
+ "apiKey",
212
+ "quantity",
213
+ "adjustableQuantity",
214
+ ],
215
+ },
216
+ {
217
+ name: "some properties change",
218
+ olds: { priceId: "old", sparksAmount: "10", redirectUrl: "url1" },
219
+ news: { priceId: "old", sparksAmount: "20", redirectUrl: "url1" },
220
+ expectedChanges: true,
221
+ expectedReplaces: ["sparksAmount"],
222
+ },
223
+ {
224
+ name: "quantity changes",
225
+ olds: { priceId: "same", sparksAmount: "10", quantity: 1 },
226
+ news: { priceId: "same", sparksAmount: "10", quantity: 2 },
227
+ expectedChanges: true,
228
+ expectedReplaces: ["quantity"],
229
+ },
230
+ {
231
+ name: "adjustableQuantity changes",
232
+ olds: {
233
+ priceId: "same",
234
+ sparksAmount: "10",
235
+ adjustableQuantity: { enabled: true, minimum: 1, maximum: 10 },
236
+ },
237
+ news: {
238
+ priceId: "same",
239
+ sparksAmount: "10",
240
+ adjustableQuantity: { enabled: true, minimum: 2, maximum: 10 },
241
+ },
242
+ expectedChanges: true,
243
+ expectedReplaces: ["adjustableQuantity"],
244
+ },
245
+ {
246
+ name: "no properties change",
247
+ olds: { priceId: "same", sparksAmount: "10" },
248
+ news: { priceId: "same", sparksAmount: "10" },
249
+ expectedChanges: false,
250
+ expectedReplaces: [],
251
+ },
252
+ ];
253
+
254
+ it.each(diffCases)(
255
+ "should correctly detect diff when $name",
256
+ async ({ olds, news, expectedChanges, expectedReplaces }) => {
257
+ const diffResult = await provider.diff(
258
+ "plink_123",
259
+ olds as any,
260
+ news as any,
261
+ );
262
+ expect(diffResult.changes).toBe(expectedChanges);
263
+ expect(diffResult.replaces).toEqual(
264
+ expect.arrayContaining(expectedReplaces),
265
+ );
266
+ expect(diffResult.replaces?.length).toBe(expectedReplaces.length);
267
+ },
268
+ );
269
+ });
270
+
271
+ describe("delete", () => {
272
+ const deleteCases = [
273
+ {
274
+ name: "apiKey is in state",
275
+ state: { apiKey: "sk_test_state" },
276
+ envSecret: undefined,
277
+ envApi: undefined,
278
+ mockError: false,
279
+ expectStripeInit: "sk_test_state",
280
+ expectUpdate: true,
281
+ },
282
+ {
283
+ name: "apiKey is missing, falls back to STRIPE_SECRET_KEY",
284
+ state: {},
285
+ envSecret: "sk_test_secret",
286
+ envApi: undefined,
287
+ mockError: false,
288
+ expectStripeInit: "sk_test_secret",
289
+ expectUpdate: true,
290
+ },
291
+ {
292
+ name: "apiKey is missing, falls back to STRIPE_API_KEY",
293
+ state: {},
294
+ envSecret: undefined,
295
+ envApi: "sk_test_api",
296
+ mockError: false,
297
+ expectStripeInit: "sk_test_api",
298
+ expectUpdate: true,
299
+ },
300
+ {
301
+ name: "apiKey is missing everywhere (NoOp)",
302
+ state: {},
303
+ envSecret: undefined,
304
+ envApi: undefined,
305
+ mockError: false,
306
+ expectStripeInit: null,
307
+ expectUpdate: false,
308
+ },
309
+ {
310
+ name: "Stripe SDK throws an error (caught safely)",
311
+ state: { apiKey: "sk_test_error" },
312
+ envSecret: undefined,
313
+ envApi: undefined,
314
+ mockError: true,
315
+ expectStripeInit: "sk_test_error",
316
+ expectUpdate: true,
317
+ },
318
+ ];
319
+
320
+ it.each(deleteCases)(
321
+ "should handle deletion when $name",
322
+ async ({
323
+ state,
324
+ envSecret,
325
+ envApi,
326
+ mockError,
327
+ expectStripeInit,
328
+ expectUpdate,
329
+ }) => {
330
+ const originalSecret = process.env.STRIPE_SECRET_KEY;
331
+ const originalApi = process.env.STRIPE_API_KEY;
332
+
333
+ if (envSecret !== undefined)
334
+ process.env.STRIPE_SECRET_KEY = envSecret;
335
+ else delete process.env.STRIPE_SECRET_KEY;
336
+
337
+ if (envApi !== undefined) process.env.STRIPE_API_KEY = envApi;
338
+ else delete process.env.STRIPE_API_KEY;
339
+
340
+ if (mockError) {
341
+ mockPaymentLinksUpdate.mockRejectedValueOnce(
342
+ new Error("Stripe error"),
343
+ );
344
+ }
345
+
346
+ await provider.delete("plink_123", state as any);
347
+
348
+ if (expectStripeInit) {
349
+ expect(MockedStripe).toHaveBeenCalledWith(expectStripeInit);
350
+ }
351
+
352
+ if (expectUpdate) {
353
+ expect(mockPaymentLinksUpdate).toHaveBeenCalledWith("plink_123", {
354
+ active: false,
355
+ });
356
+ } else {
357
+ expect(mockPaymentLinksUpdate).not.toHaveBeenCalled();
358
+ }
359
+
360
+ if (originalSecret !== undefined)
361
+ process.env.STRIPE_SECRET_KEY = originalSecret;
362
+ else delete process.env.STRIPE_SECRET_KEY;
363
+
364
+ if (originalApi !== undefined)
365
+ process.env.STRIPE_API_KEY = originalApi;
366
+ else delete process.env.STRIPE_API_KEY;
367
+ },
368
+ );
369
+ });
370
+ });
371
+
372
+ describe("StripeProductProvider", () => {
373
+ const provider = new StripeProductProvider();
374
+
375
+ describe("create", () => {
376
+ const createCases = [
377
+ {
378
+ name: "all properties provided",
379
+ inputs: {
380
+ apiKey: "sk_test_123",
381
+ name: "Full Product",
382
+ description: "A complete product",
383
+ taxCode: "txcd_123",
384
+ },
385
+ expectedPayload: {
386
+ name: "Full Product",
387
+ description: "A complete product",
388
+ tax_code: "txcd_123",
389
+ },
390
+ },
391
+ {
392
+ name: "only required properties provided",
393
+ inputs: {
394
+ apiKey: "sk_test_123",
395
+ name: "Minimal Product",
396
+ },
397
+ expectedPayload: {
398
+ name: "Minimal Product",
399
+ description: undefined,
400
+ tax_code: undefined,
401
+ },
402
+ },
403
+ ];
404
+
405
+ it.each(createCases)(
406
+ "should correctly create a product when $name",
407
+ async ({ inputs, expectedPayload }) => {
408
+ const result = await provider.create(inputs as any);
409
+
410
+ expect(result.id).toBe("prod_123");
411
+ expect(result.outs?.productId).toBe("prod_123");
412
+
413
+ expect(MockedStripe).toHaveBeenCalledWith(inputs.apiKey);
414
+ expect(mockProductsCreate).toHaveBeenCalledWith(expectedPayload);
415
+ },
416
+ );
417
+ });
418
+
419
+ describe("diff", () => {
420
+ const diffCases = [
421
+ {
422
+ name: "all properties change",
423
+ olds: {
424
+ name: "old name",
425
+ description: "old desc",
426
+ taxCode: "tx_1",
427
+ apiKey: "old_key",
428
+ },
429
+ news: {
430
+ name: "new name",
431
+ description: "new desc",
432
+ taxCode: "tx_2",
433
+ apiKey: "new_key",
434
+ },
435
+ expectedChanges: true,
436
+ },
437
+ {
438
+ name: "some properties change",
439
+ olds: { name: "old name", description: "old desc" },
440
+ news: { name: "old name", description: "new desc" },
441
+ expectedChanges: true,
442
+ },
443
+ {
444
+ name: "no properties change",
445
+ olds: { name: "same name", description: "same desc" },
446
+ news: { name: "same name", description: "same desc" },
447
+ expectedChanges: false,
448
+ },
449
+ ];
450
+
451
+ it.each(diffCases)(
452
+ "should correctly detect diff when $name",
453
+ async ({ olds, news, expectedChanges }) => {
454
+ const diffResult = await provider.diff(
455
+ "prod_123",
456
+ olds as any,
457
+ news as any,
458
+ );
459
+ expect(diffResult.changes).toBe(expectedChanges);
460
+ expect(diffResult.replaces).toHaveLength(0);
461
+ },
462
+ );
463
+ });
464
+
465
+ describe("update", () => {
466
+ it("should correctly update a product", async () => {
467
+ const result = await provider.update(
468
+ "prod_123",
469
+ { apiKey: "sk_test_123", name: "Old Name" } as any,
470
+ {
471
+ apiKey: "sk_test_123",
472
+ name: "New Name",
473
+ description: "New Desc",
474
+ taxCode: "txcd_999",
475
+ } as any,
476
+ );
477
+
478
+ expect(result.outs?.productId).toBe("prod_123");
479
+ expect(result.outs?.name).toBe("New Name");
480
+
481
+ expect(mockProductsUpdate).toHaveBeenCalledWith("prod_123", {
482
+ name: "New Name",
483
+ description: "New Desc",
484
+ tax_code: "txcd_999",
485
+ });
486
+ });
487
+ });
488
+
489
+ describe("delete", () => {
490
+ const deleteCases = [
491
+ {
492
+ name: "apiKey is in state",
493
+ state: { apiKey: "sk_test_state" },
494
+ envSecret: undefined,
495
+ envApi: undefined,
496
+ mockError: false,
497
+ expectStripeInit: "sk_test_state",
498
+ expectUpdate: true,
499
+ },
500
+ {
501
+ name: "apiKey is missing, falls back to STRIPE_SECRET_KEY",
502
+ state: {},
503
+ envSecret: "sk_test_secret",
504
+ envApi: undefined,
505
+ mockError: false,
506
+ expectStripeInit: "sk_test_secret",
507
+ expectUpdate: true,
508
+ },
509
+ {
510
+ name: "apiKey is missing, falls back to STRIPE_API_KEY",
511
+ state: {},
512
+ envSecret: undefined,
513
+ envApi: "sk_test_api",
514
+ mockError: false,
515
+ expectStripeInit: "sk_test_api",
516
+ expectUpdate: true,
517
+ },
518
+ {
519
+ name: "apiKey is missing everywhere (NoOp)",
520
+ state: {},
521
+ envSecret: undefined,
522
+ envApi: undefined,
523
+ mockError: false,
524
+ expectStripeInit: null,
525
+ expectUpdate: false,
526
+ },
527
+ {
528
+ name: "Stripe SDK throws an error (caught safely)",
529
+ state: { apiKey: "sk_test_error" },
530
+ envSecret: undefined,
531
+ envApi: undefined,
532
+ mockError: true,
533
+ expectStripeInit: "sk_test_error",
534
+ expectUpdate: true,
535
+ },
536
+ ];
537
+
538
+ it.each(deleteCases)(
539
+ "should handle deletion when $name",
540
+ async ({
541
+ state,
542
+ envSecret,
543
+ envApi,
544
+ mockError,
545
+ expectStripeInit,
546
+ expectUpdate,
547
+ }) => {
548
+ const originalSecret = process.env.STRIPE_SECRET_KEY;
549
+ const originalApi = process.env.STRIPE_API_KEY;
550
+
551
+ if (envSecret !== undefined)
552
+ process.env.STRIPE_SECRET_KEY = envSecret;
553
+ else delete process.env.STRIPE_SECRET_KEY;
554
+
555
+ if (envApi !== undefined) process.env.STRIPE_API_KEY = envApi;
556
+ else delete process.env.STRIPE_API_KEY;
557
+
558
+ if (mockError) {
559
+ mockProductsUpdate.mockRejectedValueOnce(new Error("Stripe error"));
560
+ }
561
+
562
+ await provider.delete("prod_123", state as any);
563
+
564
+ if (expectStripeInit) {
565
+ expect(MockedStripe).toHaveBeenCalledWith(expectStripeInit);
566
+ }
567
+
568
+ if (expectUpdate) {
569
+ expect(mockProductsUpdate).toHaveBeenCalledWith("prod_123", {
570
+ active: false,
571
+ });
572
+ } else {
573
+ expect(mockProductsUpdate).not.toHaveBeenCalled();
574
+ }
575
+
576
+ if (originalSecret !== undefined)
577
+ process.env.STRIPE_SECRET_KEY = originalSecret;
578
+ else delete process.env.STRIPE_SECRET_KEY;
579
+
580
+ if (originalApi !== undefined)
581
+ process.env.STRIPE_API_KEY = originalApi;
582
+ else delete process.env.STRIPE_API_KEY;
583
+ },
584
+ );
585
+ });
586
+ });
587
+
588
+ describe("StripePriceProvider", () => {
589
+ const provider = new StripePriceProvider();
590
+
591
+ describe("create", () => {
592
+ const createCases = [
593
+ {
594
+ name: "all properties provided",
595
+ inputs: {
596
+ apiKey: "sk_test_123",
597
+ productId: "prod_123",
598
+ unitAmount: 1000,
599
+ currency: "usd",
600
+ taxBehavior: "inclusive",
601
+ },
602
+ expectedPayload: {
603
+ product: "prod_123",
604
+ unit_amount: 1000,
605
+ currency: "usd",
606
+ tax_behavior: "inclusive",
607
+ },
608
+ },
609
+ {
610
+ name: "required properties provided",
611
+ inputs: {
612
+ apiKey: "sk_test_123",
613
+ productId: "prod_123",
614
+ unitAmount: 1000,
615
+ currency: "usd",
616
+ },
617
+ expectedPayload: {
618
+ product: "prod_123",
619
+ unit_amount: 1000,
620
+ currency: "usd",
621
+ tax_behavior: undefined,
622
+ },
623
+ },
624
+ {
625
+ name: "recurring pricing provided",
626
+ inputs: {
627
+ apiKey: "sk_test_123",
628
+ productId: "prod_123",
629
+ unitAmount: 1000,
630
+ currency: "usd",
631
+ recurring: {
632
+ interval: "month",
633
+ intervalCount: 1,
634
+ usageType: "licensed",
635
+ },
636
+ },
637
+ expectedPayload: {
638
+ product: "prod_123",
639
+ unit_amount: 1000,
640
+ currency: "usd",
641
+ recurring: {
642
+ interval: "month",
643
+ interval_count: 1,
644
+ usage_type: "licensed",
645
+ },
646
+ },
647
+ },
648
+ {
649
+ name: "tiered pricing provided",
650
+ inputs: {
651
+ apiKey: "sk_test_123",
652
+ productId: "prod_123",
653
+ currency: "usd",
654
+ billingScheme: "tiered",
655
+ tiersMode: "volume",
656
+ tiers: [
657
+ { upTo: 10, unitAmount: 1000 },
658
+ { upTo: "inf", unitAmount: 800 },
659
+ ],
660
+ },
661
+ expectedPayload: {
662
+ product: "prod_123",
663
+ currency: "usd",
664
+ billing_scheme: "tiered",
665
+ tiers_mode: "volume",
666
+ tiers: [
667
+ { up_to: 10, unit_amount: 1000, flat_amount: undefined },
668
+ { up_to: "inf", unit_amount: 800, flat_amount: undefined },
669
+ ],
670
+ },
671
+ },
672
+ {
673
+ name: "package pricing provided",
674
+ inputs: {
675
+ apiKey: "sk_test_123",
676
+ productId: "prod_123",
677
+ unitAmount: 5000,
678
+ currency: "usd",
679
+ transformQuantity: { divideBy: 5, round: "up" },
680
+ },
681
+ expectedPayload: {
682
+ product: "prod_123",
683
+ unit_amount: 5000,
684
+ currency: "usd",
685
+ transform_quantity: { divide_by: 5, round: "up" },
686
+ },
687
+ },
688
+ ];
689
+
690
+ it.each(createCases)(
691
+ "should correctly create a price when $name",
692
+ async ({ inputs, expectedPayload }) => {
693
+ const result = await provider.create(inputs as any);
694
+
695
+ expect(result.id).toBe("price_123");
696
+ expect(result.outs?.priceId).toBe("price_123");
697
+
698
+ expect(MockedStripe).toHaveBeenCalledWith(inputs.apiKey);
699
+ expect(mockPricesCreate).toHaveBeenCalledWith(expectedPayload);
700
+ },
701
+ );
702
+ });
703
+
704
+ describe("diff", () => {
705
+ const diffCases = [
706
+ {
707
+ name: "all properties change",
708
+ olds: {
709
+ productId: "prod_1",
710
+ unitAmount: 100,
711
+ currency: "usd",
712
+ taxBehavior: "inclusive",
713
+ apiKey: "old_key",
714
+ },
715
+ news: {
716
+ productId: "prod_2",
717
+ unitAmount: 200,
718
+ currency: "eur",
719
+ taxBehavior: "exclusive",
720
+ apiKey: "new_key",
721
+ },
722
+ expectedChanges: true,
723
+ expectedReplaces: [
724
+ "productId",
725
+ "unitAmount",
726
+ "currency",
727
+ "taxBehavior",
728
+ "apiKey",
729
+ ],
730
+ },
731
+ {
732
+ name: "pricing models properties change",
733
+ olds: {
734
+ productId: "prod_1",
735
+ currency: "usd",
736
+ recurring: { interval: "month" },
737
+ billingScheme: "per_unit",
738
+ tiersMode: "volume",
739
+ tiers: [{ upTo: 10 }],
740
+ transformQuantity: { divideBy: 5 },
741
+ },
742
+ news: {
743
+ productId: "prod_1",
744
+ currency: "usd",
745
+ recurring: { interval: "year" },
746
+ billingScheme: "tiered",
747
+ tiersMode: "graduated",
748
+ tiers: [{ upTo: 20 }],
749
+ transformQuantity: { divideBy: 10 },
750
+ },
751
+ expectedChanges: true,
752
+ expectedReplaces: [
753
+ "recurring",
754
+ "billingScheme",
755
+ "tiersMode",
756
+ "tiers",
757
+ "transformQuantity",
758
+ ],
759
+ },
760
+ {
761
+ name: "some properties change",
762
+ olds: { productId: "prod_1", unitAmount: 100, currency: "usd" },
763
+ news: { productId: "prod_1", unitAmount: 200, currency: "usd" },
764
+ expectedChanges: true,
765
+ expectedReplaces: ["unitAmount"],
766
+ },
767
+ {
768
+ name: "no properties change",
769
+ olds: { productId: "prod_1", unitAmount: 100, currency: "usd" },
770
+ news: { productId: "prod_1", unitAmount: 100, currency: "usd" },
771
+ expectedChanges: false,
772
+ expectedReplaces: [],
773
+ },
774
+ ];
775
+
776
+ it.each(diffCases)(
777
+ "should correctly detect diff when $name",
778
+ async ({ olds, news, expectedChanges, expectedReplaces }) => {
779
+ const diffResult = await provider.diff(
780
+ "price_123",
781
+ olds as any,
782
+ news as any,
783
+ );
784
+ expect(diffResult.changes).toBe(expectedChanges);
785
+ expect(diffResult.replaces).toEqual(
786
+ expect.arrayContaining(expectedReplaces),
787
+ );
788
+ expect(diffResult.replaces?.length).toBe(expectedReplaces.length);
789
+ },
790
+ );
791
+ });
792
+
793
+ describe("delete", () => {
794
+ const deleteCases = [
795
+ {
796
+ name: "apiKey is in state",
797
+ state: { apiKey: "sk_test_state" },
798
+ envSecret: undefined,
799
+ envApi: undefined,
800
+ mockError: false,
801
+ expectStripeInit: "sk_test_state",
802
+ expectUpdate: true,
803
+ },
804
+ {
805
+ name: "apiKey is missing, falls back to STRIPE_SECRET_KEY",
806
+ state: {},
807
+ envSecret: "sk_test_secret",
808
+ envApi: undefined,
809
+ mockError: false,
810
+ expectStripeInit: "sk_test_secret",
811
+ expectUpdate: true,
812
+ },
813
+ {
814
+ name: "apiKey is missing, falls back to STRIPE_API_KEY",
815
+ state: {},
816
+ envSecret: undefined,
817
+ envApi: "sk_test_api",
818
+ mockError: false,
819
+ expectStripeInit: "sk_test_api",
820
+ expectUpdate: true,
821
+ },
822
+ {
823
+ name: "apiKey is missing everywhere (NoOp)",
824
+ state: {},
825
+ envSecret: undefined,
826
+ envApi: undefined,
827
+ mockError: false,
828
+ expectStripeInit: null,
829
+ expectUpdate: false,
830
+ },
831
+ {
832
+ name: "Stripe SDK throws an error (caught safely)",
833
+ state: { apiKey: "sk_test_error" },
834
+ envSecret: undefined,
835
+ envApi: undefined,
836
+ mockError: true,
837
+ expectStripeInit: "sk_test_error",
838
+ expectUpdate: true,
839
+ },
840
+ ];
841
+
842
+ it.each(deleteCases)(
843
+ "should handle deletion when $name",
844
+ async ({
845
+ state,
846
+ envSecret,
847
+ envApi,
848
+ mockError,
849
+ expectStripeInit,
850
+ expectUpdate,
851
+ }) => {
852
+ const originalSecret = process.env.STRIPE_SECRET_KEY;
853
+ const originalApi = process.env.STRIPE_API_KEY;
854
+
855
+ if (envSecret !== undefined)
856
+ process.env.STRIPE_SECRET_KEY = envSecret;
857
+ else delete process.env.STRIPE_SECRET_KEY;
858
+
859
+ if (envApi !== undefined) process.env.STRIPE_API_KEY = envApi;
860
+ else delete process.env.STRIPE_API_KEY;
861
+
862
+ if (mockError) {
863
+ mockPricesUpdate.mockRejectedValueOnce(new Error("Stripe error"));
864
+ }
865
+
866
+ await provider.delete("price_123", state as any);
867
+
868
+ if (expectStripeInit) {
869
+ expect(MockedStripe).toHaveBeenCalledWith(expectStripeInit);
870
+ }
871
+
872
+ if (expectUpdate) {
873
+ expect(mockPricesUpdate).toHaveBeenCalledWith("price_123", {
874
+ active: false,
875
+ });
876
+ } else {
877
+ expect(mockPricesUpdate).not.toHaveBeenCalled();
878
+ }
879
+
880
+ if (originalSecret !== undefined)
881
+ process.env.STRIPE_SECRET_KEY = originalSecret;
882
+ else delete process.env.STRIPE_SECRET_KEY;
883
+
884
+ if (originalApi !== undefined)
885
+ process.env.STRIPE_API_KEY = originalApi;
886
+ else delete process.env.STRIPE_API_KEY;
887
+ },
888
+ );
889
+ });
890
+ });
322
891
  });