@better-auth/stripe 1.5.0-beta.8 → 1.5.0
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.
- package/README.md +17 -0
- package/dist/client.d.mts +52 -171
- package/dist/client.mjs +3 -2
- package/dist/client.mjs.map +1 -0
- package/dist/{error-codes-Clj-xYDP.mjs → error-codes-CCosYkXx.mjs} +4 -1
- package/dist/error-codes-CCosYkXx.mjs.map +1 -0
- package/dist/index.d.mts +519 -2
- package/dist/index.mjs +586 -232
- package/dist/index.mjs.map +1 -0
- package/dist/types-OT6L84x4.d.mts +480 -0
- package/package.json +28 -22
- package/.turbo/turbo-build.log +0 -17
- package/CHANGELOG.md +0 -24
- package/dist/index-BqGWQFAv.d.mts +0 -1015
- package/src/client.ts +0 -38
- package/src/error-codes.ts +0 -30
- package/src/hooks.ts +0 -435
- package/src/index.ts +0 -314
- package/src/middleware.ts +0 -104
- package/src/routes.ts +0 -1681
- package/src/schema.ts +0 -128
- package/src/types.ts +0 -449
- package/src/utils.ts +0 -70
- package/test/stripe-organization.test.ts +0 -1993
- package/test/stripe.test.ts +0 -4807
- package/tsconfig.json +0 -14
- package/tsdown.config.ts +0 -8
- package/vitest.config.ts +0 -8
package/test/stripe.test.ts
DELETED
|
@@ -1,4807 +0,0 @@
|
|
|
1
|
-
import { runWithEndpointContext } from "@better-auth/core/context";
|
|
2
|
-
import type { Auth, User } from "better-auth";
|
|
3
|
-
import { memoryAdapter } from "better-auth/adapters/memory";
|
|
4
|
-
import { organization } from "better-auth/plugins/organization";
|
|
5
|
-
import { getTestInstance } from "better-auth/test";
|
|
6
|
-
import type Stripe from "stripe";
|
|
7
|
-
import {
|
|
8
|
-
assert,
|
|
9
|
-
beforeEach,
|
|
10
|
-
describe,
|
|
11
|
-
expect,
|
|
12
|
-
expectTypeOf,
|
|
13
|
-
it,
|
|
14
|
-
vi,
|
|
15
|
-
} from "vitest";
|
|
16
|
-
import type { StripePlugin } from "../src";
|
|
17
|
-
import { stripe } from "../src";
|
|
18
|
-
import { stripeClient } from "../src/client";
|
|
19
|
-
import type { StripeOptions, Subscription } from "../src/types";
|
|
20
|
-
|
|
21
|
-
describe("stripe type", () => {
|
|
22
|
-
it("should api endpoint exists", () => {
|
|
23
|
-
type Plugins = [
|
|
24
|
-
StripePlugin<{
|
|
25
|
-
stripeClient: Stripe;
|
|
26
|
-
stripeWebhookSecret: string;
|
|
27
|
-
subscription: {
|
|
28
|
-
enabled: false;
|
|
29
|
-
};
|
|
30
|
-
}>,
|
|
31
|
-
];
|
|
32
|
-
type MyAuth = Auth<{
|
|
33
|
-
plugins: Plugins;
|
|
34
|
-
}>;
|
|
35
|
-
expectTypeOf<MyAuth["api"]["stripeWebhook"]>().toBeFunction();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("should have subscription endpoints", () => {
|
|
39
|
-
type Plugins = [
|
|
40
|
-
StripePlugin<{
|
|
41
|
-
stripeClient: Stripe;
|
|
42
|
-
stripeWebhookSecret: string;
|
|
43
|
-
subscription: {
|
|
44
|
-
enabled: true;
|
|
45
|
-
plans: [];
|
|
46
|
-
};
|
|
47
|
-
}>,
|
|
48
|
-
];
|
|
49
|
-
type MyAuth = Auth<{
|
|
50
|
-
plugins: Plugins;
|
|
51
|
-
}>;
|
|
52
|
-
expectTypeOf<MyAuth["api"]["stripeWebhook"]>().toBeFunction();
|
|
53
|
-
expectTypeOf<MyAuth["api"]["subscriptionSuccess"]>().toBeFunction();
|
|
54
|
-
expectTypeOf<MyAuth["api"]["listActiveSubscriptions"]>().toBeFunction();
|
|
55
|
-
expectTypeOf<MyAuth["api"]["cancelSubscriptionCallback"]>().toBeFunction();
|
|
56
|
-
expectTypeOf<MyAuth["api"]["cancelSubscription"]>().toBeFunction();
|
|
57
|
-
expectTypeOf<MyAuth["api"]["restoreSubscription"]>().toBeFunction();
|
|
58
|
-
expectTypeOf<MyAuth["api"]["upgradeSubscription"]>().toBeFunction();
|
|
59
|
-
expectTypeOf<MyAuth["api"]["createBillingPortal"]>().toBeFunction();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("should infer plugin schema fields on user type", async () => {
|
|
63
|
-
const { auth } = await getTestInstance({
|
|
64
|
-
plugins: [
|
|
65
|
-
stripe({
|
|
66
|
-
stripeClient: {} as Stripe,
|
|
67
|
-
stripeWebhookSecret: "test",
|
|
68
|
-
}),
|
|
69
|
-
],
|
|
70
|
-
});
|
|
71
|
-
expectTypeOf<
|
|
72
|
-
(typeof auth)["$Infer"]["Session"]["user"]["stripeCustomerId"]
|
|
73
|
-
>().toEqualTypeOf<string | null | undefined>();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("should infer plugin schema fields alongside additional user fields", async () => {
|
|
77
|
-
const { auth } = await getTestInstance({
|
|
78
|
-
plugins: [
|
|
79
|
-
stripe({
|
|
80
|
-
stripeClient: {} as Stripe,
|
|
81
|
-
stripeWebhookSecret: "test",
|
|
82
|
-
}),
|
|
83
|
-
],
|
|
84
|
-
user: {
|
|
85
|
-
additionalFields: {
|
|
86
|
-
customField: {
|
|
87
|
-
type: "string",
|
|
88
|
-
required: false,
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
expectTypeOf<
|
|
94
|
-
(typeof auth)["$Infer"]["Session"]["user"]["stripeCustomerId"]
|
|
95
|
-
>().toEqualTypeOf<string | null | undefined>();
|
|
96
|
-
expectTypeOf<
|
|
97
|
-
(typeof auth)["$Infer"]["Session"]["user"]["customField"]
|
|
98
|
-
>().toEqualTypeOf<string | null | undefined>();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe("stripe", () => {
|
|
103
|
-
const mockStripe = {
|
|
104
|
-
prices: {
|
|
105
|
-
list: vi.fn().mockResolvedValue({ data: [{ id: "price_lookup_123" }] }),
|
|
106
|
-
},
|
|
107
|
-
customers: {
|
|
108
|
-
create: vi.fn().mockResolvedValue({ id: "cus_mock123" }),
|
|
109
|
-
list: vi.fn().mockResolvedValue({ data: [] }),
|
|
110
|
-
search: vi.fn().mockResolvedValue({ data: [] }),
|
|
111
|
-
retrieve: vi.fn().mockResolvedValue({
|
|
112
|
-
id: "cus_mock123",
|
|
113
|
-
email: "test@email.com",
|
|
114
|
-
deleted: false,
|
|
115
|
-
}),
|
|
116
|
-
update: vi.fn().mockResolvedValue({
|
|
117
|
-
id: "cus_mock123",
|
|
118
|
-
email: "newemail@example.com",
|
|
119
|
-
}),
|
|
120
|
-
},
|
|
121
|
-
checkout: {
|
|
122
|
-
sessions: {
|
|
123
|
-
create: vi.fn().mockResolvedValue({
|
|
124
|
-
url: "https://checkout.stripe.com/mock",
|
|
125
|
-
id: "",
|
|
126
|
-
}),
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
billingPortal: {
|
|
130
|
-
sessions: {
|
|
131
|
-
create: vi
|
|
132
|
-
.fn()
|
|
133
|
-
.mockResolvedValue({ url: "https://billing.stripe.com/mock" }),
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
subscriptions: {
|
|
137
|
-
retrieve: vi.fn(),
|
|
138
|
-
list: vi.fn().mockResolvedValue({ data: [] }),
|
|
139
|
-
update: vi.fn(),
|
|
140
|
-
},
|
|
141
|
-
webhooks: {
|
|
142
|
-
constructEventAsync: vi.fn(),
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
const _stripe = mockStripe as unknown as Stripe;
|
|
146
|
-
const stripeOptions = {
|
|
147
|
-
stripeClient: _stripe,
|
|
148
|
-
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
149
|
-
createCustomerOnSignUp: true,
|
|
150
|
-
subscription: {
|
|
151
|
-
enabled: true,
|
|
152
|
-
plans: [
|
|
153
|
-
{
|
|
154
|
-
priceId: process.env.STRIPE_PRICE_ID_1!,
|
|
155
|
-
name: "starter",
|
|
156
|
-
lookupKey: "lookup_key_123",
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
priceId: process.env.STRIPE_PRICE_ID_2!,
|
|
160
|
-
name: "premium",
|
|
161
|
-
lookupKey: "lookup_key_234",
|
|
162
|
-
},
|
|
163
|
-
],
|
|
164
|
-
},
|
|
165
|
-
} satisfies StripeOptions;
|
|
166
|
-
|
|
167
|
-
const testUser = {
|
|
168
|
-
email: "test@email.com",
|
|
169
|
-
password: "password",
|
|
170
|
-
name: "Test User",
|
|
171
|
-
};
|
|
172
|
-
const data = {
|
|
173
|
-
user: [],
|
|
174
|
-
session: [],
|
|
175
|
-
verification: [],
|
|
176
|
-
account: [],
|
|
177
|
-
customer: [],
|
|
178
|
-
subscription: [],
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
beforeEach(() => {
|
|
182
|
-
data.user = [];
|
|
183
|
-
data.session = [];
|
|
184
|
-
data.verification = [];
|
|
185
|
-
data.account = [];
|
|
186
|
-
data.customer = [];
|
|
187
|
-
data.subscription = [];
|
|
188
|
-
|
|
189
|
-
vi.clearAllMocks();
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
const memory = memoryAdapter(data);
|
|
193
|
-
|
|
194
|
-
it("should create a customer on sign up", async () => {
|
|
195
|
-
const { client, auth } = await getTestInstance(
|
|
196
|
-
{
|
|
197
|
-
database: memory,
|
|
198
|
-
plugins: [stripe(stripeOptions)],
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
disableTestUser: true,
|
|
202
|
-
clientOptions: {
|
|
203
|
-
plugins: [stripeClient({ subscription: true })],
|
|
204
|
-
},
|
|
205
|
-
},
|
|
206
|
-
);
|
|
207
|
-
const ctx = await auth.$context;
|
|
208
|
-
|
|
209
|
-
const userRes = await client.signUp.email(testUser, {
|
|
210
|
-
throw: true,
|
|
211
|
-
});
|
|
212
|
-
const res = await ctx.adapter.findOne<User>({
|
|
213
|
-
model: "user",
|
|
214
|
-
where: [
|
|
215
|
-
{
|
|
216
|
-
field: "id",
|
|
217
|
-
value: userRes.user.id,
|
|
218
|
-
},
|
|
219
|
-
],
|
|
220
|
-
});
|
|
221
|
-
expect(res).toMatchObject({
|
|
222
|
-
id: expect.any(String),
|
|
223
|
-
stripeCustomerId: expect.any(String),
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("should create a subscription", async () => {
|
|
228
|
-
const { client, auth, sessionSetter } = await getTestInstance(
|
|
229
|
-
{
|
|
230
|
-
database: memory,
|
|
231
|
-
plugins: [stripe(stripeOptions)],
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
disableTestUser: true,
|
|
235
|
-
clientOptions: {
|
|
236
|
-
plugins: [stripeClient({ subscription: true })],
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
);
|
|
240
|
-
const ctx = await auth.$context;
|
|
241
|
-
|
|
242
|
-
const userRes = await client.signUp.email(testUser, {
|
|
243
|
-
throw: true,
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
const headers = new Headers();
|
|
247
|
-
await client.signIn.email(testUser, {
|
|
248
|
-
throw: true,
|
|
249
|
-
onSuccess: sessionSetter(headers),
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
const res = await client.subscription.upgrade({
|
|
253
|
-
plan: "starter",
|
|
254
|
-
fetchOptions: {
|
|
255
|
-
headers,
|
|
256
|
-
},
|
|
257
|
-
});
|
|
258
|
-
expect(res.data?.url).toBeDefined();
|
|
259
|
-
const subscription = await ctx.adapter.findOne<Subscription>({
|
|
260
|
-
model: "subscription",
|
|
261
|
-
where: [
|
|
262
|
-
{
|
|
263
|
-
field: "referenceId",
|
|
264
|
-
value: userRes.user.id,
|
|
265
|
-
},
|
|
266
|
-
],
|
|
267
|
-
});
|
|
268
|
-
expect(subscription).toMatchObject({
|
|
269
|
-
id: expect.any(String),
|
|
270
|
-
plan: "starter",
|
|
271
|
-
referenceId: userRes.user.id,
|
|
272
|
-
stripeCustomerId: expect.any(String),
|
|
273
|
-
status: "incomplete",
|
|
274
|
-
periodStart: undefined,
|
|
275
|
-
cancelAtPeriodEnd: false,
|
|
276
|
-
trialStart: undefined,
|
|
277
|
-
trialEnd: undefined,
|
|
278
|
-
});
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it("should not allow cross-user subscriptionId operations (upgrade/cancel/restore)", async () => {
|
|
282
|
-
const { client, auth, sessionSetter } = await getTestInstance(
|
|
283
|
-
{
|
|
284
|
-
database: memory,
|
|
285
|
-
plugins: [stripe(stripeOptions)],
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
disableTestUser: true,
|
|
289
|
-
clientOptions: {
|
|
290
|
-
plugins: [stripeClient({ subscription: true })],
|
|
291
|
-
},
|
|
292
|
-
},
|
|
293
|
-
);
|
|
294
|
-
const ctx = await auth.$context;
|
|
295
|
-
|
|
296
|
-
const userA = {
|
|
297
|
-
email: "user-a@email.com",
|
|
298
|
-
password: "password",
|
|
299
|
-
name: "User A",
|
|
300
|
-
};
|
|
301
|
-
const userARes = await client.signUp.email(userA, { throw: true });
|
|
302
|
-
|
|
303
|
-
const userAHeaders = new Headers();
|
|
304
|
-
await client.signIn.email(userA, {
|
|
305
|
-
throw: true,
|
|
306
|
-
onSuccess: sessionSetter(userAHeaders),
|
|
307
|
-
});
|
|
308
|
-
await client.subscription.upgrade({
|
|
309
|
-
plan: "starter",
|
|
310
|
-
fetchOptions: { headers: userAHeaders },
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
const userASub = await ctx.adapter.findOne<Subscription>({
|
|
314
|
-
model: "subscription",
|
|
315
|
-
where: [{ field: "referenceId", value: userARes.user.id }],
|
|
316
|
-
});
|
|
317
|
-
expect(userASub).toBeTruthy();
|
|
318
|
-
|
|
319
|
-
const userB = {
|
|
320
|
-
email: "user-b@email.com",
|
|
321
|
-
password: "password",
|
|
322
|
-
name: "User B",
|
|
323
|
-
};
|
|
324
|
-
await client.signUp.email(userB, { throw: true });
|
|
325
|
-
const userBHeaders = new Headers();
|
|
326
|
-
await client.signIn.email(userB, {
|
|
327
|
-
throw: true,
|
|
328
|
-
onSuccess: sessionSetter(userBHeaders),
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
mockStripe.checkout.sessions.create.mockClear();
|
|
332
|
-
mockStripe.billingPortal.sessions.create.mockClear();
|
|
333
|
-
mockStripe.subscriptions.list.mockClear();
|
|
334
|
-
mockStripe.subscriptions.update.mockClear();
|
|
335
|
-
|
|
336
|
-
const upgradeRes = await client.subscription.upgrade({
|
|
337
|
-
plan: "premium",
|
|
338
|
-
subscriptionId: userASub!.id,
|
|
339
|
-
fetchOptions: { headers: userBHeaders },
|
|
340
|
-
});
|
|
341
|
-
expect(upgradeRes.error?.message).toContain("Subscription not found");
|
|
342
|
-
expect(mockStripe.checkout.sessions.create).not.toHaveBeenCalled();
|
|
343
|
-
expect(mockStripe.billingPortal.sessions.create).not.toHaveBeenCalled();
|
|
344
|
-
|
|
345
|
-
const cancelHeaders = new Headers(userBHeaders);
|
|
346
|
-
cancelHeaders.set("content-type", "application/json");
|
|
347
|
-
const cancelResponse = await auth.handler(
|
|
348
|
-
new Request("http://localhost:3000/api/auth/subscription/cancel", {
|
|
349
|
-
method: "POST",
|
|
350
|
-
headers: cancelHeaders,
|
|
351
|
-
body: JSON.stringify({
|
|
352
|
-
subscriptionId: userASub!.id,
|
|
353
|
-
returnUrl: "/account",
|
|
354
|
-
}),
|
|
355
|
-
}),
|
|
356
|
-
);
|
|
357
|
-
expect(cancelResponse.status).toBe(400);
|
|
358
|
-
expect((await cancelResponse.json()).message).toContain(
|
|
359
|
-
"Subscription not found",
|
|
360
|
-
);
|
|
361
|
-
expect(mockStripe.billingPortal.sessions.create).not.toHaveBeenCalled();
|
|
362
|
-
|
|
363
|
-
const restoreHeaders = new Headers(userBHeaders);
|
|
364
|
-
restoreHeaders.set("content-type", "application/json");
|
|
365
|
-
const restoreResponse = await auth.handler(
|
|
366
|
-
new Request("http://localhost:3000/api/auth/subscription/restore", {
|
|
367
|
-
method: "POST",
|
|
368
|
-
headers: restoreHeaders,
|
|
369
|
-
body: JSON.stringify({
|
|
370
|
-
subscriptionId: userASub!.id,
|
|
371
|
-
}),
|
|
372
|
-
}),
|
|
373
|
-
);
|
|
374
|
-
expect(restoreResponse.status).toBe(400);
|
|
375
|
-
expect((await restoreResponse.json()).message).toContain(
|
|
376
|
-
"Subscription not found",
|
|
377
|
-
);
|
|
378
|
-
expect(mockStripe.subscriptions.update).not.toHaveBeenCalled();
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it("should pass metadata to subscription when upgrading", async () => {
|
|
382
|
-
const { client, sessionSetter } = await getTestInstance(
|
|
383
|
-
{
|
|
384
|
-
database: memory,
|
|
385
|
-
plugins: [stripe(stripeOptions)],
|
|
386
|
-
},
|
|
387
|
-
{
|
|
388
|
-
disableTestUser: true,
|
|
389
|
-
clientOptions: {
|
|
390
|
-
plugins: [
|
|
391
|
-
stripeClient({
|
|
392
|
-
subscription: true,
|
|
393
|
-
}),
|
|
394
|
-
],
|
|
395
|
-
},
|
|
396
|
-
},
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
await client.signUp.email(
|
|
400
|
-
{
|
|
401
|
-
...testUser,
|
|
402
|
-
email: "metadata-test@email.com",
|
|
403
|
-
},
|
|
404
|
-
{
|
|
405
|
-
throw: true,
|
|
406
|
-
},
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
const headers = new Headers();
|
|
410
|
-
await client.signIn.email(
|
|
411
|
-
{
|
|
412
|
-
...testUser,
|
|
413
|
-
email: "metadata-test@email.com",
|
|
414
|
-
},
|
|
415
|
-
{
|
|
416
|
-
throw: true,
|
|
417
|
-
onSuccess: sessionSetter(headers),
|
|
418
|
-
},
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
const customMetadata = {
|
|
422
|
-
customField: "customValue",
|
|
423
|
-
organizationId: "org_123",
|
|
424
|
-
projectId: "proj_456",
|
|
425
|
-
};
|
|
426
|
-
|
|
427
|
-
await client.subscription.upgrade({
|
|
428
|
-
plan: "starter",
|
|
429
|
-
metadata: customMetadata,
|
|
430
|
-
fetchOptions: {
|
|
431
|
-
headers,
|
|
432
|
-
},
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
expect(mockStripe.checkout.sessions.create).toHaveBeenCalledWith(
|
|
436
|
-
expect.objectContaining({
|
|
437
|
-
subscription_data: expect.objectContaining({
|
|
438
|
-
metadata: expect.objectContaining(customMetadata),
|
|
439
|
-
}),
|
|
440
|
-
metadata: expect.objectContaining(customMetadata),
|
|
441
|
-
}),
|
|
442
|
-
undefined,
|
|
443
|
-
);
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it("should list active subscriptions", async () => {
|
|
447
|
-
const { client, auth, sessionSetter } = await getTestInstance(
|
|
448
|
-
{
|
|
449
|
-
database: memory,
|
|
450
|
-
plugins: [stripe(stripeOptions)],
|
|
451
|
-
},
|
|
452
|
-
{
|
|
453
|
-
disableTestUser: true,
|
|
454
|
-
clientOptions: {
|
|
455
|
-
plugins: [stripeClient({ subscription: true })],
|
|
456
|
-
},
|
|
457
|
-
},
|
|
458
|
-
);
|
|
459
|
-
const ctx = await auth.$context;
|
|
460
|
-
|
|
461
|
-
const userRes = await client.signUp.email(
|
|
462
|
-
{
|
|
463
|
-
...testUser,
|
|
464
|
-
email: "list-test@email.com",
|
|
465
|
-
},
|
|
466
|
-
{
|
|
467
|
-
throw: true,
|
|
468
|
-
},
|
|
469
|
-
);
|
|
470
|
-
const userId = userRes.user.id;
|
|
471
|
-
|
|
472
|
-
const headers = new Headers();
|
|
473
|
-
await client.signIn.email(
|
|
474
|
-
{
|
|
475
|
-
...testUser,
|
|
476
|
-
email: "list-test@email.com",
|
|
477
|
-
},
|
|
478
|
-
{
|
|
479
|
-
throw: true,
|
|
480
|
-
onSuccess: sessionSetter(headers),
|
|
481
|
-
},
|
|
482
|
-
);
|
|
483
|
-
|
|
484
|
-
const listRes = await client.subscription.list({
|
|
485
|
-
fetchOptions: {
|
|
486
|
-
headers,
|
|
487
|
-
},
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
expect(Array.isArray(listRes.data)).toBe(true);
|
|
491
|
-
|
|
492
|
-
await client.subscription.upgrade({
|
|
493
|
-
plan: "starter",
|
|
494
|
-
fetchOptions: {
|
|
495
|
-
headers,
|
|
496
|
-
},
|
|
497
|
-
});
|
|
498
|
-
const listBeforeActive = await client.subscription.list({
|
|
499
|
-
fetchOptions: {
|
|
500
|
-
headers,
|
|
501
|
-
},
|
|
502
|
-
});
|
|
503
|
-
expect(listBeforeActive.data?.length).toBe(0);
|
|
504
|
-
// Update the subscription status to active
|
|
505
|
-
await ctx.adapter.update({
|
|
506
|
-
model: "subscription",
|
|
507
|
-
update: {
|
|
508
|
-
status: "active",
|
|
509
|
-
},
|
|
510
|
-
where: [
|
|
511
|
-
{
|
|
512
|
-
field: "referenceId",
|
|
513
|
-
value: userId,
|
|
514
|
-
},
|
|
515
|
-
],
|
|
516
|
-
});
|
|
517
|
-
const listAfterRes = await client.subscription.list({
|
|
518
|
-
fetchOptions: {
|
|
519
|
-
headers,
|
|
520
|
-
},
|
|
521
|
-
});
|
|
522
|
-
expect(listAfterRes.data?.length).toBeGreaterThan(0);
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
it("should handle subscription webhook events", async () => {
|
|
526
|
-
const { auth: testAuth } = await getTestInstance(
|
|
527
|
-
{
|
|
528
|
-
database: memory,
|
|
529
|
-
plugins: [stripe(stripeOptions)],
|
|
530
|
-
},
|
|
531
|
-
{
|
|
532
|
-
disableTestUser: true,
|
|
533
|
-
},
|
|
534
|
-
);
|
|
535
|
-
const testCtx = await testAuth.$context;
|
|
536
|
-
|
|
537
|
-
const { id: testReferenceId } = await testCtx.adapter.create({
|
|
538
|
-
model: "user",
|
|
539
|
-
data: {
|
|
540
|
-
email: "test@email.com",
|
|
541
|
-
},
|
|
542
|
-
});
|
|
543
|
-
const { id: testSubscriptionId } = await testCtx.adapter.create({
|
|
544
|
-
model: "subscription",
|
|
545
|
-
data: {
|
|
546
|
-
referenceId: testReferenceId,
|
|
547
|
-
stripeCustomerId: "cus_mock123",
|
|
548
|
-
status: "active",
|
|
549
|
-
plan: "starter",
|
|
550
|
-
},
|
|
551
|
-
});
|
|
552
|
-
const mockCheckoutSessionEvent = {
|
|
553
|
-
type: "checkout.session.completed",
|
|
554
|
-
data: {
|
|
555
|
-
object: {
|
|
556
|
-
mode: "subscription",
|
|
557
|
-
subscription: testSubscriptionId,
|
|
558
|
-
metadata: {
|
|
559
|
-
referenceId: testReferenceId,
|
|
560
|
-
subscriptionId: testSubscriptionId,
|
|
561
|
-
},
|
|
562
|
-
},
|
|
563
|
-
},
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
const mockSubscription = {
|
|
567
|
-
id: testSubscriptionId,
|
|
568
|
-
status: "active",
|
|
569
|
-
items: {
|
|
570
|
-
data: [
|
|
571
|
-
{
|
|
572
|
-
price: { id: process.env.STRIPE_PRICE_ID_1 },
|
|
573
|
-
quantity: 1,
|
|
574
|
-
},
|
|
575
|
-
],
|
|
576
|
-
},
|
|
577
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
578
|
-
current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
const stripeForTest = {
|
|
582
|
-
...stripeOptions.stripeClient,
|
|
583
|
-
subscriptions: {
|
|
584
|
-
...stripeOptions.stripeClient.subscriptions,
|
|
585
|
-
retrieve: vi.fn().mockResolvedValue(mockSubscription),
|
|
586
|
-
},
|
|
587
|
-
webhooks: {
|
|
588
|
-
constructEventAsync: vi
|
|
589
|
-
.fn()
|
|
590
|
-
.mockResolvedValue(mockCheckoutSessionEvent),
|
|
591
|
-
},
|
|
592
|
-
};
|
|
593
|
-
|
|
594
|
-
const testOptions = {
|
|
595
|
-
...stripeOptions,
|
|
596
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
597
|
-
stripeWebhookSecret: "test_secret",
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
const { auth: webhookTestAuth } = await getTestInstance(
|
|
601
|
-
{
|
|
602
|
-
database: memory,
|
|
603
|
-
plugins: [stripe(testOptions)],
|
|
604
|
-
},
|
|
605
|
-
{
|
|
606
|
-
disableTestUser: true,
|
|
607
|
-
},
|
|
608
|
-
);
|
|
609
|
-
|
|
610
|
-
const webhookTestCtx = await webhookTestAuth.$context;
|
|
611
|
-
|
|
612
|
-
const mockRequest = new Request(
|
|
613
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
614
|
-
{
|
|
615
|
-
method: "POST",
|
|
616
|
-
headers: {
|
|
617
|
-
"stripe-signature": "test_signature",
|
|
618
|
-
},
|
|
619
|
-
body: JSON.stringify(mockCheckoutSessionEvent),
|
|
620
|
-
},
|
|
621
|
-
);
|
|
622
|
-
const response = await webhookTestAuth.handler(mockRequest);
|
|
623
|
-
expect(response.status).toBe(200);
|
|
624
|
-
|
|
625
|
-
const updatedSubscription =
|
|
626
|
-
await webhookTestCtx.adapter.findOne<Subscription>({
|
|
627
|
-
model: "subscription",
|
|
628
|
-
where: [
|
|
629
|
-
{
|
|
630
|
-
field: "id",
|
|
631
|
-
value: testSubscriptionId,
|
|
632
|
-
},
|
|
633
|
-
],
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
expect(updatedSubscription).toMatchObject({
|
|
637
|
-
id: testSubscriptionId,
|
|
638
|
-
status: "active",
|
|
639
|
-
periodStart: expect.any(Date),
|
|
640
|
-
periodEnd: expect.any(Date),
|
|
641
|
-
plan: "starter",
|
|
642
|
-
});
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
it("should handle subscription webhook events with trial", async () => {
|
|
646
|
-
const { auth: testAuth } = await getTestInstance(
|
|
647
|
-
{
|
|
648
|
-
database: memory,
|
|
649
|
-
plugins: [stripe(stripeOptions)],
|
|
650
|
-
},
|
|
651
|
-
{
|
|
652
|
-
disableTestUser: true,
|
|
653
|
-
},
|
|
654
|
-
);
|
|
655
|
-
const testCtx = await testAuth.$context;
|
|
656
|
-
|
|
657
|
-
const { id: testReferenceId } = await testCtx.adapter.create({
|
|
658
|
-
model: "user",
|
|
659
|
-
data: {
|
|
660
|
-
email: "test@email.com",
|
|
661
|
-
},
|
|
662
|
-
});
|
|
663
|
-
const { id: testSubscriptionId } = await testCtx.adapter.create({
|
|
664
|
-
model: "subscription",
|
|
665
|
-
data: {
|
|
666
|
-
referenceId: testReferenceId,
|
|
667
|
-
stripeCustomerId: "cus_mock123",
|
|
668
|
-
status: "incomplete",
|
|
669
|
-
plan: "starter",
|
|
670
|
-
},
|
|
671
|
-
});
|
|
672
|
-
const mockCheckoutSessionEvent = {
|
|
673
|
-
type: "checkout.session.completed",
|
|
674
|
-
data: {
|
|
675
|
-
object: {
|
|
676
|
-
mode: "subscription",
|
|
677
|
-
subscription: testSubscriptionId,
|
|
678
|
-
metadata: {
|
|
679
|
-
referenceId: testReferenceId,
|
|
680
|
-
subscriptionId: testSubscriptionId,
|
|
681
|
-
},
|
|
682
|
-
},
|
|
683
|
-
},
|
|
684
|
-
};
|
|
685
|
-
|
|
686
|
-
const mockSubscription = {
|
|
687
|
-
id: testSubscriptionId,
|
|
688
|
-
status: "active",
|
|
689
|
-
items: {
|
|
690
|
-
data: [
|
|
691
|
-
{
|
|
692
|
-
price: { id: process.env.STRIPE_PRICE_ID_1 },
|
|
693
|
-
quantity: 1,
|
|
694
|
-
},
|
|
695
|
-
],
|
|
696
|
-
},
|
|
697
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
698
|
-
current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
699
|
-
trial_start: Math.floor(Date.now() / 1000),
|
|
700
|
-
trial_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
701
|
-
};
|
|
702
|
-
|
|
703
|
-
const stripeForTest = {
|
|
704
|
-
...stripeOptions.stripeClient,
|
|
705
|
-
subscriptions: {
|
|
706
|
-
...stripeOptions.stripeClient.subscriptions,
|
|
707
|
-
retrieve: vi.fn().mockResolvedValue(mockSubscription),
|
|
708
|
-
},
|
|
709
|
-
webhooks: {
|
|
710
|
-
constructEventAsync: vi
|
|
711
|
-
.fn()
|
|
712
|
-
.mockResolvedValue(mockCheckoutSessionEvent),
|
|
713
|
-
},
|
|
714
|
-
};
|
|
715
|
-
|
|
716
|
-
const testOptions = {
|
|
717
|
-
...stripeOptions,
|
|
718
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
719
|
-
stripeWebhookSecret: "test_secret",
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
const { auth: webhookTestAuth } = await getTestInstance(
|
|
723
|
-
{
|
|
724
|
-
database: memory,
|
|
725
|
-
plugins: [stripe(testOptions)],
|
|
726
|
-
},
|
|
727
|
-
{
|
|
728
|
-
disableTestUser: true,
|
|
729
|
-
},
|
|
730
|
-
);
|
|
731
|
-
|
|
732
|
-
const webhookTestCtx = await webhookTestAuth.$context;
|
|
733
|
-
|
|
734
|
-
const mockRequest = new Request(
|
|
735
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
736
|
-
{
|
|
737
|
-
method: "POST",
|
|
738
|
-
headers: {
|
|
739
|
-
"stripe-signature": "test_signature",
|
|
740
|
-
},
|
|
741
|
-
body: JSON.stringify(mockCheckoutSessionEvent),
|
|
742
|
-
},
|
|
743
|
-
);
|
|
744
|
-
const response = await webhookTestAuth.handler(mockRequest);
|
|
745
|
-
expect(response.status).toBe(200);
|
|
746
|
-
|
|
747
|
-
const updatedSubscription =
|
|
748
|
-
await webhookTestCtx.adapter.findOne<Subscription>({
|
|
749
|
-
model: "subscription",
|
|
750
|
-
where: [
|
|
751
|
-
{
|
|
752
|
-
field: "id",
|
|
753
|
-
value: testSubscriptionId,
|
|
754
|
-
},
|
|
755
|
-
],
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
expect(updatedSubscription).toMatchObject({
|
|
759
|
-
id: testSubscriptionId,
|
|
760
|
-
status: "active",
|
|
761
|
-
periodStart: expect.any(Date),
|
|
762
|
-
periodEnd: expect.any(Date),
|
|
763
|
-
plan: "starter",
|
|
764
|
-
trialStart: expect.any(Date),
|
|
765
|
-
trialEnd: expect.any(Date),
|
|
766
|
-
});
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
it("should handle subscription deletion webhook", async () => {
|
|
770
|
-
const { auth: testAuth } = await getTestInstance(
|
|
771
|
-
{
|
|
772
|
-
database: memory,
|
|
773
|
-
plugins: [stripe(stripeOptions)],
|
|
774
|
-
},
|
|
775
|
-
{
|
|
776
|
-
disableTestUser: true,
|
|
777
|
-
},
|
|
778
|
-
);
|
|
779
|
-
const testCtx = await testAuth.$context;
|
|
780
|
-
|
|
781
|
-
const { id: userId } = await testCtx.adapter.create({
|
|
782
|
-
model: "user",
|
|
783
|
-
data: {
|
|
784
|
-
email: "delete-test@email.com",
|
|
785
|
-
},
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
const subId = "test_sub_delete";
|
|
789
|
-
|
|
790
|
-
await testCtx.adapter.create({
|
|
791
|
-
model: "subscription",
|
|
792
|
-
data: {
|
|
793
|
-
referenceId: userId,
|
|
794
|
-
stripeCustomerId: "cus_delete_test",
|
|
795
|
-
status: "active",
|
|
796
|
-
plan: "starter",
|
|
797
|
-
stripeSubscriptionId: "sub_delete_test",
|
|
798
|
-
},
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
const subscription = await testCtx.adapter.findOne<Subscription>({
|
|
802
|
-
model: "subscription",
|
|
803
|
-
where: [
|
|
804
|
-
{
|
|
805
|
-
field: "referenceId",
|
|
806
|
-
value: userId,
|
|
807
|
-
},
|
|
808
|
-
],
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
const mockDeleteEvent = {
|
|
812
|
-
type: "customer.subscription.deleted",
|
|
813
|
-
data: {
|
|
814
|
-
object: {
|
|
815
|
-
id: "sub_delete_test",
|
|
816
|
-
customer: subscription?.stripeCustomerId,
|
|
817
|
-
status: "canceled",
|
|
818
|
-
metadata: {
|
|
819
|
-
referenceId: subscription?.referenceId,
|
|
820
|
-
subscriptionId: subscription?.id,
|
|
821
|
-
},
|
|
822
|
-
},
|
|
823
|
-
},
|
|
824
|
-
};
|
|
825
|
-
|
|
826
|
-
const stripeForTest = {
|
|
827
|
-
...stripeOptions.stripeClient,
|
|
828
|
-
webhooks: {
|
|
829
|
-
constructEventAsync: vi.fn().mockResolvedValue(mockDeleteEvent),
|
|
830
|
-
},
|
|
831
|
-
subscriptions: {
|
|
832
|
-
retrieve: vi.fn().mockResolvedValue({
|
|
833
|
-
status: "canceled",
|
|
834
|
-
id: subId,
|
|
835
|
-
}),
|
|
836
|
-
},
|
|
837
|
-
};
|
|
838
|
-
|
|
839
|
-
const testOptions = {
|
|
840
|
-
...stripeOptions,
|
|
841
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
842
|
-
stripeWebhookSecret: "test_secret",
|
|
843
|
-
};
|
|
844
|
-
|
|
845
|
-
const { auth: webhookTestAuth } = await getTestInstance(
|
|
846
|
-
{
|
|
847
|
-
database: memory,
|
|
848
|
-
plugins: [stripe(testOptions)],
|
|
849
|
-
},
|
|
850
|
-
{
|
|
851
|
-
disableTestUser: true,
|
|
852
|
-
},
|
|
853
|
-
);
|
|
854
|
-
|
|
855
|
-
const webhookTestCtx = await webhookTestAuth.$context;
|
|
856
|
-
|
|
857
|
-
const mockRequest = new Request(
|
|
858
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
859
|
-
{
|
|
860
|
-
method: "POST",
|
|
861
|
-
headers: {
|
|
862
|
-
"stripe-signature": "test_signature",
|
|
863
|
-
},
|
|
864
|
-
body: JSON.stringify(mockDeleteEvent),
|
|
865
|
-
},
|
|
866
|
-
);
|
|
867
|
-
|
|
868
|
-
const response = await webhookTestAuth.handler(mockRequest);
|
|
869
|
-
expect(response.status).toBe(200);
|
|
870
|
-
|
|
871
|
-
if (subscription) {
|
|
872
|
-
const updatedSubscription =
|
|
873
|
-
await webhookTestCtx.adapter.findOne<Subscription>({
|
|
874
|
-
model: "subscription",
|
|
875
|
-
where: [
|
|
876
|
-
{
|
|
877
|
-
field: "id",
|
|
878
|
-
value: subscription.id,
|
|
879
|
-
},
|
|
880
|
-
],
|
|
881
|
-
});
|
|
882
|
-
expect(updatedSubscription?.status).toBe("canceled");
|
|
883
|
-
}
|
|
884
|
-
});
|
|
885
|
-
|
|
886
|
-
it("should handle customer.subscription.created webhook event", async () => {
|
|
887
|
-
const stripeForTest = {
|
|
888
|
-
...stripeOptions.stripeClient,
|
|
889
|
-
webhooks: {
|
|
890
|
-
constructEventAsync: vi.fn(),
|
|
891
|
-
},
|
|
892
|
-
};
|
|
893
|
-
|
|
894
|
-
const testOptions = {
|
|
895
|
-
...stripeOptions,
|
|
896
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
897
|
-
stripeWebhookSecret: "test_secret",
|
|
898
|
-
};
|
|
899
|
-
|
|
900
|
-
const { auth: testAuth } = await getTestInstance(
|
|
901
|
-
{
|
|
902
|
-
database: memory,
|
|
903
|
-
plugins: [stripe(testOptions)],
|
|
904
|
-
},
|
|
905
|
-
{
|
|
906
|
-
disableTestUser: true,
|
|
907
|
-
},
|
|
908
|
-
);
|
|
909
|
-
const testCtx = await testAuth.$context;
|
|
910
|
-
|
|
911
|
-
// Create a user with stripeCustomerId
|
|
912
|
-
const userWithCustomerId = await testCtx.adapter.create({
|
|
913
|
-
model: "user",
|
|
914
|
-
data: {
|
|
915
|
-
email: "dashboard-user@test.com",
|
|
916
|
-
name: "Dashboard User",
|
|
917
|
-
emailVerified: true,
|
|
918
|
-
stripeCustomerId: "cus_dashboard_test",
|
|
919
|
-
},
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
const mockEvent = {
|
|
923
|
-
type: "customer.subscription.created",
|
|
924
|
-
data: {
|
|
925
|
-
object: {
|
|
926
|
-
id: "sub_dashboard_created",
|
|
927
|
-
customer: "cus_dashboard_test",
|
|
928
|
-
status: "active",
|
|
929
|
-
items: {
|
|
930
|
-
data: [
|
|
931
|
-
{
|
|
932
|
-
price: { id: process.env.STRIPE_PRICE_ID_1 },
|
|
933
|
-
quantity: 1,
|
|
934
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
935
|
-
current_period_end:
|
|
936
|
-
Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
937
|
-
},
|
|
938
|
-
],
|
|
939
|
-
},
|
|
940
|
-
cancel_at_period_end: false,
|
|
941
|
-
},
|
|
942
|
-
},
|
|
943
|
-
};
|
|
944
|
-
|
|
945
|
-
(stripeForTest.webhooks.constructEventAsync as any).mockResolvedValue(
|
|
946
|
-
mockEvent,
|
|
947
|
-
);
|
|
948
|
-
|
|
949
|
-
const mockRequest = new Request(
|
|
950
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
951
|
-
{
|
|
952
|
-
method: "POST",
|
|
953
|
-
headers: {
|
|
954
|
-
"stripe-signature": "test_signature",
|
|
955
|
-
},
|
|
956
|
-
body: JSON.stringify(mockEvent),
|
|
957
|
-
},
|
|
958
|
-
);
|
|
959
|
-
|
|
960
|
-
const response = await testAuth.handler(mockRequest);
|
|
961
|
-
expect(response.status).toBe(200);
|
|
962
|
-
|
|
963
|
-
// Verify subscription was created in database
|
|
964
|
-
const subscription = await testCtx.adapter.findOne<Subscription>({
|
|
965
|
-
model: "subscription",
|
|
966
|
-
where: [
|
|
967
|
-
{ field: "stripeSubscriptionId", value: "sub_dashboard_created" },
|
|
968
|
-
],
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
expect(subscription).toBeDefined();
|
|
972
|
-
expect(subscription?.referenceId).toBe(userWithCustomerId.id);
|
|
973
|
-
expect(subscription?.stripeCustomerId).toBe("cus_dashboard_test");
|
|
974
|
-
expect(subscription?.status).toBe("active");
|
|
975
|
-
expect(subscription?.plan).toBe("starter");
|
|
976
|
-
expect(subscription?.seats).toBe(1);
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
it("should not create duplicate subscription if already exists", async () => {
|
|
980
|
-
const onSubscriptionCreatedCallback = vi.fn();
|
|
981
|
-
|
|
982
|
-
const stripeForTest = {
|
|
983
|
-
...stripeOptions.stripeClient,
|
|
984
|
-
webhooks: {
|
|
985
|
-
constructEventAsync: vi.fn(),
|
|
986
|
-
},
|
|
987
|
-
};
|
|
988
|
-
|
|
989
|
-
const testOptions = {
|
|
990
|
-
...stripeOptions,
|
|
991
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
992
|
-
stripeWebhookSecret: "test_secret",
|
|
993
|
-
subscription: {
|
|
994
|
-
...stripeOptions.subscription,
|
|
995
|
-
onSubscriptionCreated: onSubscriptionCreatedCallback,
|
|
996
|
-
},
|
|
997
|
-
} as StripeOptions;
|
|
998
|
-
|
|
999
|
-
const { auth: testAuth } = await getTestInstance(
|
|
1000
|
-
{
|
|
1001
|
-
database: memory,
|
|
1002
|
-
plugins: [stripe(testOptions)],
|
|
1003
|
-
},
|
|
1004
|
-
{
|
|
1005
|
-
disableTestUser: true,
|
|
1006
|
-
},
|
|
1007
|
-
);
|
|
1008
|
-
const testCtx = await testAuth.$context;
|
|
1009
|
-
|
|
1010
|
-
// Create user
|
|
1011
|
-
const user = await testCtx.adapter.create({
|
|
1012
|
-
model: "user",
|
|
1013
|
-
data: {
|
|
1014
|
-
email: "duplicate-sub@test.com",
|
|
1015
|
-
name: "Duplicate Test",
|
|
1016
|
-
emailVerified: true,
|
|
1017
|
-
stripeCustomerId: "cus_duplicate_test",
|
|
1018
|
-
},
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
// Create existing subscription
|
|
1022
|
-
await testCtx.adapter.create({
|
|
1023
|
-
model: "subscription",
|
|
1024
|
-
data: {
|
|
1025
|
-
referenceId: user.id,
|
|
1026
|
-
stripeCustomerId: "cus_duplicate_test",
|
|
1027
|
-
stripeSubscriptionId: "sub_already_exists",
|
|
1028
|
-
status: "active",
|
|
1029
|
-
plan: "starter",
|
|
1030
|
-
},
|
|
1031
|
-
});
|
|
1032
|
-
|
|
1033
|
-
const mockEvent = {
|
|
1034
|
-
type: "customer.subscription.created",
|
|
1035
|
-
data: {
|
|
1036
|
-
object: {
|
|
1037
|
-
id: "sub_already_exists",
|
|
1038
|
-
customer: "cus_duplicate_test",
|
|
1039
|
-
status: "active",
|
|
1040
|
-
items: {
|
|
1041
|
-
data: [
|
|
1042
|
-
{
|
|
1043
|
-
price: { id: process.env.STRIPE_PRICE_ID_1 },
|
|
1044
|
-
quantity: 1,
|
|
1045
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
1046
|
-
current_period_end:
|
|
1047
|
-
Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
1048
|
-
},
|
|
1049
|
-
],
|
|
1050
|
-
},
|
|
1051
|
-
cancel_at_period_end: false,
|
|
1052
|
-
},
|
|
1053
|
-
},
|
|
1054
|
-
};
|
|
1055
|
-
|
|
1056
|
-
(stripeForTest.webhooks.constructEventAsync as any).mockResolvedValue(
|
|
1057
|
-
mockEvent,
|
|
1058
|
-
);
|
|
1059
|
-
|
|
1060
|
-
const mockRequest = new Request(
|
|
1061
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
1062
|
-
{
|
|
1063
|
-
method: "POST",
|
|
1064
|
-
headers: {
|
|
1065
|
-
"stripe-signature": "test_signature",
|
|
1066
|
-
},
|
|
1067
|
-
body: JSON.stringify(mockEvent),
|
|
1068
|
-
},
|
|
1069
|
-
);
|
|
1070
|
-
|
|
1071
|
-
const response = await testAuth.handler(mockRequest);
|
|
1072
|
-
expect(response.status).toBe(200);
|
|
1073
|
-
|
|
1074
|
-
// Verify only one subscription exists (no duplicate)
|
|
1075
|
-
const subscriptions = await testCtx.adapter.findMany<Subscription>({
|
|
1076
|
-
model: "subscription",
|
|
1077
|
-
where: [
|
|
1078
|
-
{
|
|
1079
|
-
field: "stripeSubscriptionId",
|
|
1080
|
-
value: "sub_already_exists",
|
|
1081
|
-
},
|
|
1082
|
-
],
|
|
1083
|
-
});
|
|
1084
|
-
|
|
1085
|
-
expect(subscriptions.length).toBe(1);
|
|
1086
|
-
|
|
1087
|
-
// Verify callback was NOT called (early return due to existing subscription)
|
|
1088
|
-
expect(onSubscriptionCreatedCallback).not.toHaveBeenCalled();
|
|
1089
|
-
});
|
|
1090
|
-
|
|
1091
|
-
it("should skip subscription creation when user not found", async () => {
|
|
1092
|
-
const onSubscriptionCreatedCallback = vi.fn();
|
|
1093
|
-
|
|
1094
|
-
const stripeForTest = {
|
|
1095
|
-
...stripeOptions.stripeClient,
|
|
1096
|
-
webhooks: {
|
|
1097
|
-
constructEventAsync: vi.fn(),
|
|
1098
|
-
},
|
|
1099
|
-
};
|
|
1100
|
-
|
|
1101
|
-
const testOptions = {
|
|
1102
|
-
...stripeOptions,
|
|
1103
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
1104
|
-
stripeWebhookSecret: "test_secret",
|
|
1105
|
-
subscription: {
|
|
1106
|
-
...stripeOptions.subscription,
|
|
1107
|
-
onSubscriptionCreated: onSubscriptionCreatedCallback,
|
|
1108
|
-
},
|
|
1109
|
-
} as StripeOptions;
|
|
1110
|
-
|
|
1111
|
-
const { auth: testAuth } = await getTestInstance(
|
|
1112
|
-
{
|
|
1113
|
-
database: memory,
|
|
1114
|
-
plugins: [stripe(testOptions)],
|
|
1115
|
-
},
|
|
1116
|
-
{
|
|
1117
|
-
disableTestUser: true,
|
|
1118
|
-
},
|
|
1119
|
-
);
|
|
1120
|
-
const testCtx = await testAuth.$context;
|
|
1121
|
-
|
|
1122
|
-
const mockEvent = {
|
|
1123
|
-
type: "customer.subscription.created",
|
|
1124
|
-
data: {
|
|
1125
|
-
object: {
|
|
1126
|
-
id: "sub_no_user",
|
|
1127
|
-
customer: "cus_nonexistent",
|
|
1128
|
-
status: "active",
|
|
1129
|
-
items: {
|
|
1130
|
-
data: [
|
|
1131
|
-
{
|
|
1132
|
-
price: { id: process.env.STRIPE_PRICE_ID_1 },
|
|
1133
|
-
quantity: 1,
|
|
1134
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
1135
|
-
current_period_end:
|
|
1136
|
-
Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
1137
|
-
},
|
|
1138
|
-
],
|
|
1139
|
-
},
|
|
1140
|
-
cancel_at_period_end: false,
|
|
1141
|
-
},
|
|
1142
|
-
},
|
|
1143
|
-
};
|
|
1144
|
-
|
|
1145
|
-
(stripeForTest.webhooks.constructEventAsync as any).mockResolvedValue(
|
|
1146
|
-
mockEvent,
|
|
1147
|
-
);
|
|
1148
|
-
|
|
1149
|
-
const mockRequest = new Request(
|
|
1150
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
1151
|
-
{
|
|
1152
|
-
method: "POST",
|
|
1153
|
-
headers: {
|
|
1154
|
-
"stripe-signature": "test_signature",
|
|
1155
|
-
},
|
|
1156
|
-
body: JSON.stringify(mockEvent),
|
|
1157
|
-
},
|
|
1158
|
-
);
|
|
1159
|
-
|
|
1160
|
-
const response = await testAuth.handler(mockRequest);
|
|
1161
|
-
expect(response.status).toBe(200);
|
|
1162
|
-
|
|
1163
|
-
// Verify subscription was NOT created
|
|
1164
|
-
const subscription = await testCtx.adapter.findOne<Subscription>({
|
|
1165
|
-
model: "subscription",
|
|
1166
|
-
where: [{ field: "stripeSubscriptionId", value: "sub_no_user" }],
|
|
1167
|
-
});
|
|
1168
|
-
|
|
1169
|
-
expect(subscription).toBeNull();
|
|
1170
|
-
|
|
1171
|
-
// Verify callback was NOT called (early return due to user not found)
|
|
1172
|
-
expect(onSubscriptionCreatedCallback).not.toHaveBeenCalled();
|
|
1173
|
-
});
|
|
1174
|
-
|
|
1175
|
-
it("should skip subscription creation when plan not found", async () => {
|
|
1176
|
-
const onSubscriptionCreatedCallback = vi.fn();
|
|
1177
|
-
|
|
1178
|
-
const stripeForTest = {
|
|
1179
|
-
...stripeOptions.stripeClient,
|
|
1180
|
-
webhooks: {
|
|
1181
|
-
constructEventAsync: vi.fn(),
|
|
1182
|
-
},
|
|
1183
|
-
};
|
|
1184
|
-
|
|
1185
|
-
const testOptions = {
|
|
1186
|
-
...stripeOptions,
|
|
1187
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
1188
|
-
stripeWebhookSecret: "test_secret",
|
|
1189
|
-
subscription: {
|
|
1190
|
-
...stripeOptions.subscription,
|
|
1191
|
-
onSubscriptionCreated: onSubscriptionCreatedCallback,
|
|
1192
|
-
},
|
|
1193
|
-
} as StripeOptions;
|
|
1194
|
-
|
|
1195
|
-
const { auth: testAuth } = await getTestInstance(
|
|
1196
|
-
{
|
|
1197
|
-
database: memory,
|
|
1198
|
-
plugins: [stripe(testOptions)],
|
|
1199
|
-
},
|
|
1200
|
-
{
|
|
1201
|
-
disableTestUser: true,
|
|
1202
|
-
},
|
|
1203
|
-
);
|
|
1204
|
-
const testCtx = await testAuth.$context;
|
|
1205
|
-
|
|
1206
|
-
// Create user
|
|
1207
|
-
await testCtx.adapter.create({
|
|
1208
|
-
model: "user",
|
|
1209
|
-
data: {
|
|
1210
|
-
email: "no-plan@test.com",
|
|
1211
|
-
name: "No Plan User",
|
|
1212
|
-
emailVerified: true,
|
|
1213
|
-
stripeCustomerId: "cus_no_plan",
|
|
1214
|
-
},
|
|
1215
|
-
});
|
|
1216
|
-
|
|
1217
|
-
const mockEvent = {
|
|
1218
|
-
type: "customer.subscription.created",
|
|
1219
|
-
data: {
|
|
1220
|
-
object: {
|
|
1221
|
-
id: "sub_no_plan",
|
|
1222
|
-
customer: "cus_no_plan",
|
|
1223
|
-
status: "active",
|
|
1224
|
-
items: {
|
|
1225
|
-
data: [
|
|
1226
|
-
{
|
|
1227
|
-
price: { id: "price_unknown" }, // Unknown price
|
|
1228
|
-
quantity: 1,
|
|
1229
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
1230
|
-
current_period_end:
|
|
1231
|
-
Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
1232
|
-
},
|
|
1233
|
-
],
|
|
1234
|
-
},
|
|
1235
|
-
cancel_at_period_end: false,
|
|
1236
|
-
},
|
|
1237
|
-
},
|
|
1238
|
-
};
|
|
1239
|
-
|
|
1240
|
-
(stripeForTest.webhooks.constructEventAsync as any).mockResolvedValue(
|
|
1241
|
-
mockEvent,
|
|
1242
|
-
);
|
|
1243
|
-
|
|
1244
|
-
const mockRequest = new Request(
|
|
1245
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
1246
|
-
{
|
|
1247
|
-
method: "POST",
|
|
1248
|
-
headers: {
|
|
1249
|
-
"stripe-signature": "test_signature",
|
|
1250
|
-
},
|
|
1251
|
-
body: JSON.stringify(mockEvent),
|
|
1252
|
-
},
|
|
1253
|
-
);
|
|
1254
|
-
|
|
1255
|
-
const response = await testAuth.handler(mockRequest);
|
|
1256
|
-
expect(response.status).toBe(200);
|
|
1257
|
-
|
|
1258
|
-
// Verify subscription was NOT created (no matching plan)
|
|
1259
|
-
const subscription = await testCtx.adapter.findOne<Subscription>({
|
|
1260
|
-
model: "subscription",
|
|
1261
|
-
where: [{ field: "stripeSubscriptionId", value: "sub_no_plan" }],
|
|
1262
|
-
});
|
|
1263
|
-
|
|
1264
|
-
expect(subscription).toBeNull();
|
|
1265
|
-
|
|
1266
|
-
// Verify callback was NOT called (early return due to plan not found)
|
|
1267
|
-
expect(onSubscriptionCreatedCallback).not.toHaveBeenCalled();
|
|
1268
|
-
});
|
|
1269
|
-
|
|
1270
|
-
it("should skip creating subscription when metadata.subscriptionId exists", async () => {
|
|
1271
|
-
const stripeForTest = {
|
|
1272
|
-
...stripeOptions.stripeClient,
|
|
1273
|
-
webhooks: {
|
|
1274
|
-
constructEventAsync: vi.fn(),
|
|
1275
|
-
},
|
|
1276
|
-
};
|
|
1277
|
-
|
|
1278
|
-
const testOptions = {
|
|
1279
|
-
...stripeOptions,
|
|
1280
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
1281
|
-
stripeWebhookSecret: "test_secret",
|
|
1282
|
-
};
|
|
1283
|
-
|
|
1284
|
-
const {
|
|
1285
|
-
auth: testAuth,
|
|
1286
|
-
client,
|
|
1287
|
-
sessionSetter,
|
|
1288
|
-
} = await getTestInstance(
|
|
1289
|
-
{
|
|
1290
|
-
database: memory,
|
|
1291
|
-
plugins: [stripe(testOptions)],
|
|
1292
|
-
},
|
|
1293
|
-
{
|
|
1294
|
-
disableTestUser: true,
|
|
1295
|
-
clientOptions: {
|
|
1296
|
-
plugins: [stripeClient({ subscription: true })],
|
|
1297
|
-
},
|
|
1298
|
-
},
|
|
1299
|
-
);
|
|
1300
|
-
const testCtx = await testAuth.$context;
|
|
1301
|
-
|
|
1302
|
-
// Create user and sign in
|
|
1303
|
-
const userRes = await client.signUp.email(
|
|
1304
|
-
{
|
|
1305
|
-
...testUser,
|
|
1306
|
-
},
|
|
1307
|
-
{ throw: true },
|
|
1308
|
-
);
|
|
1309
|
-
const userId = userRes.user.id;
|
|
1310
|
-
|
|
1311
|
-
const headers = new Headers();
|
|
1312
|
-
await client.signIn.email(
|
|
1313
|
-
{
|
|
1314
|
-
...testUser,
|
|
1315
|
-
},
|
|
1316
|
-
{
|
|
1317
|
-
throw: true,
|
|
1318
|
-
onSuccess: sessionSetter(headers),
|
|
1319
|
-
},
|
|
1320
|
-
);
|
|
1321
|
-
|
|
1322
|
-
// User upgrades to paid plan - this creates an "incomplete" subscription
|
|
1323
|
-
await client.subscription.upgrade({
|
|
1324
|
-
plan: "starter",
|
|
1325
|
-
fetchOptions: { headers },
|
|
1326
|
-
});
|
|
1327
|
-
|
|
1328
|
-
// Verify the incomplete subscription was created
|
|
1329
|
-
const incompleteSubscription = await testCtx.adapter.findOne<Subscription>({
|
|
1330
|
-
model: "subscription",
|
|
1331
|
-
where: [{ field: "referenceId", value: userId }],
|
|
1332
|
-
});
|
|
1333
|
-
assert(
|
|
1334
|
-
incompleteSubscription,
|
|
1335
|
-
"Expected incomplete subscription to be created",
|
|
1336
|
-
);
|
|
1337
|
-
expect(incompleteSubscription.status).toBe("incomplete");
|
|
1338
|
-
expect(incompleteSubscription.stripeSubscriptionId).toBeUndefined();
|
|
1339
|
-
|
|
1340
|
-
// Get user with stripeCustomerId
|
|
1341
|
-
const user = await testCtx.adapter.findOne<any>({
|
|
1342
|
-
model: "user",
|
|
1343
|
-
where: [{ field: "id", value: userId }],
|
|
1344
|
-
});
|
|
1345
|
-
const stripeCustomerId = user?.stripeCustomerId;
|
|
1346
|
-
expect(stripeCustomerId).toBeDefined();
|
|
1347
|
-
|
|
1348
|
-
// Simulate `customer.subscription.created` webhook arriving
|
|
1349
|
-
const mockEvent = {
|
|
1350
|
-
type: "customer.subscription.created",
|
|
1351
|
-
data: {
|
|
1352
|
-
object: {
|
|
1353
|
-
id: "sub_new_from_checkout",
|
|
1354
|
-
customer: stripeCustomerId,
|
|
1355
|
-
status: "active",
|
|
1356
|
-
metadata: {
|
|
1357
|
-
subscriptionId: incompleteSubscription.id,
|
|
1358
|
-
},
|
|
1359
|
-
items: {
|
|
1360
|
-
data: [
|
|
1361
|
-
{
|
|
1362
|
-
price: { id: process.env.STRIPE_PRICE_ID_1 },
|
|
1363
|
-
quantity: 1,
|
|
1364
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
1365
|
-
current_period_end:
|
|
1366
|
-
Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
1367
|
-
},
|
|
1368
|
-
],
|
|
1369
|
-
},
|
|
1370
|
-
cancel_at_period_end: false,
|
|
1371
|
-
},
|
|
1372
|
-
},
|
|
1373
|
-
};
|
|
1374
|
-
|
|
1375
|
-
(stripeForTest.webhooks.constructEventAsync as any).mockResolvedValue(
|
|
1376
|
-
mockEvent,
|
|
1377
|
-
);
|
|
1378
|
-
|
|
1379
|
-
const mockRequest = new Request(
|
|
1380
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
1381
|
-
{
|
|
1382
|
-
method: "POST",
|
|
1383
|
-
headers: {
|
|
1384
|
-
"stripe-signature": "test_signature",
|
|
1385
|
-
},
|
|
1386
|
-
body: JSON.stringify(mockEvent),
|
|
1387
|
-
},
|
|
1388
|
-
);
|
|
1389
|
-
|
|
1390
|
-
const response = await testAuth.handler(mockRequest);
|
|
1391
|
-
expect(response.status).toBe(200);
|
|
1392
|
-
|
|
1393
|
-
// Verify that no duplicate subscription was created
|
|
1394
|
-
const allSubscriptions = await testCtx.adapter.findMany<Subscription>({
|
|
1395
|
-
model: "subscription",
|
|
1396
|
-
where: [{ field: "referenceId", value: userId }],
|
|
1397
|
-
});
|
|
1398
|
-
expect(allSubscriptions.length).toBe(1);
|
|
1399
|
-
});
|
|
1400
|
-
|
|
1401
|
-
it("should execute subscription event handlers", async () => {
|
|
1402
|
-
const { auth: testAuth } = await getTestInstance(
|
|
1403
|
-
{
|
|
1404
|
-
database: memory,
|
|
1405
|
-
plugins: [stripe(stripeOptions)],
|
|
1406
|
-
},
|
|
1407
|
-
{
|
|
1408
|
-
disableTestUser: true,
|
|
1409
|
-
},
|
|
1410
|
-
);
|
|
1411
|
-
const testCtx = await testAuth.$context;
|
|
1412
|
-
|
|
1413
|
-
const { id: userId } = await testCtx.adapter.create({
|
|
1414
|
-
model: "user",
|
|
1415
|
-
data: {
|
|
1416
|
-
email: "event-handler-test@email.com",
|
|
1417
|
-
},
|
|
1418
|
-
});
|
|
1419
|
-
|
|
1420
|
-
const onSubscriptionComplete = vi.fn();
|
|
1421
|
-
const onSubscriptionUpdate = vi.fn();
|
|
1422
|
-
const onSubscriptionCancel = vi.fn();
|
|
1423
|
-
const onSubscriptionDeleted = vi.fn();
|
|
1424
|
-
|
|
1425
|
-
const testOptions = {
|
|
1426
|
-
...stripeOptions,
|
|
1427
|
-
subscription: {
|
|
1428
|
-
...stripeOptions.subscription,
|
|
1429
|
-
onSubscriptionComplete,
|
|
1430
|
-
onSubscriptionUpdate,
|
|
1431
|
-
onSubscriptionCancel,
|
|
1432
|
-
onSubscriptionDeleted,
|
|
1433
|
-
},
|
|
1434
|
-
stripeWebhookSecret: "test_secret",
|
|
1435
|
-
} as unknown as StripeOptions;
|
|
1436
|
-
|
|
1437
|
-
// Test subscription complete handler
|
|
1438
|
-
const completeEvent = {
|
|
1439
|
-
type: "checkout.session.completed",
|
|
1440
|
-
data: {
|
|
1441
|
-
object: {
|
|
1442
|
-
mode: "subscription",
|
|
1443
|
-
subscription: "sub_123",
|
|
1444
|
-
metadata: {
|
|
1445
|
-
referenceId: "user_123",
|
|
1446
|
-
subscriptionId: "sub_123",
|
|
1447
|
-
},
|
|
1448
|
-
},
|
|
1449
|
-
},
|
|
1450
|
-
};
|
|
1451
|
-
|
|
1452
|
-
const mockSubscription = {
|
|
1453
|
-
status: "active",
|
|
1454
|
-
items: {
|
|
1455
|
-
data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
|
|
1456
|
-
},
|
|
1457
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
1458
|
-
current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
1459
|
-
};
|
|
1460
|
-
|
|
1461
|
-
const mockStripeForEvents = {
|
|
1462
|
-
...testOptions.stripeClient,
|
|
1463
|
-
subscriptions: {
|
|
1464
|
-
retrieve: vi.fn().mockResolvedValue(mockSubscription),
|
|
1465
|
-
},
|
|
1466
|
-
webhooks: {
|
|
1467
|
-
constructEventAsync: vi.fn().mockResolvedValue(completeEvent),
|
|
1468
|
-
},
|
|
1469
|
-
};
|
|
1470
|
-
|
|
1471
|
-
const eventTestOptions = {
|
|
1472
|
-
...testOptions,
|
|
1473
|
-
stripeClient: mockStripeForEvents as unknown as Stripe,
|
|
1474
|
-
};
|
|
1475
|
-
|
|
1476
|
-
const { auth: eventTestAuth } = await getTestInstance(
|
|
1477
|
-
{
|
|
1478
|
-
database: memory,
|
|
1479
|
-
plugins: [stripe(eventTestOptions)],
|
|
1480
|
-
},
|
|
1481
|
-
{
|
|
1482
|
-
disableTestUser: true,
|
|
1483
|
-
},
|
|
1484
|
-
);
|
|
1485
|
-
|
|
1486
|
-
const eventTestCtx = await eventTestAuth.$context;
|
|
1487
|
-
|
|
1488
|
-
const { id: testSubscriptionId } = await eventTestCtx.adapter.create({
|
|
1489
|
-
model: "subscription",
|
|
1490
|
-
data: {
|
|
1491
|
-
referenceId: userId,
|
|
1492
|
-
stripeCustomerId: "cus_123",
|
|
1493
|
-
stripeSubscriptionId: "sub_123",
|
|
1494
|
-
status: "incomplete",
|
|
1495
|
-
plan: "starter",
|
|
1496
|
-
},
|
|
1497
|
-
});
|
|
1498
|
-
|
|
1499
|
-
const webhookRequest = new Request(
|
|
1500
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
1501
|
-
{
|
|
1502
|
-
method: "POST",
|
|
1503
|
-
headers: {
|
|
1504
|
-
"stripe-signature": "test_signature",
|
|
1505
|
-
},
|
|
1506
|
-
body: JSON.stringify(completeEvent),
|
|
1507
|
-
},
|
|
1508
|
-
);
|
|
1509
|
-
|
|
1510
|
-
await eventTestAuth.handler(webhookRequest);
|
|
1511
|
-
|
|
1512
|
-
expect(onSubscriptionComplete).toHaveBeenCalledWith(
|
|
1513
|
-
expect.objectContaining({
|
|
1514
|
-
event: expect.any(Object),
|
|
1515
|
-
subscription: expect.any(Object),
|
|
1516
|
-
stripeSubscription: expect.any(Object),
|
|
1517
|
-
plan: expect.any(Object),
|
|
1518
|
-
}),
|
|
1519
|
-
expect.objectContaining({
|
|
1520
|
-
context: expect.any(Object),
|
|
1521
|
-
_flag: expect.any(String),
|
|
1522
|
-
}),
|
|
1523
|
-
);
|
|
1524
|
-
|
|
1525
|
-
const updateEvent = {
|
|
1526
|
-
type: "customer.subscription.updated",
|
|
1527
|
-
data: {
|
|
1528
|
-
object: {
|
|
1529
|
-
id: testSubscriptionId,
|
|
1530
|
-
customer: "cus_123",
|
|
1531
|
-
status: "active",
|
|
1532
|
-
items: {
|
|
1533
|
-
data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
|
|
1534
|
-
},
|
|
1535
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
1536
|
-
current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
1537
|
-
},
|
|
1538
|
-
},
|
|
1539
|
-
};
|
|
1540
|
-
|
|
1541
|
-
const updateRequest = new Request(
|
|
1542
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
1543
|
-
{
|
|
1544
|
-
method: "POST",
|
|
1545
|
-
headers: {
|
|
1546
|
-
"stripe-signature": "test_signature",
|
|
1547
|
-
},
|
|
1548
|
-
body: JSON.stringify(updateEvent),
|
|
1549
|
-
},
|
|
1550
|
-
);
|
|
1551
|
-
|
|
1552
|
-
mockStripeForEvents.webhooks.constructEventAsync.mockReturnValue(
|
|
1553
|
-
updateEvent,
|
|
1554
|
-
);
|
|
1555
|
-
await eventTestAuth.handler(updateRequest);
|
|
1556
|
-
expect(onSubscriptionUpdate).toHaveBeenCalledWith(
|
|
1557
|
-
expect.objectContaining({
|
|
1558
|
-
event: expect.any(Object),
|
|
1559
|
-
subscription: expect.any(Object),
|
|
1560
|
-
}),
|
|
1561
|
-
);
|
|
1562
|
-
|
|
1563
|
-
const userCancelEvent = {
|
|
1564
|
-
type: "customer.subscription.updated",
|
|
1565
|
-
data: {
|
|
1566
|
-
object: {
|
|
1567
|
-
id: testSubscriptionId,
|
|
1568
|
-
customer: "cus_123",
|
|
1569
|
-
status: "active",
|
|
1570
|
-
cancel_at_period_end: true,
|
|
1571
|
-
cancellation_details: {
|
|
1572
|
-
reason: "cancellation_requested",
|
|
1573
|
-
comment: "Customer canceled subscription",
|
|
1574
|
-
},
|
|
1575
|
-
items: {
|
|
1576
|
-
data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
|
|
1577
|
-
},
|
|
1578
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
1579
|
-
current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
1580
|
-
},
|
|
1581
|
-
},
|
|
1582
|
-
};
|
|
1583
|
-
|
|
1584
|
-
const userCancelRequest = new Request(
|
|
1585
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
1586
|
-
{
|
|
1587
|
-
method: "POST",
|
|
1588
|
-
headers: {
|
|
1589
|
-
"stripe-signature": "test_signature",
|
|
1590
|
-
},
|
|
1591
|
-
body: JSON.stringify(userCancelEvent),
|
|
1592
|
-
},
|
|
1593
|
-
);
|
|
1594
|
-
|
|
1595
|
-
mockStripeForEvents.webhooks.constructEventAsync.mockReturnValue(
|
|
1596
|
-
userCancelEvent,
|
|
1597
|
-
);
|
|
1598
|
-
await eventTestAuth.handler(userCancelRequest);
|
|
1599
|
-
const cancelEvent = {
|
|
1600
|
-
type: "customer.subscription.updated",
|
|
1601
|
-
data: {
|
|
1602
|
-
object: {
|
|
1603
|
-
id: testSubscriptionId,
|
|
1604
|
-
customer: "cus_123",
|
|
1605
|
-
status: "active",
|
|
1606
|
-
cancel_at_period_end: true,
|
|
1607
|
-
items: {
|
|
1608
|
-
data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
|
|
1609
|
-
},
|
|
1610
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
1611
|
-
current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
1612
|
-
},
|
|
1613
|
-
},
|
|
1614
|
-
};
|
|
1615
|
-
|
|
1616
|
-
const cancelRequest = new Request(
|
|
1617
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
1618
|
-
{
|
|
1619
|
-
method: "POST",
|
|
1620
|
-
headers: {
|
|
1621
|
-
"stripe-signature": "test_signature",
|
|
1622
|
-
},
|
|
1623
|
-
body: JSON.stringify(cancelEvent),
|
|
1624
|
-
},
|
|
1625
|
-
);
|
|
1626
|
-
|
|
1627
|
-
mockStripeForEvents.webhooks.constructEventAsync.mockReturnValue(
|
|
1628
|
-
cancelEvent,
|
|
1629
|
-
);
|
|
1630
|
-
await eventTestAuth.handler(cancelRequest);
|
|
1631
|
-
|
|
1632
|
-
expect(onSubscriptionCancel).toHaveBeenCalled();
|
|
1633
|
-
|
|
1634
|
-
const deleteEvent = {
|
|
1635
|
-
type: "customer.subscription.deleted",
|
|
1636
|
-
data: {
|
|
1637
|
-
object: {
|
|
1638
|
-
id: testSubscriptionId,
|
|
1639
|
-
customer: "cus_123",
|
|
1640
|
-
status: "canceled",
|
|
1641
|
-
metadata: {
|
|
1642
|
-
referenceId: userId,
|
|
1643
|
-
subscriptionId: testSubscriptionId,
|
|
1644
|
-
},
|
|
1645
|
-
},
|
|
1646
|
-
},
|
|
1647
|
-
};
|
|
1648
|
-
|
|
1649
|
-
const deleteRequest = new Request(
|
|
1650
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
1651
|
-
{
|
|
1652
|
-
method: "POST",
|
|
1653
|
-
headers: {
|
|
1654
|
-
"stripe-signature": "test_signature",
|
|
1655
|
-
},
|
|
1656
|
-
body: JSON.stringify(deleteEvent),
|
|
1657
|
-
},
|
|
1658
|
-
);
|
|
1659
|
-
|
|
1660
|
-
mockStripeForEvents.webhooks.constructEventAsync.mockReturnValue(
|
|
1661
|
-
deleteEvent,
|
|
1662
|
-
);
|
|
1663
|
-
await eventTestAuth.handler(deleteRequest);
|
|
1664
|
-
|
|
1665
|
-
expect(onSubscriptionDeleted).toHaveBeenCalled();
|
|
1666
|
-
});
|
|
1667
|
-
|
|
1668
|
-
it("should return updated subscription in onSubscriptionUpdate callback", async () => {
|
|
1669
|
-
const onSubscriptionUpdate = vi.fn();
|
|
1670
|
-
|
|
1671
|
-
// Simulate subscription update event (e.g., seat change from 1 to 5)
|
|
1672
|
-
const updateEvent = {
|
|
1673
|
-
type: "customer.subscription.updated",
|
|
1674
|
-
data: {
|
|
1675
|
-
object: {
|
|
1676
|
-
id: "sub_update_test",
|
|
1677
|
-
customer: "cus_update_test",
|
|
1678
|
-
status: "active",
|
|
1679
|
-
items: {
|
|
1680
|
-
data: [
|
|
1681
|
-
{
|
|
1682
|
-
price: { id: process.env.STRIPE_PRICE_ID_1 },
|
|
1683
|
-
quantity: 5, // Updated from 1 to 5
|
|
1684
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
1685
|
-
current_period_end:
|
|
1686
|
-
Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
1687
|
-
},
|
|
1688
|
-
],
|
|
1689
|
-
},
|
|
1690
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
1691
|
-
current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
1692
|
-
},
|
|
1693
|
-
},
|
|
1694
|
-
};
|
|
1695
|
-
|
|
1696
|
-
const stripeForTest = {
|
|
1697
|
-
...stripeOptions.stripeClient,
|
|
1698
|
-
webhooks: {
|
|
1699
|
-
constructEventAsync: vi.fn().mockResolvedValue(updateEvent),
|
|
1700
|
-
},
|
|
1701
|
-
};
|
|
1702
|
-
|
|
1703
|
-
const testOptions = {
|
|
1704
|
-
...stripeOptions,
|
|
1705
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
1706
|
-
stripeWebhookSecret: "test_secret",
|
|
1707
|
-
subscription: {
|
|
1708
|
-
...stripeOptions.subscription,
|
|
1709
|
-
onSubscriptionUpdate,
|
|
1710
|
-
},
|
|
1711
|
-
} as unknown as StripeOptions;
|
|
1712
|
-
|
|
1713
|
-
const { auth: testAuth } = await getTestInstance(
|
|
1714
|
-
{
|
|
1715
|
-
database: memory,
|
|
1716
|
-
plugins: [stripe(testOptions)],
|
|
1717
|
-
},
|
|
1718
|
-
{
|
|
1719
|
-
disableTestUser: true,
|
|
1720
|
-
},
|
|
1721
|
-
);
|
|
1722
|
-
|
|
1723
|
-
const ctx = await testAuth.$context;
|
|
1724
|
-
|
|
1725
|
-
const { id: testReferenceId } = await ctx.adapter.create({
|
|
1726
|
-
model: "user",
|
|
1727
|
-
data: {
|
|
1728
|
-
email: "update-callback@email.com",
|
|
1729
|
-
},
|
|
1730
|
-
});
|
|
1731
|
-
|
|
1732
|
-
const { id: testSubscriptionId } = await ctx.adapter.create({
|
|
1733
|
-
model: "subscription",
|
|
1734
|
-
data: {
|
|
1735
|
-
referenceId: testReferenceId,
|
|
1736
|
-
stripeCustomerId: "cus_update_test",
|
|
1737
|
-
stripeSubscriptionId: "sub_update_test",
|
|
1738
|
-
status: "active",
|
|
1739
|
-
plan: "starter",
|
|
1740
|
-
seats: 1,
|
|
1741
|
-
},
|
|
1742
|
-
});
|
|
1743
|
-
|
|
1744
|
-
const mockRequest = new Request(
|
|
1745
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
1746
|
-
{
|
|
1747
|
-
method: "POST",
|
|
1748
|
-
headers: {
|
|
1749
|
-
"stripe-signature": "test_signature",
|
|
1750
|
-
},
|
|
1751
|
-
body: JSON.stringify(updateEvent),
|
|
1752
|
-
},
|
|
1753
|
-
);
|
|
1754
|
-
|
|
1755
|
-
await testAuth.handler(mockRequest);
|
|
1756
|
-
|
|
1757
|
-
// Verify that onSubscriptionUpdate was called
|
|
1758
|
-
expect(onSubscriptionUpdate).toHaveBeenCalledTimes(1);
|
|
1759
|
-
|
|
1760
|
-
// Verify that the callback received the UPDATED subscription (seats: 5, not 1)
|
|
1761
|
-
const callbackArg = onSubscriptionUpdate.mock.calls[0]?.[0];
|
|
1762
|
-
expect(callbackArg).toBeDefined();
|
|
1763
|
-
expect(callbackArg.subscription).toMatchObject({
|
|
1764
|
-
id: testSubscriptionId,
|
|
1765
|
-
seats: 5, // Should be the NEW value, not the old value (1)
|
|
1766
|
-
status: "active",
|
|
1767
|
-
plan: "starter",
|
|
1768
|
-
});
|
|
1769
|
-
|
|
1770
|
-
// Also verify the subscription was actually updated in the database
|
|
1771
|
-
const updatedSub = await ctx.adapter.findOne<Subscription>({
|
|
1772
|
-
model: "subscription",
|
|
1773
|
-
where: [{ field: "id", value: testSubscriptionId }],
|
|
1774
|
-
});
|
|
1775
|
-
expect(updatedSub?.seats).toBe(5);
|
|
1776
|
-
});
|
|
1777
|
-
|
|
1778
|
-
it("should allow seat upgrades for the same plan", async () => {
|
|
1779
|
-
const { client, auth, sessionSetter } = await getTestInstance(
|
|
1780
|
-
{
|
|
1781
|
-
database: memory,
|
|
1782
|
-
plugins: [stripe(stripeOptions)],
|
|
1783
|
-
},
|
|
1784
|
-
{
|
|
1785
|
-
disableTestUser: true,
|
|
1786
|
-
clientOptions: {
|
|
1787
|
-
plugins: [stripeClient({ subscription: true })],
|
|
1788
|
-
},
|
|
1789
|
-
},
|
|
1790
|
-
);
|
|
1791
|
-
const ctx = await auth.$context;
|
|
1792
|
-
|
|
1793
|
-
const userRes = await client.signUp.email(
|
|
1794
|
-
{
|
|
1795
|
-
...testUser,
|
|
1796
|
-
email: "seat-upgrade@email.com",
|
|
1797
|
-
},
|
|
1798
|
-
{
|
|
1799
|
-
throw: true,
|
|
1800
|
-
},
|
|
1801
|
-
);
|
|
1802
|
-
|
|
1803
|
-
const headers = new Headers();
|
|
1804
|
-
await client.signIn.email(
|
|
1805
|
-
{
|
|
1806
|
-
...testUser,
|
|
1807
|
-
email: "seat-upgrade@email.com",
|
|
1808
|
-
},
|
|
1809
|
-
{
|
|
1810
|
-
throw: true,
|
|
1811
|
-
onSuccess: sessionSetter(headers),
|
|
1812
|
-
},
|
|
1813
|
-
);
|
|
1814
|
-
|
|
1815
|
-
await client.subscription.upgrade({
|
|
1816
|
-
plan: "starter",
|
|
1817
|
-
seats: 1,
|
|
1818
|
-
fetchOptions: {
|
|
1819
|
-
headers,
|
|
1820
|
-
},
|
|
1821
|
-
});
|
|
1822
|
-
|
|
1823
|
-
await ctx.adapter.update({
|
|
1824
|
-
model: "subscription",
|
|
1825
|
-
update: {
|
|
1826
|
-
status: "active",
|
|
1827
|
-
},
|
|
1828
|
-
where: [
|
|
1829
|
-
{
|
|
1830
|
-
field: "referenceId",
|
|
1831
|
-
value: userRes.user.id,
|
|
1832
|
-
},
|
|
1833
|
-
],
|
|
1834
|
-
});
|
|
1835
|
-
|
|
1836
|
-
const upgradeRes = await client.subscription.upgrade({
|
|
1837
|
-
plan: "starter",
|
|
1838
|
-
seats: 5,
|
|
1839
|
-
fetchOptions: {
|
|
1840
|
-
headers,
|
|
1841
|
-
},
|
|
1842
|
-
});
|
|
1843
|
-
|
|
1844
|
-
expect(upgradeRes.data?.url).toBeDefined();
|
|
1845
|
-
});
|
|
1846
|
-
|
|
1847
|
-
it("should prevent duplicate subscriptions with same plan and same seats", async () => {
|
|
1848
|
-
const { client, auth, sessionSetter } = await getTestInstance(
|
|
1849
|
-
{
|
|
1850
|
-
database: memory,
|
|
1851
|
-
plugins: [stripe(stripeOptions)],
|
|
1852
|
-
},
|
|
1853
|
-
{
|
|
1854
|
-
disableTestUser: true,
|
|
1855
|
-
clientOptions: {
|
|
1856
|
-
plugins: [stripeClient({ subscription: true })],
|
|
1857
|
-
},
|
|
1858
|
-
},
|
|
1859
|
-
);
|
|
1860
|
-
const ctx = await auth.$context;
|
|
1861
|
-
|
|
1862
|
-
const userRes = await client.signUp.email(
|
|
1863
|
-
{
|
|
1864
|
-
...testUser,
|
|
1865
|
-
email: "duplicate-prevention@email.com",
|
|
1866
|
-
},
|
|
1867
|
-
{
|
|
1868
|
-
throw: true,
|
|
1869
|
-
},
|
|
1870
|
-
);
|
|
1871
|
-
|
|
1872
|
-
const headers = new Headers();
|
|
1873
|
-
await client.signIn.email(
|
|
1874
|
-
{
|
|
1875
|
-
...testUser,
|
|
1876
|
-
email: "duplicate-prevention@email.com",
|
|
1877
|
-
},
|
|
1878
|
-
{
|
|
1879
|
-
throw: true,
|
|
1880
|
-
onSuccess: sessionSetter(headers),
|
|
1881
|
-
},
|
|
1882
|
-
);
|
|
1883
|
-
|
|
1884
|
-
await client.subscription.upgrade({
|
|
1885
|
-
plan: "starter",
|
|
1886
|
-
seats: 3,
|
|
1887
|
-
fetchOptions: {
|
|
1888
|
-
headers,
|
|
1889
|
-
},
|
|
1890
|
-
});
|
|
1891
|
-
|
|
1892
|
-
await ctx.adapter.update({
|
|
1893
|
-
model: "subscription",
|
|
1894
|
-
update: {
|
|
1895
|
-
status: "active",
|
|
1896
|
-
seats: 3,
|
|
1897
|
-
},
|
|
1898
|
-
where: [
|
|
1899
|
-
{
|
|
1900
|
-
field: "referenceId",
|
|
1901
|
-
value: userRes.user.id,
|
|
1902
|
-
},
|
|
1903
|
-
],
|
|
1904
|
-
});
|
|
1905
|
-
|
|
1906
|
-
const upgradeRes = await client.subscription.upgrade({
|
|
1907
|
-
plan: "starter",
|
|
1908
|
-
seats: 3,
|
|
1909
|
-
fetchOptions: {
|
|
1910
|
-
headers,
|
|
1911
|
-
},
|
|
1912
|
-
});
|
|
1913
|
-
|
|
1914
|
-
expect(upgradeRes.error).toBeDefined();
|
|
1915
|
-
expect(upgradeRes.error?.message).toContain("already subscribed");
|
|
1916
|
-
});
|
|
1917
|
-
|
|
1918
|
-
it("should only call Stripe customers.create once for signup and upgrade", async () => {
|
|
1919
|
-
const { client, sessionSetter } = await getTestInstance(
|
|
1920
|
-
{
|
|
1921
|
-
database: memory,
|
|
1922
|
-
plugins: [stripe(stripeOptions)],
|
|
1923
|
-
},
|
|
1924
|
-
{
|
|
1925
|
-
disableTestUser: true,
|
|
1926
|
-
clientOptions: {
|
|
1927
|
-
plugins: [stripeClient({ subscription: true })],
|
|
1928
|
-
},
|
|
1929
|
-
},
|
|
1930
|
-
);
|
|
1931
|
-
|
|
1932
|
-
await client.signUp.email(
|
|
1933
|
-
{ ...testUser, email: "single-create@email.com" },
|
|
1934
|
-
{ throw: true },
|
|
1935
|
-
);
|
|
1936
|
-
|
|
1937
|
-
const headers = new Headers();
|
|
1938
|
-
await client.signIn.email(
|
|
1939
|
-
{ ...testUser, email: "single-create@email.com" },
|
|
1940
|
-
{
|
|
1941
|
-
throw: true,
|
|
1942
|
-
onSuccess: sessionSetter(headers),
|
|
1943
|
-
},
|
|
1944
|
-
);
|
|
1945
|
-
|
|
1946
|
-
await client.subscription.upgrade({
|
|
1947
|
-
plan: "starter",
|
|
1948
|
-
fetchOptions: { headers },
|
|
1949
|
-
});
|
|
1950
|
-
|
|
1951
|
-
expect(mockStripe.customers.create).toHaveBeenCalledTimes(1);
|
|
1952
|
-
});
|
|
1953
|
-
|
|
1954
|
-
it("should create billing portal session", async () => {
|
|
1955
|
-
const { client, sessionSetter } = await getTestInstance(
|
|
1956
|
-
{
|
|
1957
|
-
database: memory,
|
|
1958
|
-
plugins: [stripe(stripeOptions)],
|
|
1959
|
-
},
|
|
1960
|
-
{
|
|
1961
|
-
disableTestUser: true,
|
|
1962
|
-
clientOptions: {
|
|
1963
|
-
plugins: [stripeClient({ subscription: true })],
|
|
1964
|
-
},
|
|
1965
|
-
},
|
|
1966
|
-
);
|
|
1967
|
-
|
|
1968
|
-
await client.signUp.email(
|
|
1969
|
-
{
|
|
1970
|
-
...testUser,
|
|
1971
|
-
email: "billing-portal@email.com",
|
|
1972
|
-
},
|
|
1973
|
-
{
|
|
1974
|
-
throw: true,
|
|
1975
|
-
},
|
|
1976
|
-
);
|
|
1977
|
-
|
|
1978
|
-
const headers = new Headers();
|
|
1979
|
-
await client.signIn.email(
|
|
1980
|
-
{
|
|
1981
|
-
...testUser,
|
|
1982
|
-
email: "billing-portal@email.com",
|
|
1983
|
-
},
|
|
1984
|
-
{
|
|
1985
|
-
throw: true,
|
|
1986
|
-
onSuccess: sessionSetter(headers),
|
|
1987
|
-
},
|
|
1988
|
-
);
|
|
1989
|
-
const billingPortalRes = await client.subscription.billingPortal({
|
|
1990
|
-
returnUrl: "/dashboard",
|
|
1991
|
-
fetchOptions: {
|
|
1992
|
-
headers,
|
|
1993
|
-
},
|
|
1994
|
-
});
|
|
1995
|
-
expect(billingPortalRes.data?.url).toBe("https://billing.stripe.com/mock");
|
|
1996
|
-
expect(billingPortalRes.data?.redirect).toBe(true);
|
|
1997
|
-
expect(mockStripe.billingPortal.sessions.create).toHaveBeenCalledWith({
|
|
1998
|
-
customer: expect.any(String),
|
|
1999
|
-
return_url: "http://localhost:3000/dashboard",
|
|
2000
|
-
});
|
|
2001
|
-
});
|
|
2002
|
-
|
|
2003
|
-
it("should not update personal subscription when upgrading with an org referenceId", async () => {
|
|
2004
|
-
/* cspell:disable-next-line */
|
|
2005
|
-
const orgId = "org_b67GF32Cljh7u588AuEblmLVobclDRcP";
|
|
2006
|
-
|
|
2007
|
-
const testOptions = {
|
|
2008
|
-
...stripeOptions,
|
|
2009
|
-
stripeClient: _stripe,
|
|
2010
|
-
subscription: {
|
|
2011
|
-
...stripeOptions.subscription,
|
|
2012
|
-
authorizeReference: async () => true,
|
|
2013
|
-
},
|
|
2014
|
-
} as unknown as StripeOptions;
|
|
2015
|
-
|
|
2016
|
-
const {
|
|
2017
|
-
auth: testAuth,
|
|
2018
|
-
client: testClient,
|
|
2019
|
-
sessionSetter: testSessionSetter,
|
|
2020
|
-
} = await getTestInstance(
|
|
2021
|
-
{
|
|
2022
|
-
database: memory,
|
|
2023
|
-
plugins: [stripe(testOptions)],
|
|
2024
|
-
},
|
|
2025
|
-
{
|
|
2026
|
-
disableTestUser: true,
|
|
2027
|
-
clientOptions: {
|
|
2028
|
-
plugins: [stripeClient({ subscription: true })],
|
|
2029
|
-
},
|
|
2030
|
-
},
|
|
2031
|
-
);
|
|
2032
|
-
const testCtx = await testAuth.$context;
|
|
2033
|
-
|
|
2034
|
-
// Sign up and sign in the user
|
|
2035
|
-
const userRes = await testClient.signUp.email(
|
|
2036
|
-
{ ...testUser, email: "org-ref@email.com" },
|
|
2037
|
-
{ throw: true },
|
|
2038
|
-
);
|
|
2039
|
-
const headers = new Headers();
|
|
2040
|
-
await testClient.signIn.email(
|
|
2041
|
-
{ ...testUser, email: "org-ref@email.com" },
|
|
2042
|
-
{ throw: true, onSuccess: testSessionSetter(headers) },
|
|
2043
|
-
);
|
|
2044
|
-
|
|
2045
|
-
// Create a personal subscription (referenceId = user id)
|
|
2046
|
-
await testClient.subscription.upgrade({
|
|
2047
|
-
plan: "starter",
|
|
2048
|
-
fetchOptions: { headers },
|
|
2049
|
-
});
|
|
2050
|
-
|
|
2051
|
-
const personalSub = await testCtx.adapter.findOne<Subscription>({
|
|
2052
|
-
model: "subscription",
|
|
2053
|
-
where: [{ field: "referenceId", value: userRes.user.id }],
|
|
2054
|
-
});
|
|
2055
|
-
expect(personalSub).toBeTruthy();
|
|
2056
|
-
|
|
2057
|
-
await testCtx.adapter.update({
|
|
2058
|
-
model: "subscription",
|
|
2059
|
-
update: {
|
|
2060
|
-
status: "active",
|
|
2061
|
-
stripeSubscriptionId: "sub_personal_active_123",
|
|
2062
|
-
},
|
|
2063
|
-
where: [{ field: "id", value: personalSub!.id }],
|
|
2064
|
-
});
|
|
2065
|
-
|
|
2066
|
-
mockStripe.subscriptions.list.mockResolvedValue({
|
|
2067
|
-
data: [
|
|
2068
|
-
{
|
|
2069
|
-
id: "sub_personal_active_123",
|
|
2070
|
-
status: "active",
|
|
2071
|
-
items: {
|
|
2072
|
-
data: [
|
|
2073
|
-
{
|
|
2074
|
-
id: "si_1",
|
|
2075
|
-
price: { id: process.env.STRIPE_PRICE_ID_1 },
|
|
2076
|
-
quantity: 1,
|
|
2077
|
-
},
|
|
2078
|
-
],
|
|
2079
|
-
},
|
|
2080
|
-
},
|
|
2081
|
-
],
|
|
2082
|
-
});
|
|
2083
|
-
|
|
2084
|
-
// Attempt to upgrade using an org referenceId
|
|
2085
|
-
const upgradeRes = await testClient.subscription.upgrade({
|
|
2086
|
-
plan: "starter",
|
|
2087
|
-
referenceId: orgId,
|
|
2088
|
-
fetchOptions: { headers },
|
|
2089
|
-
});
|
|
2090
|
-
// It should NOT go through billing portal (which would update the personal sub)
|
|
2091
|
-
expect(mockStripe.billingPortal.sessions.create).not.toHaveBeenCalled();
|
|
2092
|
-
expect(upgradeRes.data?.url).toBeDefined();
|
|
2093
|
-
|
|
2094
|
-
const orgSub = await testCtx.adapter.findOne<Subscription>({
|
|
2095
|
-
model: "subscription",
|
|
2096
|
-
where: [{ field: "referenceId", value: orgId }],
|
|
2097
|
-
});
|
|
2098
|
-
expect(orgSub).toMatchObject({
|
|
2099
|
-
referenceId: orgId,
|
|
2100
|
-
status: "incomplete",
|
|
2101
|
-
plan: "starter",
|
|
2102
|
-
});
|
|
2103
|
-
|
|
2104
|
-
const personalAfter = await testCtx.adapter.findOne<Subscription>({
|
|
2105
|
-
model: "subscription",
|
|
2106
|
-
where: [{ field: "id", value: personalSub!.id }],
|
|
2107
|
-
});
|
|
2108
|
-
expect(personalAfter?.status).toBe("active");
|
|
2109
|
-
});
|
|
2110
|
-
|
|
2111
|
-
it("should prevent multiple free trials for the same user", async () => {
|
|
2112
|
-
const { client, auth, sessionSetter } = await getTestInstance(
|
|
2113
|
-
{
|
|
2114
|
-
database: memory,
|
|
2115
|
-
plugins: [stripe(stripeOptions)],
|
|
2116
|
-
},
|
|
2117
|
-
{
|
|
2118
|
-
disableTestUser: true,
|
|
2119
|
-
clientOptions: {
|
|
2120
|
-
plugins: [stripeClient({ subscription: true })],
|
|
2121
|
-
},
|
|
2122
|
-
},
|
|
2123
|
-
);
|
|
2124
|
-
const ctx = await auth.$context;
|
|
2125
|
-
|
|
2126
|
-
// Create a user
|
|
2127
|
-
const userRes = await client.signUp.email(
|
|
2128
|
-
{ ...testUser, email: "trial-prevention@email.com" },
|
|
2129
|
-
{ throw: true },
|
|
2130
|
-
);
|
|
2131
|
-
|
|
2132
|
-
const headers = new Headers();
|
|
2133
|
-
await client.signIn.email(
|
|
2134
|
-
{ ...testUser, email: "trial-prevention@email.com" },
|
|
2135
|
-
{
|
|
2136
|
-
throw: true,
|
|
2137
|
-
onSuccess: sessionSetter(headers),
|
|
2138
|
-
},
|
|
2139
|
-
);
|
|
2140
|
-
|
|
2141
|
-
// First subscription with trial
|
|
2142
|
-
const firstUpgradeRes = await client.subscription.upgrade({
|
|
2143
|
-
plan: "starter",
|
|
2144
|
-
fetchOptions: { headers },
|
|
2145
|
-
});
|
|
2146
|
-
|
|
2147
|
-
expect(firstUpgradeRes.data?.url).toBeDefined();
|
|
2148
|
-
|
|
2149
|
-
// Simulate the subscription being created with trial data
|
|
2150
|
-
await ctx.adapter.update({
|
|
2151
|
-
model: "subscription",
|
|
2152
|
-
update: {
|
|
2153
|
-
status: "trialing",
|
|
2154
|
-
trialStart: new Date(),
|
|
2155
|
-
trialEnd: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days from now
|
|
2156
|
-
},
|
|
2157
|
-
where: [
|
|
2158
|
-
{
|
|
2159
|
-
field: "referenceId",
|
|
2160
|
-
value: userRes.user.id,
|
|
2161
|
-
},
|
|
2162
|
-
],
|
|
2163
|
-
});
|
|
2164
|
-
|
|
2165
|
-
// Cancel the subscription
|
|
2166
|
-
await ctx.adapter.update({
|
|
2167
|
-
model: "subscription",
|
|
2168
|
-
update: {
|
|
2169
|
-
status: "canceled",
|
|
2170
|
-
},
|
|
2171
|
-
where: [
|
|
2172
|
-
{
|
|
2173
|
-
field: "referenceId",
|
|
2174
|
-
value: userRes.user.id,
|
|
2175
|
-
},
|
|
2176
|
-
],
|
|
2177
|
-
});
|
|
2178
|
-
|
|
2179
|
-
// Try to subscribe again - should NOT get a trial
|
|
2180
|
-
const secondUpgradeRes = await client.subscription.upgrade({
|
|
2181
|
-
plan: "starter",
|
|
2182
|
-
fetchOptions: { headers },
|
|
2183
|
-
});
|
|
2184
|
-
|
|
2185
|
-
expect(secondUpgradeRes.data?.url).toBeDefined();
|
|
2186
|
-
|
|
2187
|
-
// Verify that the checkout session was created without trial_period_days
|
|
2188
|
-
// We can't directly test the Stripe session, but we can verify the logic
|
|
2189
|
-
// by checking that the user has trial history
|
|
2190
|
-
const subscriptions = (await ctx.adapter.findMany({
|
|
2191
|
-
model: "subscription",
|
|
2192
|
-
where: [
|
|
2193
|
-
{
|
|
2194
|
-
field: "referenceId",
|
|
2195
|
-
value: userRes.user.id,
|
|
2196
|
-
},
|
|
2197
|
-
],
|
|
2198
|
-
})) as Subscription[];
|
|
2199
|
-
|
|
2200
|
-
// Should have 2 subscriptions (first canceled, second new)
|
|
2201
|
-
expect(subscriptions).toHaveLength(2);
|
|
2202
|
-
|
|
2203
|
-
// At least one should have trial data
|
|
2204
|
-
const hasTrialData = subscriptions.some(
|
|
2205
|
-
(s: Subscription) => s.trialStart || s.trialEnd,
|
|
2206
|
-
);
|
|
2207
|
-
expect(hasTrialData).toBe(true);
|
|
2208
|
-
});
|
|
2209
|
-
|
|
2210
|
-
it("should prevent trial abuse when processing incomplete subscription with past trial history", async () => {
|
|
2211
|
-
const { client, auth, sessionSetter } = await getTestInstance(
|
|
2212
|
-
{
|
|
2213
|
-
database: memory,
|
|
2214
|
-
plugins: [
|
|
2215
|
-
stripe({
|
|
2216
|
-
...stripeOptions,
|
|
2217
|
-
subscription: {
|
|
2218
|
-
...stripeOptions.subscription,
|
|
2219
|
-
plans: stripeOptions.subscription.plans.map((plan) => ({
|
|
2220
|
-
...plan,
|
|
2221
|
-
freeTrial: { days: 7 },
|
|
2222
|
-
})),
|
|
2223
|
-
},
|
|
2224
|
-
}),
|
|
2225
|
-
],
|
|
2226
|
-
},
|
|
2227
|
-
{
|
|
2228
|
-
disableTestUser: true,
|
|
2229
|
-
clientOptions: {
|
|
2230
|
-
plugins: [stripeClient({ subscription: true })],
|
|
2231
|
-
},
|
|
2232
|
-
},
|
|
2233
|
-
);
|
|
2234
|
-
const ctx = await auth.$context;
|
|
2235
|
-
|
|
2236
|
-
const userRes = await client.signUp.email(
|
|
2237
|
-
{ ...testUser, email: "trial-findone-test@email.com" },
|
|
2238
|
-
{ throw: true },
|
|
2239
|
-
);
|
|
2240
|
-
|
|
2241
|
-
const headers = new Headers();
|
|
2242
|
-
await client.signIn.email(
|
|
2243
|
-
{ ...testUser, email: "trial-findone-test@email.com" },
|
|
2244
|
-
{ throw: true, onSuccess: sessionSetter(headers) },
|
|
2245
|
-
);
|
|
2246
|
-
|
|
2247
|
-
// Create a canceled subscription with trial history first
|
|
2248
|
-
await ctx.adapter.create({
|
|
2249
|
-
model: "subscription",
|
|
2250
|
-
data: {
|
|
2251
|
-
referenceId: userRes.user.id,
|
|
2252
|
-
stripeCustomerId: "cus_old_customer",
|
|
2253
|
-
status: "canceled",
|
|
2254
|
-
plan: "starter",
|
|
2255
|
-
stripeSubscriptionId: "sub_canceled_with_trial",
|
|
2256
|
-
trialStart: new Date(Date.now() - 1000000),
|
|
2257
|
-
trialEnd: new Date(Date.now() - 500000),
|
|
2258
|
-
},
|
|
2259
|
-
});
|
|
2260
|
-
|
|
2261
|
-
// Create an new incomplete subscription (without trial info)
|
|
2262
|
-
const incompleteSubId = "sub_incomplete_new";
|
|
2263
|
-
await ctx.adapter.create({
|
|
2264
|
-
model: "subscription",
|
|
2265
|
-
data: {
|
|
2266
|
-
referenceId: userRes.user.id,
|
|
2267
|
-
stripeCustomerId: "cus_old_customer",
|
|
2268
|
-
status: "incomplete",
|
|
2269
|
-
plan: "premium",
|
|
2270
|
-
stripeSubscriptionId: incompleteSubId,
|
|
2271
|
-
},
|
|
2272
|
-
});
|
|
2273
|
-
|
|
2274
|
-
// When upgrading with a specific subscriptionId pointing to the incomplete one,
|
|
2275
|
-
// the system should still check ALL subscriptions for trial history
|
|
2276
|
-
const upgradeRes = await client.subscription.upgrade({
|
|
2277
|
-
plan: "premium",
|
|
2278
|
-
subscriptionId: incompleteSubId,
|
|
2279
|
-
fetchOptions: { headers },
|
|
2280
|
-
});
|
|
2281
|
-
|
|
2282
|
-
expect(upgradeRes.data?.url).toBeDefined();
|
|
2283
|
-
|
|
2284
|
-
// Verify that NO trial was granted despite processing the incomplete subscription
|
|
2285
|
-
const callArgs = mockStripe.checkout.sessions.create.mock.lastCall?.[0];
|
|
2286
|
-
expect(callArgs?.subscription_data?.trial_period_days).toBeUndefined();
|
|
2287
|
-
});
|
|
2288
|
-
|
|
2289
|
-
it("should upgrade existing subscription instead of creating new one", async () => {
|
|
2290
|
-
// Reset mocks for this test
|
|
2291
|
-
vi.clearAllMocks();
|
|
2292
|
-
|
|
2293
|
-
const { client, auth, sessionSetter } = await getTestInstance(
|
|
2294
|
-
{
|
|
2295
|
-
database: memory,
|
|
2296
|
-
plugins: [stripe(stripeOptions)],
|
|
2297
|
-
},
|
|
2298
|
-
{
|
|
2299
|
-
disableTestUser: true,
|
|
2300
|
-
clientOptions: {
|
|
2301
|
-
plugins: [stripeClient({ subscription: true })],
|
|
2302
|
-
},
|
|
2303
|
-
},
|
|
2304
|
-
);
|
|
2305
|
-
const ctx = await auth.$context;
|
|
2306
|
-
|
|
2307
|
-
// Create a user
|
|
2308
|
-
const userRes = await client.signUp.email(
|
|
2309
|
-
{ ...testUser, email: "upgrade-existing@email.com" },
|
|
2310
|
-
{ throw: true },
|
|
2311
|
-
);
|
|
2312
|
-
|
|
2313
|
-
const headers = new Headers();
|
|
2314
|
-
await client.signIn.email(
|
|
2315
|
-
{ ...testUser, email: "upgrade-existing@email.com" },
|
|
2316
|
-
{
|
|
2317
|
-
throw: true,
|
|
2318
|
-
onSuccess: sessionSetter(headers),
|
|
2319
|
-
},
|
|
2320
|
-
);
|
|
2321
|
-
|
|
2322
|
-
// Mock customers.search to find existing user customer
|
|
2323
|
-
mockStripe.customers.search.mockResolvedValueOnce({
|
|
2324
|
-
data: [{ id: "cus_test_123" }],
|
|
2325
|
-
});
|
|
2326
|
-
|
|
2327
|
-
// First create a starter subscription
|
|
2328
|
-
await client.subscription.upgrade({
|
|
2329
|
-
plan: "starter",
|
|
2330
|
-
fetchOptions: { headers },
|
|
2331
|
-
});
|
|
2332
|
-
|
|
2333
|
-
// Simulate the subscription being active
|
|
2334
|
-
const starterSub = await ctx.adapter.findOne<Subscription>({
|
|
2335
|
-
model: "subscription",
|
|
2336
|
-
where: [
|
|
2337
|
-
{
|
|
2338
|
-
field: "referenceId",
|
|
2339
|
-
value: userRes.user.id,
|
|
2340
|
-
},
|
|
2341
|
-
],
|
|
2342
|
-
});
|
|
2343
|
-
|
|
2344
|
-
await ctx.adapter.update({
|
|
2345
|
-
model: "subscription",
|
|
2346
|
-
update: {
|
|
2347
|
-
status: "active",
|
|
2348
|
-
stripeSubscriptionId: "sub_active_test_123",
|
|
2349
|
-
stripeCustomerId: "cus_mock123", // Use the same customer ID as the mock
|
|
2350
|
-
},
|
|
2351
|
-
where: [
|
|
2352
|
-
{
|
|
2353
|
-
field: "id",
|
|
2354
|
-
value: starterSub!.id,
|
|
2355
|
-
},
|
|
2356
|
-
],
|
|
2357
|
-
});
|
|
2358
|
-
|
|
2359
|
-
// Also update the user with the Stripe customer ID
|
|
2360
|
-
await ctx.adapter.update({
|
|
2361
|
-
model: "user",
|
|
2362
|
-
update: {
|
|
2363
|
-
stripeCustomerId: "cus_mock123",
|
|
2364
|
-
},
|
|
2365
|
-
where: [
|
|
2366
|
-
{
|
|
2367
|
-
field: "id",
|
|
2368
|
-
value: userRes.user.id,
|
|
2369
|
-
},
|
|
2370
|
-
],
|
|
2371
|
-
});
|
|
2372
|
-
|
|
2373
|
-
// Mock Stripe subscriptions.list to return the active subscription
|
|
2374
|
-
mockStripe.subscriptions.list.mockResolvedValueOnce({
|
|
2375
|
-
data: [
|
|
2376
|
-
{
|
|
2377
|
-
id: "sub_active_test_123",
|
|
2378
|
-
status: "active",
|
|
2379
|
-
items: {
|
|
2380
|
-
data: [
|
|
2381
|
-
{
|
|
2382
|
-
id: "si_test_123",
|
|
2383
|
-
price: { id: process.env.STRIPE_PRICE_ID_1 },
|
|
2384
|
-
quantity: 1,
|
|
2385
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
2386
|
-
current_period_end:
|
|
2387
|
-
Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
2388
|
-
},
|
|
2389
|
-
],
|
|
2390
|
-
},
|
|
2391
|
-
},
|
|
2392
|
-
],
|
|
2393
|
-
});
|
|
2394
|
-
|
|
2395
|
-
// Clear mock calls before the upgrade
|
|
2396
|
-
mockStripe.checkout.sessions.create.mockClear();
|
|
2397
|
-
mockStripe.billingPortal.sessions.create.mockClear();
|
|
2398
|
-
|
|
2399
|
-
// Now upgrade to premium plan - should use billing portal to update existing subscription
|
|
2400
|
-
const upgradeRes = await client.subscription.upgrade({
|
|
2401
|
-
plan: "premium",
|
|
2402
|
-
fetchOptions: { headers },
|
|
2403
|
-
});
|
|
2404
|
-
|
|
2405
|
-
// Verify that billing portal was called (indicating update, not new subscription)
|
|
2406
|
-
expect(mockStripe.billingPortal.sessions.create).toHaveBeenCalledWith(
|
|
2407
|
-
expect.objectContaining({
|
|
2408
|
-
customer: "cus_mock123",
|
|
2409
|
-
flow_data: expect.objectContaining({
|
|
2410
|
-
type: "subscription_update_confirm",
|
|
2411
|
-
subscription_update_confirm: expect.objectContaining({
|
|
2412
|
-
subscription: "sub_active_test_123",
|
|
2413
|
-
}),
|
|
2414
|
-
}),
|
|
2415
|
-
}),
|
|
2416
|
-
);
|
|
2417
|
-
|
|
2418
|
-
// Should not create a new checkout session
|
|
2419
|
-
expect(mockStripe.checkout.sessions.create).not.toHaveBeenCalled();
|
|
2420
|
-
|
|
2421
|
-
// Verify the response has a redirect URL
|
|
2422
|
-
expect(upgradeRes.data?.url).toBe("https://billing.stripe.com/mock");
|
|
2423
|
-
expect(upgradeRes.data?.redirect).toBe(true);
|
|
2424
|
-
|
|
2425
|
-
// Verify no new subscription was created in the database
|
|
2426
|
-
const allSubs = await ctx.adapter.findMany<Subscription>({
|
|
2427
|
-
model: "subscription",
|
|
2428
|
-
where: [
|
|
2429
|
-
{
|
|
2430
|
-
field: "referenceId",
|
|
2431
|
-
value: userRes.user.id,
|
|
2432
|
-
},
|
|
2433
|
-
],
|
|
2434
|
-
});
|
|
2435
|
-
expect(allSubs).toHaveLength(1); // Should still have only one subscription
|
|
2436
|
-
});
|
|
2437
|
-
|
|
2438
|
-
it("should prevent multiple free trials across different plans", async () => {
|
|
2439
|
-
const { client, auth, sessionSetter } = await getTestInstance(
|
|
2440
|
-
{
|
|
2441
|
-
database: memory,
|
|
2442
|
-
plugins: [stripe(stripeOptions)],
|
|
2443
|
-
},
|
|
2444
|
-
{
|
|
2445
|
-
disableTestUser: true,
|
|
2446
|
-
clientOptions: {
|
|
2447
|
-
plugins: [stripeClient({ subscription: true })],
|
|
2448
|
-
},
|
|
2449
|
-
},
|
|
2450
|
-
);
|
|
2451
|
-
const ctx = await auth.$context;
|
|
2452
|
-
|
|
2453
|
-
// Create a user
|
|
2454
|
-
const userRes = await client.signUp.email(
|
|
2455
|
-
{ ...testUser, email: "cross-plan-trial@email.com" },
|
|
2456
|
-
{ throw: true },
|
|
2457
|
-
);
|
|
2458
|
-
|
|
2459
|
-
const headers = new Headers();
|
|
2460
|
-
await client.signIn.email(
|
|
2461
|
-
{ ...testUser, email: "cross-plan-trial@email.com" },
|
|
2462
|
-
{
|
|
2463
|
-
throw: true,
|
|
2464
|
-
onSuccess: sessionSetter(headers),
|
|
2465
|
-
},
|
|
2466
|
-
);
|
|
2467
|
-
|
|
2468
|
-
// First subscription with trial on starter plan
|
|
2469
|
-
const firstUpgradeRes = await client.subscription.upgrade({
|
|
2470
|
-
plan: "starter",
|
|
2471
|
-
fetchOptions: { headers },
|
|
2472
|
-
});
|
|
2473
|
-
|
|
2474
|
-
expect(firstUpgradeRes.data?.url).toBeDefined();
|
|
2475
|
-
|
|
2476
|
-
// Simulate the subscription being created with trial data
|
|
2477
|
-
await ctx.adapter.update({
|
|
2478
|
-
model: "subscription",
|
|
2479
|
-
update: {
|
|
2480
|
-
status: "trialing",
|
|
2481
|
-
trialStart: new Date(),
|
|
2482
|
-
trialEnd: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
2483
|
-
},
|
|
2484
|
-
where: [
|
|
2485
|
-
{
|
|
2486
|
-
field: "referenceId",
|
|
2487
|
-
value: userRes.user.id,
|
|
2488
|
-
},
|
|
2489
|
-
],
|
|
2490
|
-
});
|
|
2491
|
-
|
|
2492
|
-
// Cancel the subscription
|
|
2493
|
-
await ctx.adapter.update({
|
|
2494
|
-
model: "subscription",
|
|
2495
|
-
update: {
|
|
2496
|
-
status: "canceled",
|
|
2497
|
-
},
|
|
2498
|
-
where: [
|
|
2499
|
-
{
|
|
2500
|
-
field: "referenceId",
|
|
2501
|
-
value: userRes.user.id,
|
|
2502
|
-
},
|
|
2503
|
-
],
|
|
2504
|
-
});
|
|
2505
|
-
|
|
2506
|
-
// Try to subscribe to a different plan - should NOT get a trial
|
|
2507
|
-
const secondUpgradeRes = await client.subscription.upgrade({
|
|
2508
|
-
plan: "premium",
|
|
2509
|
-
fetchOptions: { headers },
|
|
2510
|
-
});
|
|
2511
|
-
|
|
2512
|
-
expect(secondUpgradeRes.data?.url).toBeDefined();
|
|
2513
|
-
|
|
2514
|
-
// Verify that the user has trial history from the first plan
|
|
2515
|
-
const subscriptions = (await ctx.adapter.findMany({
|
|
2516
|
-
model: "subscription",
|
|
2517
|
-
where: [
|
|
2518
|
-
{
|
|
2519
|
-
field: "referenceId",
|
|
2520
|
-
value: userRes.user.id,
|
|
2521
|
-
},
|
|
2522
|
-
],
|
|
2523
|
-
})) as Subscription[];
|
|
2524
|
-
|
|
2525
|
-
// Should have at least 1 subscription (the starter with trial data)
|
|
2526
|
-
expect(subscriptions.length).toBeGreaterThanOrEqual(1);
|
|
2527
|
-
|
|
2528
|
-
// The starter subscription should have trial data
|
|
2529
|
-
const starterSub = subscriptions.find(
|
|
2530
|
-
(s: Subscription) => s.plan === "starter",
|
|
2531
|
-
) as Subscription | undefined;
|
|
2532
|
-
expect(starterSub?.trialStart).toBeDefined();
|
|
2533
|
-
expect(starterSub?.trialEnd).toBeDefined();
|
|
2534
|
-
|
|
2535
|
-
// Verify that the trial eligibility logic is working by checking
|
|
2536
|
-
// that the user has ever had a trial (which should prevent future trials)
|
|
2537
|
-
const hasEverTrialed = subscriptions.some((s: Subscription) => {
|
|
2538
|
-
const hadTrial =
|
|
2539
|
-
!!(s.trialStart || s.trialEnd) || s.status === "trialing";
|
|
2540
|
-
return hadTrial;
|
|
2541
|
-
});
|
|
2542
|
-
expect(hasEverTrialed).toBe(true);
|
|
2543
|
-
});
|
|
2544
|
-
|
|
2545
|
-
it("should update stripe customer email when user email changes", async () => {
|
|
2546
|
-
const { client, auth } = await getTestInstance(
|
|
2547
|
-
{
|
|
2548
|
-
database: memory,
|
|
2549
|
-
plugins: [stripe(stripeOptions)],
|
|
2550
|
-
},
|
|
2551
|
-
{
|
|
2552
|
-
disableTestUser: true,
|
|
2553
|
-
clientOptions: {
|
|
2554
|
-
plugins: [stripeClient({ subscription: true })],
|
|
2555
|
-
},
|
|
2556
|
-
},
|
|
2557
|
-
);
|
|
2558
|
-
const ctx = await auth.$context;
|
|
2559
|
-
|
|
2560
|
-
// Setup mock for customer retrieve and update
|
|
2561
|
-
mockStripe.customers.retrieve = vi.fn().mockResolvedValue({
|
|
2562
|
-
id: "cus_mock123",
|
|
2563
|
-
email: "test@email.com",
|
|
2564
|
-
deleted: false,
|
|
2565
|
-
});
|
|
2566
|
-
mockStripe.customers.update = vi.fn().mockResolvedValue({
|
|
2567
|
-
id: "cus_mock123",
|
|
2568
|
-
email: "newemail@example.com",
|
|
2569
|
-
});
|
|
2570
|
-
|
|
2571
|
-
// Sign up a user
|
|
2572
|
-
const userRes = await client.signUp.email(testUser, {
|
|
2573
|
-
throw: true,
|
|
2574
|
-
});
|
|
2575
|
-
|
|
2576
|
-
expect(userRes.user).toBeDefined();
|
|
2577
|
-
|
|
2578
|
-
// Verify customer was created during signup
|
|
2579
|
-
expect(mockStripe.customers.create).toHaveBeenCalledWith({
|
|
2580
|
-
email: testUser.email,
|
|
2581
|
-
name: testUser.name,
|
|
2582
|
-
metadata: {
|
|
2583
|
-
customerType: "user",
|
|
2584
|
-
userId: userRes.user.id,
|
|
2585
|
-
},
|
|
2586
|
-
});
|
|
2587
|
-
|
|
2588
|
-
// Clear mocks to track the update
|
|
2589
|
-
vi.clearAllMocks();
|
|
2590
|
-
|
|
2591
|
-
// Re-setup the retrieve mock for the update flow
|
|
2592
|
-
mockStripe.customers.retrieve = vi.fn().mockResolvedValue({
|
|
2593
|
-
id: "cus_mock123",
|
|
2594
|
-
email: "test@email.com",
|
|
2595
|
-
deleted: false,
|
|
2596
|
-
});
|
|
2597
|
-
mockStripe.customers.update = vi.fn().mockResolvedValue({
|
|
2598
|
-
id: "cus_mock123",
|
|
2599
|
-
email: "newemail@example.com",
|
|
2600
|
-
});
|
|
2601
|
-
|
|
2602
|
-
// Update the user's email using internal adapter (which triggers hooks)
|
|
2603
|
-
await runWithEndpointContext(
|
|
2604
|
-
{
|
|
2605
|
-
context: ctx,
|
|
2606
|
-
},
|
|
2607
|
-
() =>
|
|
2608
|
-
ctx.internalAdapter.updateUserByEmail(testUser.email, {
|
|
2609
|
-
email: "newemail@example.com",
|
|
2610
|
-
}),
|
|
2611
|
-
);
|
|
2612
|
-
|
|
2613
|
-
// Verify that Stripe customer.retrieve was called
|
|
2614
|
-
expect(mockStripe.customers.retrieve).toHaveBeenCalledWith("cus_mock123");
|
|
2615
|
-
|
|
2616
|
-
// Verify that Stripe customer.update was called with the new email
|
|
2617
|
-
expect(mockStripe.customers.update).toHaveBeenCalledWith("cus_mock123", {
|
|
2618
|
-
email: "newemail@example.com",
|
|
2619
|
-
});
|
|
2620
|
-
});
|
|
2621
|
-
|
|
2622
|
-
describe("getCustomerCreateParams", () => {
|
|
2623
|
-
it("should call getCustomerCreateParams and merge with default params", async () => {
|
|
2624
|
-
const getCustomerCreateParamsMock = vi
|
|
2625
|
-
.fn()
|
|
2626
|
-
.mockResolvedValue({ metadata: { customField: "customValue" } });
|
|
2627
|
-
|
|
2628
|
-
const testOptions = {
|
|
2629
|
-
...stripeOptions,
|
|
2630
|
-
createCustomerOnSignUp: true,
|
|
2631
|
-
getCustomerCreateParams: getCustomerCreateParamsMock,
|
|
2632
|
-
} satisfies StripeOptions;
|
|
2633
|
-
|
|
2634
|
-
const { client: testClient } = await getTestInstance(
|
|
2635
|
-
{
|
|
2636
|
-
database: memory,
|
|
2637
|
-
plugins: [stripe(testOptions)],
|
|
2638
|
-
},
|
|
2639
|
-
{
|
|
2640
|
-
disableTestUser: true,
|
|
2641
|
-
clientOptions: {
|
|
2642
|
-
plugins: [stripeClient({ subscription: true })],
|
|
2643
|
-
},
|
|
2644
|
-
},
|
|
2645
|
-
);
|
|
2646
|
-
|
|
2647
|
-
// Sign up a user
|
|
2648
|
-
const userRes = await testClient.signUp.email(
|
|
2649
|
-
{
|
|
2650
|
-
email: "custom-params@email.com",
|
|
2651
|
-
password: "password",
|
|
2652
|
-
name: "Custom User",
|
|
2653
|
-
},
|
|
2654
|
-
{
|
|
2655
|
-
throw: true,
|
|
2656
|
-
},
|
|
2657
|
-
);
|
|
2658
|
-
|
|
2659
|
-
// Verify getCustomerCreateParams was called
|
|
2660
|
-
expect(getCustomerCreateParamsMock).toHaveBeenCalledWith(
|
|
2661
|
-
expect.objectContaining({
|
|
2662
|
-
id: userRes.user.id,
|
|
2663
|
-
email: "custom-params@email.com",
|
|
2664
|
-
name: "Custom User",
|
|
2665
|
-
}),
|
|
2666
|
-
expect.objectContaining({
|
|
2667
|
-
context: expect.any(Object),
|
|
2668
|
-
}),
|
|
2669
|
-
);
|
|
2670
|
-
|
|
2671
|
-
// Verify customer was created with merged params
|
|
2672
|
-
expect(mockStripe.customers.create).toHaveBeenCalledWith(
|
|
2673
|
-
expect.objectContaining({
|
|
2674
|
-
email: "custom-params@email.com",
|
|
2675
|
-
name: "Custom User",
|
|
2676
|
-
metadata: expect.objectContaining({
|
|
2677
|
-
userId: userRes.user.id,
|
|
2678
|
-
customField: "customValue",
|
|
2679
|
-
}),
|
|
2680
|
-
}),
|
|
2681
|
-
);
|
|
2682
|
-
});
|
|
2683
|
-
|
|
2684
|
-
it("should use getCustomerCreateParams to add custom address", async () => {
|
|
2685
|
-
const getCustomerCreateParamsMock = vi.fn().mockResolvedValue({
|
|
2686
|
-
address: {
|
|
2687
|
-
line1: "123 Main St",
|
|
2688
|
-
city: "San Francisco",
|
|
2689
|
-
state: "CA",
|
|
2690
|
-
postal_code: "94111",
|
|
2691
|
-
country: "US",
|
|
2692
|
-
},
|
|
2693
|
-
});
|
|
2694
|
-
|
|
2695
|
-
const testOptions = {
|
|
2696
|
-
...stripeOptions,
|
|
2697
|
-
createCustomerOnSignUp: true,
|
|
2698
|
-
getCustomerCreateParams: getCustomerCreateParamsMock,
|
|
2699
|
-
} satisfies StripeOptions;
|
|
2700
|
-
|
|
2701
|
-
const { client: testAuthClient } = await getTestInstance(
|
|
2702
|
-
{
|
|
2703
|
-
database: memory,
|
|
2704
|
-
plugins: [stripe(testOptions)],
|
|
2705
|
-
},
|
|
2706
|
-
{
|
|
2707
|
-
disableTestUser: true,
|
|
2708
|
-
clientOptions: {
|
|
2709
|
-
plugins: [stripeClient({ subscription: true })],
|
|
2710
|
-
},
|
|
2711
|
-
},
|
|
2712
|
-
);
|
|
2713
|
-
|
|
2714
|
-
// Sign up a user
|
|
2715
|
-
await testAuthClient.signUp.email(
|
|
2716
|
-
{
|
|
2717
|
-
email: "address-user@email.com",
|
|
2718
|
-
password: "password",
|
|
2719
|
-
name: "Address User",
|
|
2720
|
-
},
|
|
2721
|
-
{
|
|
2722
|
-
throw: true,
|
|
2723
|
-
},
|
|
2724
|
-
);
|
|
2725
|
-
|
|
2726
|
-
// Verify customer was created with address
|
|
2727
|
-
expect(mockStripe.customers.create).toHaveBeenCalledWith(
|
|
2728
|
-
expect.objectContaining({
|
|
2729
|
-
email: "address-user@email.com",
|
|
2730
|
-
name: "Address User",
|
|
2731
|
-
address: {
|
|
2732
|
-
line1: "123 Main St",
|
|
2733
|
-
city: "San Francisco",
|
|
2734
|
-
state: "CA",
|
|
2735
|
-
postal_code: "94111",
|
|
2736
|
-
country: "US",
|
|
2737
|
-
},
|
|
2738
|
-
metadata: expect.objectContaining({
|
|
2739
|
-
userId: expect.any(String),
|
|
2740
|
-
}),
|
|
2741
|
-
}),
|
|
2742
|
-
);
|
|
2743
|
-
});
|
|
2744
|
-
|
|
2745
|
-
it("should properly merge nested objects using defu", async () => {
|
|
2746
|
-
const getCustomerCreateParamsMock = vi.fn().mockResolvedValue({
|
|
2747
|
-
metadata: {
|
|
2748
|
-
customField: "customValue",
|
|
2749
|
-
anotherField: "anotherValue",
|
|
2750
|
-
},
|
|
2751
|
-
phone: "+1234567890",
|
|
2752
|
-
});
|
|
2753
|
-
|
|
2754
|
-
const testOptions = {
|
|
2755
|
-
...stripeOptions,
|
|
2756
|
-
createCustomerOnSignUp: true,
|
|
2757
|
-
getCustomerCreateParams: getCustomerCreateParamsMock,
|
|
2758
|
-
} satisfies StripeOptions;
|
|
2759
|
-
|
|
2760
|
-
const { client: testAuthClient } = await getTestInstance(
|
|
2761
|
-
{
|
|
2762
|
-
database: memory,
|
|
2763
|
-
plugins: [stripe(testOptions)],
|
|
2764
|
-
},
|
|
2765
|
-
{
|
|
2766
|
-
disableTestUser: true,
|
|
2767
|
-
clientOptions: {
|
|
2768
|
-
plugins: [stripeClient({ subscription: true })],
|
|
2769
|
-
},
|
|
2770
|
-
},
|
|
2771
|
-
);
|
|
2772
|
-
|
|
2773
|
-
// Sign up a user
|
|
2774
|
-
const userRes = await testAuthClient.signUp.email(
|
|
2775
|
-
{
|
|
2776
|
-
email: "merge-test@email.com",
|
|
2777
|
-
password: "password",
|
|
2778
|
-
name: "Merge User",
|
|
2779
|
-
},
|
|
2780
|
-
{
|
|
2781
|
-
throw: true,
|
|
2782
|
-
},
|
|
2783
|
-
);
|
|
2784
|
-
|
|
2785
|
-
// Verify customer was created with properly merged params
|
|
2786
|
-
// defu merges objects and preserves all fields
|
|
2787
|
-
expect(mockStripe.customers.create).toHaveBeenCalledWith(
|
|
2788
|
-
expect.objectContaining({
|
|
2789
|
-
email: "merge-test@email.com",
|
|
2790
|
-
name: "Merge User",
|
|
2791
|
-
phone: "+1234567890",
|
|
2792
|
-
metadata: {
|
|
2793
|
-
customerType: "user",
|
|
2794
|
-
userId: userRes.user.id,
|
|
2795
|
-
customField: "customValue",
|
|
2796
|
-
anotherField: "anotherValue",
|
|
2797
|
-
},
|
|
2798
|
-
}),
|
|
2799
|
-
);
|
|
2800
|
-
});
|
|
2801
|
-
|
|
2802
|
-
it("should work without getCustomerCreateParams", async () => {
|
|
2803
|
-
// This test ensures backward compatibility
|
|
2804
|
-
const testOptions = {
|
|
2805
|
-
...stripeOptions,
|
|
2806
|
-
createCustomerOnSignUp: true,
|
|
2807
|
-
// No getCustomerCreateParams provided
|
|
2808
|
-
} satisfies StripeOptions;
|
|
2809
|
-
|
|
2810
|
-
const { client: testAuthClient } = await getTestInstance(
|
|
2811
|
-
{
|
|
2812
|
-
database: memory,
|
|
2813
|
-
plugins: [stripe(testOptions)],
|
|
2814
|
-
},
|
|
2815
|
-
{
|
|
2816
|
-
disableTestUser: true,
|
|
2817
|
-
clientOptions: {
|
|
2818
|
-
plugins: [stripeClient({ subscription: true })],
|
|
2819
|
-
},
|
|
2820
|
-
},
|
|
2821
|
-
);
|
|
2822
|
-
|
|
2823
|
-
// Sign up a user
|
|
2824
|
-
const userRes = await testAuthClient.signUp.email(
|
|
2825
|
-
{
|
|
2826
|
-
email: "no-custom-params@email.com",
|
|
2827
|
-
password: "password",
|
|
2828
|
-
name: "Default User",
|
|
2829
|
-
},
|
|
2830
|
-
{
|
|
2831
|
-
throw: true,
|
|
2832
|
-
},
|
|
2833
|
-
);
|
|
2834
|
-
|
|
2835
|
-
// Verify customer was created with default params only
|
|
2836
|
-
expect(mockStripe.customers.create).toHaveBeenCalledWith({
|
|
2837
|
-
email: "no-custom-params@email.com",
|
|
2838
|
-
name: "Default User",
|
|
2839
|
-
metadata: {
|
|
2840
|
-
customerType: "user",
|
|
2841
|
-
userId: userRes.user.id,
|
|
2842
|
-
},
|
|
2843
|
-
});
|
|
2844
|
-
});
|
|
2845
|
-
});
|
|
2846
|
-
|
|
2847
|
-
describe("Webhook Error Handling (Stripe v19)", () => {
|
|
2848
|
-
it("should handle invalid webhook signature with constructEventAsync", async () => {
|
|
2849
|
-
const mockError = new Error("Invalid signature");
|
|
2850
|
-
const stripeWithError = {
|
|
2851
|
-
...stripeOptions.stripeClient,
|
|
2852
|
-
webhooks: {
|
|
2853
|
-
constructEventAsync: vi.fn().mockRejectedValue(mockError),
|
|
2854
|
-
},
|
|
2855
|
-
};
|
|
2856
|
-
|
|
2857
|
-
const testOptions = {
|
|
2858
|
-
...stripeOptions,
|
|
2859
|
-
stripeClient: stripeWithError as unknown as Stripe,
|
|
2860
|
-
stripeWebhookSecret: "test_secret",
|
|
2861
|
-
};
|
|
2862
|
-
|
|
2863
|
-
const { auth: testAuth } = await getTestInstance(
|
|
2864
|
-
{
|
|
2865
|
-
database: memory,
|
|
2866
|
-
plugins: [stripe(testOptions)],
|
|
2867
|
-
},
|
|
2868
|
-
{
|
|
2869
|
-
disableTestUser: true,
|
|
2870
|
-
},
|
|
2871
|
-
);
|
|
2872
|
-
|
|
2873
|
-
const mockRequest = new Request(
|
|
2874
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
2875
|
-
{
|
|
2876
|
-
method: "POST",
|
|
2877
|
-
headers: {
|
|
2878
|
-
"stripe-signature": "invalid_signature",
|
|
2879
|
-
},
|
|
2880
|
-
body: JSON.stringify({ type: "test.event" }),
|
|
2881
|
-
},
|
|
2882
|
-
);
|
|
2883
|
-
|
|
2884
|
-
const response = await testAuth.handler(mockRequest);
|
|
2885
|
-
expect(response.status).toBe(400);
|
|
2886
|
-
const data = await response.json();
|
|
2887
|
-
expect(data.message).toContain("Failed to construct Stripe event");
|
|
2888
|
-
});
|
|
2889
|
-
|
|
2890
|
-
it("should reject webhook request without stripe-signature header", async () => {
|
|
2891
|
-
const { auth: testAuth } = await getTestInstance(
|
|
2892
|
-
{
|
|
2893
|
-
database: memory,
|
|
2894
|
-
plugins: [stripe(stripeOptions)],
|
|
2895
|
-
},
|
|
2896
|
-
{
|
|
2897
|
-
disableTestUser: true,
|
|
2898
|
-
},
|
|
2899
|
-
);
|
|
2900
|
-
|
|
2901
|
-
const mockRequest = new Request(
|
|
2902
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
2903
|
-
{
|
|
2904
|
-
method: "POST",
|
|
2905
|
-
headers: {
|
|
2906
|
-
"content-type": "application/json",
|
|
2907
|
-
},
|
|
2908
|
-
body: JSON.stringify({ type: "test.event" }),
|
|
2909
|
-
},
|
|
2910
|
-
);
|
|
2911
|
-
|
|
2912
|
-
const response = await testAuth.handler(mockRequest);
|
|
2913
|
-
expect(response.status).toBe(400);
|
|
2914
|
-
const data = await response.json();
|
|
2915
|
-
expect(data.message).toContain("Stripe signature not found");
|
|
2916
|
-
});
|
|
2917
|
-
|
|
2918
|
-
it("should handle constructEventAsync returning null/undefined", async () => {
|
|
2919
|
-
const stripeWithNull = {
|
|
2920
|
-
...stripeOptions.stripeClient,
|
|
2921
|
-
webhooks: {
|
|
2922
|
-
constructEventAsync: vi.fn().mockResolvedValue(null),
|
|
2923
|
-
},
|
|
2924
|
-
};
|
|
2925
|
-
|
|
2926
|
-
const testOptions = {
|
|
2927
|
-
...stripeOptions,
|
|
2928
|
-
stripeClient: stripeWithNull as unknown as Stripe,
|
|
2929
|
-
stripeWebhookSecret: "test_secret",
|
|
2930
|
-
};
|
|
2931
|
-
|
|
2932
|
-
const { auth: testAuth } = await getTestInstance(
|
|
2933
|
-
{
|
|
2934
|
-
database: memory,
|
|
2935
|
-
plugins: [stripe(testOptions)],
|
|
2936
|
-
},
|
|
2937
|
-
{
|
|
2938
|
-
disableTestUser: true,
|
|
2939
|
-
},
|
|
2940
|
-
);
|
|
2941
|
-
|
|
2942
|
-
const mockRequest = new Request(
|
|
2943
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
2944
|
-
{
|
|
2945
|
-
method: "POST",
|
|
2946
|
-
headers: {
|
|
2947
|
-
"stripe-signature": "test_signature",
|
|
2948
|
-
},
|
|
2949
|
-
body: JSON.stringify({ type: "test.event" }),
|
|
2950
|
-
},
|
|
2951
|
-
);
|
|
2952
|
-
|
|
2953
|
-
const response = await testAuth.handler(mockRequest);
|
|
2954
|
-
expect(response.status).toBe(400);
|
|
2955
|
-
const data = await response.json();
|
|
2956
|
-
expect(data.message).toContain("Failed to construct Stripe event");
|
|
2957
|
-
});
|
|
2958
|
-
|
|
2959
|
-
it("should handle async errors in webhook event processing", async () => {
|
|
2960
|
-
const errorThrowingHandler = vi
|
|
2961
|
-
.fn()
|
|
2962
|
-
.mockRejectedValue(new Error("Event processing failed"));
|
|
2963
|
-
|
|
2964
|
-
const mockEvent = {
|
|
2965
|
-
type: "checkout.session.completed",
|
|
2966
|
-
data: {
|
|
2967
|
-
object: {
|
|
2968
|
-
mode: "subscription",
|
|
2969
|
-
subscription: "sub_123",
|
|
2970
|
-
metadata: {
|
|
2971
|
-
referenceId: "user_123",
|
|
2972
|
-
subscriptionId: "sub_123",
|
|
2973
|
-
},
|
|
2974
|
-
},
|
|
2975
|
-
},
|
|
2976
|
-
};
|
|
2977
|
-
|
|
2978
|
-
const stripeForTest = {
|
|
2979
|
-
...stripeOptions.stripeClient,
|
|
2980
|
-
subscriptions: {
|
|
2981
|
-
retrieve: vi.fn().mockRejectedValue(new Error("Stripe API error")),
|
|
2982
|
-
},
|
|
2983
|
-
webhooks: {
|
|
2984
|
-
constructEventAsync: vi.fn().mockResolvedValue(mockEvent),
|
|
2985
|
-
},
|
|
2986
|
-
};
|
|
2987
|
-
|
|
2988
|
-
const testOptions = {
|
|
2989
|
-
...stripeOptions,
|
|
2990
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
2991
|
-
stripeWebhookSecret: "test_secret",
|
|
2992
|
-
subscription: {
|
|
2993
|
-
...stripeOptions.subscription,
|
|
2994
|
-
onSubscriptionComplete: errorThrowingHandler,
|
|
2995
|
-
},
|
|
2996
|
-
};
|
|
2997
|
-
|
|
2998
|
-
const { auth: testAuth } = await getTestInstance(
|
|
2999
|
-
{
|
|
3000
|
-
database: memory,
|
|
3001
|
-
plugins: [stripe(testOptions as StripeOptions)],
|
|
3002
|
-
},
|
|
3003
|
-
{
|
|
3004
|
-
disableTestUser: true,
|
|
3005
|
-
},
|
|
3006
|
-
);
|
|
3007
|
-
const testCtx = await testAuth.$context;
|
|
3008
|
-
|
|
3009
|
-
await testCtx.adapter.create({
|
|
3010
|
-
model: "subscription",
|
|
3011
|
-
data: {
|
|
3012
|
-
referenceId: "user_123",
|
|
3013
|
-
stripeCustomerId: "cus_123",
|
|
3014
|
-
status: "incomplete",
|
|
3015
|
-
plan: "starter",
|
|
3016
|
-
id: "sub_123",
|
|
3017
|
-
},
|
|
3018
|
-
});
|
|
3019
|
-
|
|
3020
|
-
const mockRequest = new Request(
|
|
3021
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
3022
|
-
{
|
|
3023
|
-
method: "POST",
|
|
3024
|
-
headers: {
|
|
3025
|
-
"stripe-signature": "test_signature",
|
|
3026
|
-
},
|
|
3027
|
-
body: JSON.stringify(mockEvent),
|
|
3028
|
-
},
|
|
3029
|
-
);
|
|
3030
|
-
|
|
3031
|
-
const response = await testAuth.handler(mockRequest);
|
|
3032
|
-
// Errors inside event handlers are caught and logged but don't fail the webhook
|
|
3033
|
-
// This prevents Stripe from retrying and is the expected behavior
|
|
3034
|
-
expect(response.status).toBe(200);
|
|
3035
|
-
const data = await response.json();
|
|
3036
|
-
expect(data).toEqual({ success: true });
|
|
3037
|
-
// Verify the error was logged (via the stripeClient.subscriptions.retrieve rejection)
|
|
3038
|
-
expect(stripeForTest.subscriptions.retrieve).toHaveBeenCalled();
|
|
3039
|
-
});
|
|
3040
|
-
|
|
3041
|
-
it("should successfully process webhook with valid async signature verification", async () => {
|
|
3042
|
-
const mockEvent = {
|
|
3043
|
-
type: "customer.subscription.updated",
|
|
3044
|
-
data: {
|
|
3045
|
-
object: {
|
|
3046
|
-
id: "sub_test_async",
|
|
3047
|
-
customer: "cus_test_async",
|
|
3048
|
-
status: "active",
|
|
3049
|
-
items: {
|
|
3050
|
-
data: [
|
|
3051
|
-
{
|
|
3052
|
-
price: { id: process.env.STRIPE_PRICE_ID_1 },
|
|
3053
|
-
quantity: 1,
|
|
3054
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
3055
|
-
current_period_end:
|
|
3056
|
-
Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
3057
|
-
},
|
|
3058
|
-
],
|
|
3059
|
-
},
|
|
3060
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
3061
|
-
current_period_end:
|
|
3062
|
-
Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
3063
|
-
},
|
|
3064
|
-
},
|
|
3065
|
-
};
|
|
3066
|
-
|
|
3067
|
-
const stripeForTest = {
|
|
3068
|
-
...stripeOptions.stripeClient,
|
|
3069
|
-
webhooks: {
|
|
3070
|
-
// Simulate async verification success
|
|
3071
|
-
constructEventAsync: vi.fn().mockResolvedValue(mockEvent),
|
|
3072
|
-
},
|
|
3073
|
-
};
|
|
3074
|
-
|
|
3075
|
-
const testOptions = {
|
|
3076
|
-
...stripeOptions,
|
|
3077
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
3078
|
-
stripeWebhookSecret: "test_secret_async",
|
|
3079
|
-
};
|
|
3080
|
-
|
|
3081
|
-
const { auth: testAuth } = await getTestInstance(
|
|
3082
|
-
{
|
|
3083
|
-
database: memory,
|
|
3084
|
-
plugins: [stripe(testOptions)],
|
|
3085
|
-
},
|
|
3086
|
-
{
|
|
3087
|
-
disableTestUser: true,
|
|
3088
|
-
},
|
|
3089
|
-
);
|
|
3090
|
-
const testCtx = await testAuth.$context;
|
|
3091
|
-
|
|
3092
|
-
const { id: testUserId } = await testCtx.adapter.create({
|
|
3093
|
-
model: "user",
|
|
3094
|
-
data: {
|
|
3095
|
-
email: "async-test@email.com",
|
|
3096
|
-
},
|
|
3097
|
-
});
|
|
3098
|
-
|
|
3099
|
-
await testCtx.adapter.create({
|
|
3100
|
-
model: "subscription",
|
|
3101
|
-
data: {
|
|
3102
|
-
referenceId: testUserId,
|
|
3103
|
-
stripeCustomerId: "cus_test_async",
|
|
3104
|
-
stripeSubscriptionId: "sub_test_async",
|
|
3105
|
-
status: "incomplete",
|
|
3106
|
-
plan: "starter",
|
|
3107
|
-
},
|
|
3108
|
-
});
|
|
3109
|
-
|
|
3110
|
-
const mockRequest = new Request(
|
|
3111
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
3112
|
-
{
|
|
3113
|
-
method: "POST",
|
|
3114
|
-
headers: {
|
|
3115
|
-
"stripe-signature": "valid_async_signature",
|
|
3116
|
-
},
|
|
3117
|
-
body: JSON.stringify(mockEvent),
|
|
3118
|
-
},
|
|
3119
|
-
);
|
|
3120
|
-
|
|
3121
|
-
const response = await testAuth.handler(mockRequest);
|
|
3122
|
-
expect(response.status).toBe(200);
|
|
3123
|
-
expect(stripeForTest.webhooks.constructEventAsync).toHaveBeenCalledWith(
|
|
3124
|
-
expect.any(String),
|
|
3125
|
-
"valid_async_signature",
|
|
3126
|
-
"test_secret_async",
|
|
3127
|
-
);
|
|
3128
|
-
|
|
3129
|
-
const data = await response.json();
|
|
3130
|
-
expect(data).toEqual({ success: true });
|
|
3131
|
-
});
|
|
3132
|
-
|
|
3133
|
-
it("should call constructEventAsync with exactly 3 required parameters", async () => {
|
|
3134
|
-
const mockEvent = {
|
|
3135
|
-
type: "customer.subscription.created",
|
|
3136
|
-
data: {
|
|
3137
|
-
object: {
|
|
3138
|
-
id: "sub_test_params",
|
|
3139
|
-
customer: "cus_test_params",
|
|
3140
|
-
status: "active",
|
|
3141
|
-
},
|
|
3142
|
-
},
|
|
3143
|
-
};
|
|
3144
|
-
|
|
3145
|
-
const stripeForTest = {
|
|
3146
|
-
...stripeOptions.stripeClient,
|
|
3147
|
-
webhooks: {
|
|
3148
|
-
constructEventAsync: vi.fn().mockResolvedValue(mockEvent),
|
|
3149
|
-
},
|
|
3150
|
-
};
|
|
3151
|
-
|
|
3152
|
-
const testOptions = {
|
|
3153
|
-
...stripeOptions,
|
|
3154
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
3155
|
-
stripeWebhookSecret: "test_secret_params",
|
|
3156
|
-
};
|
|
3157
|
-
|
|
3158
|
-
const { auth: testAuth } = await getTestInstance(
|
|
3159
|
-
{
|
|
3160
|
-
database: memory,
|
|
3161
|
-
plugins: [stripe(testOptions)],
|
|
3162
|
-
},
|
|
3163
|
-
{
|
|
3164
|
-
disableTestUser: true,
|
|
3165
|
-
},
|
|
3166
|
-
);
|
|
3167
|
-
|
|
3168
|
-
const mockRequest = new Request(
|
|
3169
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
3170
|
-
{
|
|
3171
|
-
method: "POST",
|
|
3172
|
-
headers: {
|
|
3173
|
-
"stripe-signature": "test_signature_params",
|
|
3174
|
-
},
|
|
3175
|
-
body: JSON.stringify(mockEvent),
|
|
3176
|
-
},
|
|
3177
|
-
);
|
|
3178
|
-
|
|
3179
|
-
await testAuth.handler(mockRequest);
|
|
3180
|
-
|
|
3181
|
-
// Verify that constructEventAsync is called with exactly 3 required parameters
|
|
3182
|
-
// (payload, signature, secret) and no optional parameters
|
|
3183
|
-
expect(stripeForTest.webhooks.constructEventAsync).toHaveBeenCalledWith(
|
|
3184
|
-
expect.any(String), // payload
|
|
3185
|
-
"test_signature_params", // signature
|
|
3186
|
-
"test_secret_params", // secret
|
|
3187
|
-
);
|
|
3188
|
-
|
|
3189
|
-
// Verify it was called exactly once
|
|
3190
|
-
expect(stripeForTest.webhooks.constructEventAsync).toHaveBeenCalledTimes(
|
|
3191
|
-
1,
|
|
3192
|
-
);
|
|
3193
|
-
});
|
|
3194
|
-
|
|
3195
|
-
it("should support Stripe v18 with sync constructEvent method", async () => {
|
|
3196
|
-
const mockEvent = {
|
|
3197
|
-
type: "customer.subscription.updated",
|
|
3198
|
-
data: {
|
|
3199
|
-
object: {
|
|
3200
|
-
id: "sub_test_v18",
|
|
3201
|
-
customer: "cus_test_v18",
|
|
3202
|
-
status: "active",
|
|
3203
|
-
items: {
|
|
3204
|
-
data: [
|
|
3205
|
-
{
|
|
3206
|
-
price: { id: process.env.STRIPE_PRICE_ID_1 },
|
|
3207
|
-
quantity: 1,
|
|
3208
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
3209
|
-
current_period_end:
|
|
3210
|
-
Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
3211
|
-
},
|
|
3212
|
-
],
|
|
3213
|
-
},
|
|
3214
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
3215
|
-
current_period_end:
|
|
3216
|
-
Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
3217
|
-
},
|
|
3218
|
-
},
|
|
3219
|
-
};
|
|
3220
|
-
|
|
3221
|
-
// Simulate Stripe v18 - only has sync constructEvent, no constructEventAsync
|
|
3222
|
-
const stripeV18 = {
|
|
3223
|
-
...stripeOptions.stripeClient,
|
|
3224
|
-
webhooks: {
|
|
3225
|
-
constructEvent: vi.fn().mockReturnValue(mockEvent),
|
|
3226
|
-
// v18 doesn't have constructEventAsync
|
|
3227
|
-
constructEventAsync: undefined,
|
|
3228
|
-
},
|
|
3229
|
-
};
|
|
3230
|
-
|
|
3231
|
-
const testOptions = {
|
|
3232
|
-
...stripeOptions,
|
|
3233
|
-
stripeClient: stripeV18 as unknown as Stripe,
|
|
3234
|
-
stripeWebhookSecret: "test_secret_v18",
|
|
3235
|
-
};
|
|
3236
|
-
|
|
3237
|
-
const { auth: testAuth } = await getTestInstance(
|
|
3238
|
-
{
|
|
3239
|
-
database: memory,
|
|
3240
|
-
plugins: [stripe(testOptions)],
|
|
3241
|
-
},
|
|
3242
|
-
{
|
|
3243
|
-
disableTestUser: true,
|
|
3244
|
-
},
|
|
3245
|
-
);
|
|
3246
|
-
const testCtx = await testAuth.$context;
|
|
3247
|
-
|
|
3248
|
-
const { id: testUserId } = await testCtx.adapter.create({
|
|
3249
|
-
model: "user",
|
|
3250
|
-
data: {
|
|
3251
|
-
email: "v18-test@email.com",
|
|
3252
|
-
},
|
|
3253
|
-
});
|
|
3254
|
-
|
|
3255
|
-
await testCtx.adapter.create({
|
|
3256
|
-
model: "subscription",
|
|
3257
|
-
data: {
|
|
3258
|
-
referenceId: testUserId,
|
|
3259
|
-
stripeCustomerId: "cus_test_v18",
|
|
3260
|
-
stripeSubscriptionId: "sub_test_v18",
|
|
3261
|
-
status: "incomplete",
|
|
3262
|
-
plan: "starter",
|
|
3263
|
-
},
|
|
3264
|
-
});
|
|
3265
|
-
|
|
3266
|
-
const mockRequest = new Request(
|
|
3267
|
-
"http://localhost:3000/api/auth/stripe/webhook",
|
|
3268
|
-
{
|
|
3269
|
-
method: "POST",
|
|
3270
|
-
headers: {
|
|
3271
|
-
"stripe-signature": "test_signature_v18",
|
|
3272
|
-
},
|
|
3273
|
-
body: JSON.stringify(mockEvent),
|
|
3274
|
-
},
|
|
3275
|
-
);
|
|
3276
|
-
|
|
3277
|
-
const response = await testAuth.handler(mockRequest);
|
|
3278
|
-
expect(response.status).toBe(200);
|
|
3279
|
-
|
|
3280
|
-
// Verify that constructEvent (sync) was called instead of constructEventAsync
|
|
3281
|
-
expect(stripeV18.webhooks.constructEvent).toHaveBeenCalledWith(
|
|
3282
|
-
expect.any(String),
|
|
3283
|
-
"test_signature_v18",
|
|
3284
|
-
"test_secret_v18",
|
|
3285
|
-
);
|
|
3286
|
-
expect(stripeV18.webhooks.constructEvent).toHaveBeenCalledTimes(1);
|
|
3287
|
-
|
|
3288
|
-
const data = await response.json();
|
|
3289
|
-
expect(data).toEqual({ success: true });
|
|
3290
|
-
});
|
|
3291
|
-
});
|
|
3292
|
-
|
|
3293
|
-
it("should support flexible limits types", async () => {
|
|
3294
|
-
const flexiblePlans = [
|
|
3295
|
-
{
|
|
3296
|
-
name: "flexible",
|
|
3297
|
-
priceId: "price_flexible",
|
|
3298
|
-
limits: {
|
|
3299
|
-
// Numbers
|
|
3300
|
-
maxUsers: 100,
|
|
3301
|
-
maxProjects: 10,
|
|
3302
|
-
// Arrays
|
|
3303
|
-
features: ["analytics", "api", "webhooks"],
|
|
3304
|
-
supportedMethods: ["GET", "POST", "PUT", "DELETE"],
|
|
3305
|
-
// Objects
|
|
3306
|
-
rateLimit: { requests: 1000, window: 3600 },
|
|
3307
|
-
permissions: { admin: true, read: true, write: false },
|
|
3308
|
-
// Mixed
|
|
3309
|
-
quotas: {
|
|
3310
|
-
storage: 50,
|
|
3311
|
-
bandwidth: [100, "GB"],
|
|
3312
|
-
},
|
|
3313
|
-
},
|
|
3314
|
-
},
|
|
3315
|
-
];
|
|
3316
|
-
|
|
3317
|
-
const {
|
|
3318
|
-
client: testClient,
|
|
3319
|
-
auth: testAuth,
|
|
3320
|
-
sessionSetter: testSessionSetter,
|
|
3321
|
-
} = await getTestInstance(
|
|
3322
|
-
{
|
|
3323
|
-
database: memory,
|
|
3324
|
-
plugins: [
|
|
3325
|
-
stripe({
|
|
3326
|
-
...stripeOptions,
|
|
3327
|
-
subscription: {
|
|
3328
|
-
enabled: true,
|
|
3329
|
-
plans: flexiblePlans,
|
|
3330
|
-
},
|
|
3331
|
-
}),
|
|
3332
|
-
],
|
|
3333
|
-
},
|
|
3334
|
-
{
|
|
3335
|
-
disableTestUser: true,
|
|
3336
|
-
clientOptions: {
|
|
3337
|
-
plugins: [stripeClient({ subscription: true })],
|
|
3338
|
-
},
|
|
3339
|
-
},
|
|
3340
|
-
);
|
|
3341
|
-
const testCtx = await testAuth.$context;
|
|
3342
|
-
|
|
3343
|
-
// Create user and sign in
|
|
3344
|
-
const headers = new Headers();
|
|
3345
|
-
const userRes = await testClient.signUp.email(
|
|
3346
|
-
{ email: "limits@test.com", password: "password", name: "Test" },
|
|
3347
|
-
{ throw: true },
|
|
3348
|
-
);
|
|
3349
|
-
const limitUserId = userRes.user.id;
|
|
3350
|
-
|
|
3351
|
-
await testClient.signIn.email(
|
|
3352
|
-
{ email: "limits@test.com", password: "password" },
|
|
3353
|
-
{ throw: true, onSuccess: testSessionSetter(headers) },
|
|
3354
|
-
);
|
|
3355
|
-
|
|
3356
|
-
// Create subscription
|
|
3357
|
-
await testCtx.adapter.create({
|
|
3358
|
-
model: "subscription",
|
|
3359
|
-
data: {
|
|
3360
|
-
referenceId: limitUserId,
|
|
3361
|
-
stripeCustomerId: "cus_limits_test",
|
|
3362
|
-
stripeSubscriptionId: "sub_limits_test",
|
|
3363
|
-
status: "active",
|
|
3364
|
-
plan: "flexible",
|
|
3365
|
-
},
|
|
3366
|
-
});
|
|
3367
|
-
|
|
3368
|
-
// List subscriptions and verify limits structure
|
|
3369
|
-
const result = await testClient.subscription.list({
|
|
3370
|
-
fetchOptions: { headers, throw: true },
|
|
3371
|
-
});
|
|
3372
|
-
|
|
3373
|
-
expect(result.length).toBe(1);
|
|
3374
|
-
const limits = result[0]?.limits;
|
|
3375
|
-
|
|
3376
|
-
// Verify different types are preserved
|
|
3377
|
-
expect(limits).toBeDefined();
|
|
3378
|
-
|
|
3379
|
-
// Type-safe access with unknown (cast once for test convenience)
|
|
3380
|
-
const typedLimits = limits as Record<string, unknown>;
|
|
3381
|
-
expect(typedLimits.maxUsers).toBe(100);
|
|
3382
|
-
expect(typedLimits.maxProjects).toBe(10);
|
|
3383
|
-
expect(typeof typedLimits.rateLimit).toBe("object");
|
|
3384
|
-
expect(typedLimits.features).toEqual(["analytics", "api", "webhooks"]);
|
|
3385
|
-
expect(Array.isArray(typedLimits.features)).toBe(true);
|
|
3386
|
-
expect(Array.isArray(typedLimits.supportedMethods)).toBe(true);
|
|
3387
|
-
expect((typedLimits.quotas as Record<string, unknown>).storage).toBe(50);
|
|
3388
|
-
expect((typedLimits.rateLimit as Record<string, unknown>).requests).toBe(
|
|
3389
|
-
1000,
|
|
3390
|
-
);
|
|
3391
|
-
expect((typedLimits.permissions as Record<string, unknown>).admin).toBe(
|
|
3392
|
-
true,
|
|
3393
|
-
);
|
|
3394
|
-
expect(
|
|
3395
|
-
Array.isArray((typedLimits.quotas as Record<string, unknown>).bandwidth),
|
|
3396
|
-
).toBe(true);
|
|
3397
|
-
});
|
|
3398
|
-
|
|
3399
|
-
describe("Duplicate customer prevention on signup", () => {
|
|
3400
|
-
it("should NOT create duplicate customer when email already exists in Stripe", async () => {
|
|
3401
|
-
const existingEmail = "duplicate-email@example.com";
|
|
3402
|
-
const existingCustomerId = "cus_stripe_existing_456";
|
|
3403
|
-
|
|
3404
|
-
mockStripe.customers.search.mockResolvedValueOnce({
|
|
3405
|
-
data: [
|
|
3406
|
-
{
|
|
3407
|
-
id: existingCustomerId,
|
|
3408
|
-
email: existingEmail,
|
|
3409
|
-
name: "Existing Stripe Customer",
|
|
3410
|
-
},
|
|
3411
|
-
],
|
|
3412
|
-
});
|
|
3413
|
-
|
|
3414
|
-
const testOptionsWithHook = {
|
|
3415
|
-
...stripeOptions,
|
|
3416
|
-
createCustomerOnSignUp: true,
|
|
3417
|
-
} satisfies StripeOptions;
|
|
3418
|
-
|
|
3419
|
-
const { client: testAuthClient, auth: testAuth } = await getTestInstance(
|
|
3420
|
-
{
|
|
3421
|
-
database: memory,
|
|
3422
|
-
plugins: [stripe(testOptionsWithHook)],
|
|
3423
|
-
},
|
|
3424
|
-
{
|
|
3425
|
-
disableTestUser: true,
|
|
3426
|
-
clientOptions: {
|
|
3427
|
-
plugins: [stripeClient({ subscription: true })],
|
|
3428
|
-
},
|
|
3429
|
-
},
|
|
3430
|
-
);
|
|
3431
|
-
const testCtx = await testAuth.$context;
|
|
3432
|
-
|
|
3433
|
-
vi.clearAllMocks();
|
|
3434
|
-
|
|
3435
|
-
// Sign up with email that exists in Stripe
|
|
3436
|
-
const userRes = await testAuthClient.signUp.email(
|
|
3437
|
-
{
|
|
3438
|
-
email: existingEmail,
|
|
3439
|
-
password: "password",
|
|
3440
|
-
name: "Duplicate Email User",
|
|
3441
|
-
},
|
|
3442
|
-
{ throw: true },
|
|
3443
|
-
);
|
|
3444
|
-
|
|
3445
|
-
// Should check for existing user customer by email (excluding organization customers)
|
|
3446
|
-
expect(mockStripe.customers.search).toHaveBeenCalledWith({
|
|
3447
|
-
query: `email:"${existingEmail}" AND -metadata["customerType"]:"organization"`,
|
|
3448
|
-
limit: 1,
|
|
3449
|
-
});
|
|
3450
|
-
|
|
3451
|
-
// Should NOT create duplicate customer
|
|
3452
|
-
expect(mockStripe.customers.create).not.toHaveBeenCalled();
|
|
3453
|
-
|
|
3454
|
-
// Verify user has the EXISTING Stripe customer ID (not new duplicate)
|
|
3455
|
-
const user = await testCtx.adapter.findOne<
|
|
3456
|
-
User & { stripeCustomerId?: string }
|
|
3457
|
-
>({
|
|
3458
|
-
model: "user",
|
|
3459
|
-
where: [{ field: "id", value: userRes.user.id }],
|
|
3460
|
-
});
|
|
3461
|
-
expect(user?.stripeCustomerId).toBe(existingCustomerId); // Should use existing ID
|
|
3462
|
-
});
|
|
3463
|
-
|
|
3464
|
-
it("should CREATE customer only when user has no stripeCustomerId and none exists in Stripe", async () => {
|
|
3465
|
-
const newEmail = "brand-new@example.com";
|
|
3466
|
-
|
|
3467
|
-
mockStripe.customers.search.mockResolvedValueOnce({
|
|
3468
|
-
data: [],
|
|
3469
|
-
});
|
|
3470
|
-
|
|
3471
|
-
mockStripe.customers.create.mockResolvedValueOnce({
|
|
3472
|
-
id: "cus_new_created_789",
|
|
3473
|
-
email: newEmail,
|
|
3474
|
-
});
|
|
3475
|
-
|
|
3476
|
-
const testOptionsWithHook = {
|
|
3477
|
-
...stripeOptions,
|
|
3478
|
-
createCustomerOnSignUp: true,
|
|
3479
|
-
} satisfies StripeOptions;
|
|
3480
|
-
|
|
3481
|
-
const { client: testAuthClient, auth: testAuth } = await getTestInstance(
|
|
3482
|
-
{
|
|
3483
|
-
database: memory,
|
|
3484
|
-
plugins: [stripe(testOptionsWithHook)],
|
|
3485
|
-
},
|
|
3486
|
-
{
|
|
3487
|
-
disableTestUser: true,
|
|
3488
|
-
clientOptions: {
|
|
3489
|
-
plugins: [stripeClient({ subscription: true })],
|
|
3490
|
-
},
|
|
3491
|
-
},
|
|
3492
|
-
);
|
|
3493
|
-
const testCtx = await testAuth.$context;
|
|
3494
|
-
|
|
3495
|
-
vi.clearAllMocks();
|
|
3496
|
-
|
|
3497
|
-
// Sign up with brand new email
|
|
3498
|
-
const userRes = await testAuthClient.signUp.email(
|
|
3499
|
-
{
|
|
3500
|
-
email: newEmail,
|
|
3501
|
-
password: "password",
|
|
3502
|
-
name: "Brand New User",
|
|
3503
|
-
},
|
|
3504
|
-
{ throw: true },
|
|
3505
|
-
);
|
|
3506
|
-
|
|
3507
|
-
// Should check for existing user customer first (excluding organization customers)
|
|
3508
|
-
expect(mockStripe.customers.search).toHaveBeenCalledWith({
|
|
3509
|
-
query: `email:"${newEmail}" AND -metadata["customerType"]:"organization"`,
|
|
3510
|
-
limit: 1,
|
|
3511
|
-
});
|
|
3512
|
-
|
|
3513
|
-
// Should create new customer (this is correct behavior)
|
|
3514
|
-
expect(mockStripe.customers.create).toHaveBeenCalledTimes(1);
|
|
3515
|
-
expect(mockStripe.customers.create).toHaveBeenCalledWith({
|
|
3516
|
-
email: newEmail,
|
|
3517
|
-
name: "Brand New User",
|
|
3518
|
-
metadata: {
|
|
3519
|
-
userId: userRes.user.id,
|
|
3520
|
-
customerType: "user",
|
|
3521
|
-
},
|
|
3522
|
-
});
|
|
3523
|
-
|
|
3524
|
-
// Verify user has the new Stripe customer ID
|
|
3525
|
-
const user = await testCtx.adapter.findOne<
|
|
3526
|
-
User & { stripeCustomerId?: string }
|
|
3527
|
-
>({
|
|
3528
|
-
model: "user",
|
|
3529
|
-
where: [{ field: "id", value: userRes.user.id }],
|
|
3530
|
-
});
|
|
3531
|
-
expect(user?.stripeCustomerId).toBeDefined();
|
|
3532
|
-
});
|
|
3533
|
-
});
|
|
3534
|
-
|
|
3535
|
-
describe("User/Organization customer collision prevention", () => {
|
|
3536
|
-
it("should NOT return organization customer when searching for user customer with same email", async () => {
|
|
3537
|
-
// Scenario: Organization has a Stripe customer with email "shared@example.com"
|
|
3538
|
-
// When a user signs up with the same email, the search should NOT find the org customer
|
|
3539
|
-
const sharedEmail = "shared@example.com";
|
|
3540
|
-
const orgCustomerId = "cus_org_123";
|
|
3541
|
-
|
|
3542
|
-
// Mock: Only organization customer exists with this email
|
|
3543
|
-
// The search query includes `-metadata['customerType']:'organization'`
|
|
3544
|
-
// so this should NOT be returned
|
|
3545
|
-
mockStripe.customers.search.mockResolvedValueOnce({
|
|
3546
|
-
data: [], // Organization customer is excluded by the search query
|
|
3547
|
-
});
|
|
3548
|
-
|
|
3549
|
-
mockStripe.customers.create.mockResolvedValueOnce({
|
|
3550
|
-
id: "cus_user_new_456",
|
|
3551
|
-
email: sharedEmail,
|
|
3552
|
-
});
|
|
3553
|
-
|
|
3554
|
-
const testOptions = {
|
|
3555
|
-
...stripeOptions,
|
|
3556
|
-
createCustomerOnSignUp: true,
|
|
3557
|
-
} satisfies StripeOptions;
|
|
3558
|
-
|
|
3559
|
-
const { client: testAuthClient, auth: testAuth } = await getTestInstance(
|
|
3560
|
-
{
|
|
3561
|
-
database: memory,
|
|
3562
|
-
plugins: [stripe(testOptions)],
|
|
3563
|
-
},
|
|
3564
|
-
{
|
|
3565
|
-
disableTestUser: true,
|
|
3566
|
-
clientOptions: {
|
|
3567
|
-
plugins: [stripeClient({ subscription: true })],
|
|
3568
|
-
},
|
|
3569
|
-
},
|
|
3570
|
-
);
|
|
3571
|
-
const testCtx = await testAuth.$context;
|
|
3572
|
-
|
|
3573
|
-
vi.clearAllMocks();
|
|
3574
|
-
|
|
3575
|
-
// User signs up with email that organization already uses
|
|
3576
|
-
const userRes = await testAuthClient.signUp.email(
|
|
3577
|
-
{
|
|
3578
|
-
email: sharedEmail,
|
|
3579
|
-
password: "password",
|
|
3580
|
-
name: "User With Shared Email",
|
|
3581
|
-
},
|
|
3582
|
-
{ throw: true },
|
|
3583
|
-
);
|
|
3584
|
-
|
|
3585
|
-
// Should search with query that EXCLUDES organization customers
|
|
3586
|
-
expect(mockStripe.customers.search).toHaveBeenCalledWith({
|
|
3587
|
-
query: `email:"${sharedEmail}" AND -metadata["customerType"]:"organization"`,
|
|
3588
|
-
limit: 1,
|
|
3589
|
-
});
|
|
3590
|
-
|
|
3591
|
-
// Should create NEW user customer (not use org customer)
|
|
3592
|
-
expect(mockStripe.customers.create).toHaveBeenCalledTimes(1);
|
|
3593
|
-
expect(mockStripe.customers.create).toHaveBeenCalledWith({
|
|
3594
|
-
email: sharedEmail,
|
|
3595
|
-
name: "User With Shared Email",
|
|
3596
|
-
metadata: {
|
|
3597
|
-
customerType: "user",
|
|
3598
|
-
userId: userRes.user.id,
|
|
3599
|
-
},
|
|
3600
|
-
});
|
|
3601
|
-
|
|
3602
|
-
// Verify user has their own customer ID (not the org's)
|
|
3603
|
-
const user = await testCtx.adapter.findOne<
|
|
3604
|
-
User & { stripeCustomerId?: string }
|
|
3605
|
-
>({
|
|
3606
|
-
model: "user",
|
|
3607
|
-
where: [{ field: "id", value: userRes.user.id }],
|
|
3608
|
-
});
|
|
3609
|
-
expect(user?.stripeCustomerId).toBe("cus_user_new_456");
|
|
3610
|
-
expect(user?.stripeCustomerId).not.toBe(orgCustomerId);
|
|
3611
|
-
});
|
|
3612
|
-
|
|
3613
|
-
it("should find existing user customer even when organization customer with same email exists", async () => {
|
|
3614
|
-
// Scenario: Both user and organization customers exist with same email
|
|
3615
|
-
// The search should only return the user customer
|
|
3616
|
-
const sharedEmail = "both-exist@example.com";
|
|
3617
|
-
const existingUserCustomerId = "cus_user_existing_789";
|
|
3618
|
-
|
|
3619
|
-
// Mock: Search returns ONLY user customer (org customer excluded by query)
|
|
3620
|
-
mockStripe.customers.search.mockResolvedValueOnce({
|
|
3621
|
-
data: [
|
|
3622
|
-
{
|
|
3623
|
-
id: existingUserCustomerId,
|
|
3624
|
-
email: sharedEmail,
|
|
3625
|
-
name: "Existing User Customer",
|
|
3626
|
-
metadata: {
|
|
3627
|
-
customerType: "user",
|
|
3628
|
-
userId: "some-old-user-id",
|
|
3629
|
-
},
|
|
3630
|
-
},
|
|
3631
|
-
],
|
|
3632
|
-
});
|
|
3633
|
-
|
|
3634
|
-
const testOptions = {
|
|
3635
|
-
...stripeOptions,
|
|
3636
|
-
createCustomerOnSignUp: true,
|
|
3637
|
-
} satisfies StripeOptions;
|
|
3638
|
-
|
|
3639
|
-
const { client: testAuthClient, auth: testAuth } = await getTestInstance(
|
|
3640
|
-
{
|
|
3641
|
-
database: memory,
|
|
3642
|
-
plugins: [stripe(testOptions)],
|
|
3643
|
-
},
|
|
3644
|
-
{
|
|
3645
|
-
disableTestUser: true,
|
|
3646
|
-
clientOptions: {
|
|
3647
|
-
plugins: [stripeClient({ subscription: true })],
|
|
3648
|
-
},
|
|
3649
|
-
},
|
|
3650
|
-
);
|
|
3651
|
-
const testCtx = await testAuth.$context;
|
|
3652
|
-
|
|
3653
|
-
vi.clearAllMocks();
|
|
3654
|
-
|
|
3655
|
-
// User signs up - should find their existing customer
|
|
3656
|
-
const userRes = await testAuthClient.signUp.email(
|
|
3657
|
-
{
|
|
3658
|
-
email: sharedEmail,
|
|
3659
|
-
password: "password",
|
|
3660
|
-
name: "User Reclaiming Account",
|
|
3661
|
-
},
|
|
3662
|
-
{ throw: true },
|
|
3663
|
-
);
|
|
3664
|
-
|
|
3665
|
-
// Should search excluding organization customers
|
|
3666
|
-
expect(mockStripe.customers.search).toHaveBeenCalledWith({
|
|
3667
|
-
query: `email:"${sharedEmail}" AND -metadata["customerType"]:"organization"`,
|
|
3668
|
-
limit: 1,
|
|
3669
|
-
});
|
|
3670
|
-
|
|
3671
|
-
// Should NOT create new customer - use existing user customer
|
|
3672
|
-
expect(mockStripe.customers.create).not.toHaveBeenCalled();
|
|
3673
|
-
|
|
3674
|
-
// Verify user has the existing user customer ID
|
|
3675
|
-
const user = await testCtx.adapter.findOne<
|
|
3676
|
-
User & { stripeCustomerId?: string }
|
|
3677
|
-
>({
|
|
3678
|
-
model: "user",
|
|
3679
|
-
where: [{ field: "id", value: userRes.user.id }],
|
|
3680
|
-
});
|
|
3681
|
-
expect(user?.stripeCustomerId).toBe(existingUserCustomerId);
|
|
3682
|
-
});
|
|
3683
|
-
|
|
3684
|
-
it("should create organization customer with customerType metadata", async () => {
|
|
3685
|
-
// Test that organization customers are properly tagged
|
|
3686
|
-
const orgEmail = "org@example.com";
|
|
3687
|
-
const orgId = "org_test_123";
|
|
3688
|
-
|
|
3689
|
-
mockStripe.customers.search.mockResolvedValueOnce({
|
|
3690
|
-
data: [],
|
|
3691
|
-
});
|
|
3692
|
-
|
|
3693
|
-
mockStripe.customers.create.mockResolvedValueOnce({
|
|
3694
|
-
id: "cus_org_new_999",
|
|
3695
|
-
email: orgEmail,
|
|
3696
|
-
});
|
|
3697
|
-
|
|
3698
|
-
const { auth: testAuth } = await getTestInstance(
|
|
3699
|
-
{
|
|
3700
|
-
database: memory,
|
|
3701
|
-
plugins: [
|
|
3702
|
-
organization(),
|
|
3703
|
-
stripe({
|
|
3704
|
-
...stripeOptions,
|
|
3705
|
-
organization: {
|
|
3706
|
-
enabled: true,
|
|
3707
|
-
createCustomerOnOrganizationCreate: true,
|
|
3708
|
-
},
|
|
3709
|
-
}),
|
|
3710
|
-
],
|
|
3711
|
-
},
|
|
3712
|
-
{ disableTestUser: true },
|
|
3713
|
-
);
|
|
3714
|
-
const testCtx = await testAuth.$context;
|
|
3715
|
-
|
|
3716
|
-
vi.clearAllMocks();
|
|
3717
|
-
|
|
3718
|
-
// Create organization
|
|
3719
|
-
await testCtx.adapter.create({
|
|
3720
|
-
model: "organization",
|
|
3721
|
-
data: {
|
|
3722
|
-
id: orgId,
|
|
3723
|
-
name: "Test Organization",
|
|
3724
|
-
slug: "test-org-collision",
|
|
3725
|
-
createdAt: new Date(),
|
|
3726
|
-
},
|
|
3727
|
-
});
|
|
3728
|
-
|
|
3729
|
-
// Manually trigger the organization customer creation flow
|
|
3730
|
-
// by calling the internal function (simulating what hooks do)
|
|
3731
|
-
const stripeClient = stripeOptions.stripeClient;
|
|
3732
|
-
const searchResult = await stripeClient.customers.search({
|
|
3733
|
-
query: `email:'${orgEmail}' AND metadata['customerType']:'organization'`,
|
|
3734
|
-
limit: 1,
|
|
3735
|
-
});
|
|
3736
|
-
|
|
3737
|
-
if (searchResult.data.length === 0) {
|
|
3738
|
-
await stripeClient.customers.create({
|
|
3739
|
-
email: orgEmail,
|
|
3740
|
-
name: "Test Organization",
|
|
3741
|
-
metadata: {
|
|
3742
|
-
customerType: "organization",
|
|
3743
|
-
organizationId: orgId,
|
|
3744
|
-
},
|
|
3745
|
-
});
|
|
3746
|
-
}
|
|
3747
|
-
|
|
3748
|
-
// Verify organization customer was created with correct metadata
|
|
3749
|
-
expect(mockStripe.customers.create).toHaveBeenCalledWith({
|
|
3750
|
-
email: orgEmail,
|
|
3751
|
-
name: "Test Organization",
|
|
3752
|
-
metadata: {
|
|
3753
|
-
customerType: "organization",
|
|
3754
|
-
organizationId: orgId,
|
|
3755
|
-
},
|
|
3756
|
-
});
|
|
3757
|
-
});
|
|
3758
|
-
});
|
|
3759
|
-
|
|
3760
|
-
describe("webhook: cancel_at_period_end cancellation", () => {
|
|
3761
|
-
it("should sync cancelAtPeriodEnd and canceledAt when user cancels via Billing Portal (at_period_end mode)", async () => {
|
|
3762
|
-
const { auth } = await getTestInstance(
|
|
3763
|
-
{
|
|
3764
|
-
database: memory,
|
|
3765
|
-
plugins: [stripe(stripeOptions)],
|
|
3766
|
-
},
|
|
3767
|
-
{ disableTestUser: true },
|
|
3768
|
-
);
|
|
3769
|
-
const ctx = await auth.$context;
|
|
3770
|
-
|
|
3771
|
-
// Setup: Create user and active subscription
|
|
3772
|
-
const { id: userId } = await ctx.adapter.create({
|
|
3773
|
-
model: "user",
|
|
3774
|
-
data: { email: "cancel-period-end@test.com" },
|
|
3775
|
-
});
|
|
3776
|
-
|
|
3777
|
-
const now = Math.floor(Date.now() / 1000);
|
|
3778
|
-
const periodEnd = now + 30 * 24 * 60 * 60;
|
|
3779
|
-
const canceledAt = now;
|
|
3780
|
-
|
|
3781
|
-
const { id: subscriptionId } = await ctx.adapter.create({
|
|
3782
|
-
model: "subscription",
|
|
3783
|
-
data: {
|
|
3784
|
-
referenceId: userId,
|
|
3785
|
-
stripeCustomerId: "cus_cancel_test",
|
|
3786
|
-
stripeSubscriptionId: "sub_cancel_period_end",
|
|
3787
|
-
status: "active",
|
|
3788
|
-
plan: "starter",
|
|
3789
|
-
cancelAtPeriodEnd: false,
|
|
3790
|
-
cancelAt: null,
|
|
3791
|
-
canceledAt: null,
|
|
3792
|
-
},
|
|
3793
|
-
});
|
|
3794
|
-
|
|
3795
|
-
// Simulate: Stripe webhook for cancel_at_period_end
|
|
3796
|
-
const webhookEvent = {
|
|
3797
|
-
type: "customer.subscription.updated",
|
|
3798
|
-
data: {
|
|
3799
|
-
object: {
|
|
3800
|
-
id: "sub_cancel_period_end",
|
|
3801
|
-
customer: "cus_cancel_test",
|
|
3802
|
-
status: "active",
|
|
3803
|
-
cancel_at_period_end: true,
|
|
3804
|
-
cancel_at: null,
|
|
3805
|
-
canceled_at: canceledAt,
|
|
3806
|
-
ended_at: null,
|
|
3807
|
-
items: {
|
|
3808
|
-
data: [
|
|
3809
|
-
{
|
|
3810
|
-
price: { id: "price_starter_123", lookup_key: null },
|
|
3811
|
-
quantity: 1,
|
|
3812
|
-
current_period_start: now,
|
|
3813
|
-
current_period_end: periodEnd,
|
|
3814
|
-
},
|
|
3815
|
-
],
|
|
3816
|
-
},
|
|
3817
|
-
cancellation_details: {
|
|
3818
|
-
reason: "cancellation_requested",
|
|
3819
|
-
comment: "User requested cancellation",
|
|
3820
|
-
},
|
|
3821
|
-
},
|
|
3822
|
-
},
|
|
3823
|
-
};
|
|
3824
|
-
|
|
3825
|
-
const stripeForTest = {
|
|
3826
|
-
...stripeOptions.stripeClient,
|
|
3827
|
-
webhooks: {
|
|
3828
|
-
constructEventAsync: vi.fn().mockResolvedValue(webhookEvent),
|
|
3829
|
-
},
|
|
3830
|
-
};
|
|
3831
|
-
|
|
3832
|
-
const testOptions = {
|
|
3833
|
-
...stripeOptions,
|
|
3834
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
3835
|
-
stripeWebhookSecret: "test_secret",
|
|
3836
|
-
};
|
|
3837
|
-
|
|
3838
|
-
const { auth: webhookAuth } = await getTestInstance(
|
|
3839
|
-
{
|
|
3840
|
-
database: memory,
|
|
3841
|
-
plugins: [stripe(testOptions)],
|
|
3842
|
-
},
|
|
3843
|
-
{ disableTestUser: true },
|
|
3844
|
-
);
|
|
3845
|
-
const webhookCtx = await webhookAuth.$context;
|
|
3846
|
-
|
|
3847
|
-
const response = await webhookAuth.handler(
|
|
3848
|
-
new Request("http://localhost:3000/api/auth/stripe/webhook", {
|
|
3849
|
-
method: "POST",
|
|
3850
|
-
headers: { "stripe-signature": "test_signature" },
|
|
3851
|
-
body: JSON.stringify(webhookEvent),
|
|
3852
|
-
}),
|
|
3853
|
-
);
|
|
3854
|
-
|
|
3855
|
-
expect(response.status).toBe(200);
|
|
3856
|
-
|
|
3857
|
-
const updatedSub = await webhookCtx.adapter.findOne<Subscription>({
|
|
3858
|
-
model: "subscription",
|
|
3859
|
-
where: [{ field: "id", value: subscriptionId }],
|
|
3860
|
-
});
|
|
3861
|
-
|
|
3862
|
-
expect(updatedSub).toMatchObject({
|
|
3863
|
-
status: "active",
|
|
3864
|
-
cancelAtPeriodEnd: true,
|
|
3865
|
-
cancelAt: null,
|
|
3866
|
-
canceledAt: expect.any(Date),
|
|
3867
|
-
endedAt: null,
|
|
3868
|
-
});
|
|
3869
|
-
});
|
|
3870
|
-
|
|
3871
|
-
it("should sync cancelAt when subscription is scheduled to cancel at a specific date", async () => {
|
|
3872
|
-
const { auth } = await getTestInstance(
|
|
3873
|
-
{
|
|
3874
|
-
database: memory,
|
|
3875
|
-
plugins: [stripe(stripeOptions)],
|
|
3876
|
-
},
|
|
3877
|
-
{ disableTestUser: true },
|
|
3878
|
-
);
|
|
3879
|
-
const ctx = await auth.$context;
|
|
3880
|
-
|
|
3881
|
-
const { id: userId } = await ctx.adapter.create({
|
|
3882
|
-
model: "user",
|
|
3883
|
-
data: { email: "cancel-at-date@test.com" },
|
|
3884
|
-
});
|
|
3885
|
-
|
|
3886
|
-
const now = Math.floor(Date.now() / 1000);
|
|
3887
|
-
const cancelAt = now + 15 * 24 * 60 * 60; // Cancel in 15 days
|
|
3888
|
-
const canceledAt = now;
|
|
3889
|
-
|
|
3890
|
-
const { id: subscriptionId } = await ctx.adapter.create({
|
|
3891
|
-
model: "subscription",
|
|
3892
|
-
data: {
|
|
3893
|
-
referenceId: userId,
|
|
3894
|
-
stripeCustomerId: "cus_cancel_at_test",
|
|
3895
|
-
stripeSubscriptionId: "sub_cancel_at_date",
|
|
3896
|
-
status: "active",
|
|
3897
|
-
plan: "starter",
|
|
3898
|
-
cancelAtPeriodEnd: false,
|
|
3899
|
-
cancelAt: null,
|
|
3900
|
-
canceledAt: null,
|
|
3901
|
-
},
|
|
3902
|
-
});
|
|
3903
|
-
|
|
3904
|
-
// Simulate: Dashboard/API cancel with specific date (cancel_at)
|
|
3905
|
-
const webhookEvent = {
|
|
3906
|
-
type: "customer.subscription.updated",
|
|
3907
|
-
data: {
|
|
3908
|
-
object: {
|
|
3909
|
-
id: "sub_cancel_at_date",
|
|
3910
|
-
customer: "cus_cancel_at_test",
|
|
3911
|
-
status: "active",
|
|
3912
|
-
cancel_at_period_end: false,
|
|
3913
|
-
cancel_at: cancelAt,
|
|
3914
|
-
canceled_at: canceledAt,
|
|
3915
|
-
ended_at: null,
|
|
3916
|
-
items: {
|
|
3917
|
-
data: [
|
|
3918
|
-
{
|
|
3919
|
-
price: { id: "price_starter_123", lookup_key: null },
|
|
3920
|
-
quantity: 1,
|
|
3921
|
-
current_period_start: now,
|
|
3922
|
-
current_period_end: now + 30 * 24 * 60 * 60,
|
|
3923
|
-
},
|
|
3924
|
-
],
|
|
3925
|
-
},
|
|
3926
|
-
},
|
|
3927
|
-
},
|
|
3928
|
-
};
|
|
3929
|
-
|
|
3930
|
-
const stripeForTest = {
|
|
3931
|
-
...stripeOptions.stripeClient,
|
|
3932
|
-
webhooks: {
|
|
3933
|
-
constructEventAsync: vi.fn().mockResolvedValue(webhookEvent),
|
|
3934
|
-
},
|
|
3935
|
-
};
|
|
3936
|
-
|
|
3937
|
-
const testOptions = {
|
|
3938
|
-
...stripeOptions,
|
|
3939
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
3940
|
-
stripeWebhookSecret: "test_secret",
|
|
3941
|
-
};
|
|
3942
|
-
|
|
3943
|
-
const { auth: webhookAuth } = await getTestInstance(
|
|
3944
|
-
{
|
|
3945
|
-
database: memory,
|
|
3946
|
-
plugins: [stripe(testOptions)],
|
|
3947
|
-
},
|
|
3948
|
-
{ disableTestUser: true },
|
|
3949
|
-
);
|
|
3950
|
-
const webhookCtx = await webhookAuth.$context;
|
|
3951
|
-
|
|
3952
|
-
const response = await webhookAuth.handler(
|
|
3953
|
-
new Request("http://localhost:3000/api/auth/stripe/webhook", {
|
|
3954
|
-
method: "POST",
|
|
3955
|
-
headers: { "stripe-signature": "test_signature" },
|
|
3956
|
-
body: JSON.stringify(webhookEvent),
|
|
3957
|
-
}),
|
|
3958
|
-
);
|
|
3959
|
-
|
|
3960
|
-
expect(response.status).toBe(200);
|
|
3961
|
-
|
|
3962
|
-
const updatedSub = await webhookCtx.adapter.findOne<Subscription>({
|
|
3963
|
-
model: "subscription",
|
|
3964
|
-
where: [{ field: "id", value: subscriptionId }],
|
|
3965
|
-
});
|
|
3966
|
-
|
|
3967
|
-
expect(updatedSub).toMatchObject({
|
|
3968
|
-
status: "active",
|
|
3969
|
-
cancelAtPeriodEnd: false,
|
|
3970
|
-
cancelAt: expect.any(Date),
|
|
3971
|
-
canceledAt: expect.any(Date),
|
|
3972
|
-
endedAt: null,
|
|
3973
|
-
});
|
|
3974
|
-
|
|
3975
|
-
// Verify the cancelAt date is correct
|
|
3976
|
-
expect(updatedSub!.cancelAt!.getTime()).toBe(cancelAt * 1000);
|
|
3977
|
-
});
|
|
3978
|
-
});
|
|
3979
|
-
|
|
3980
|
-
describe("webhook: immediate cancellation (subscription deleted)", () => {
|
|
3981
|
-
it("should set status=canceled and endedAt when subscription is immediately canceled", async () => {
|
|
3982
|
-
const { auth } = await getTestInstance(
|
|
3983
|
-
{
|
|
3984
|
-
database: memory,
|
|
3985
|
-
plugins: [stripe(stripeOptions)],
|
|
3986
|
-
},
|
|
3987
|
-
{ disableTestUser: true },
|
|
3988
|
-
);
|
|
3989
|
-
const ctx = await auth.$context;
|
|
3990
|
-
|
|
3991
|
-
const { id: userId } = await ctx.adapter.create({
|
|
3992
|
-
model: "user",
|
|
3993
|
-
data: { email: "immediate-cancel@test.com" },
|
|
3994
|
-
});
|
|
3995
|
-
|
|
3996
|
-
const now = Math.floor(Date.now() / 1000);
|
|
3997
|
-
|
|
3998
|
-
const { id: subscriptionId } = await ctx.adapter.create({
|
|
3999
|
-
model: "subscription",
|
|
4000
|
-
data: {
|
|
4001
|
-
referenceId: userId,
|
|
4002
|
-
stripeCustomerId: "cus_immediate_cancel",
|
|
4003
|
-
stripeSubscriptionId: "sub_immediate_cancel",
|
|
4004
|
-
status: "active",
|
|
4005
|
-
plan: "starter",
|
|
4006
|
-
},
|
|
4007
|
-
});
|
|
4008
|
-
|
|
4009
|
-
// Simulate: Immediate cancellation via Billing Portal (mode: immediately) or API
|
|
4010
|
-
const webhookEvent = {
|
|
4011
|
-
type: "customer.subscription.deleted",
|
|
4012
|
-
data: {
|
|
4013
|
-
object: {
|
|
4014
|
-
id: "sub_immediate_cancel",
|
|
4015
|
-
customer: "cus_immediate_cancel",
|
|
4016
|
-
status: "canceled",
|
|
4017
|
-
cancel_at_period_end: false,
|
|
4018
|
-
cancel_at: null,
|
|
4019
|
-
canceled_at: now,
|
|
4020
|
-
ended_at: now,
|
|
4021
|
-
},
|
|
4022
|
-
},
|
|
4023
|
-
};
|
|
4024
|
-
|
|
4025
|
-
const stripeForTest = {
|
|
4026
|
-
...stripeOptions.stripeClient,
|
|
4027
|
-
webhooks: {
|
|
4028
|
-
constructEventAsync: vi.fn().mockResolvedValue(webhookEvent),
|
|
4029
|
-
},
|
|
4030
|
-
};
|
|
4031
|
-
|
|
4032
|
-
const testOptions = {
|
|
4033
|
-
...stripeOptions,
|
|
4034
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
4035
|
-
stripeWebhookSecret: "test_secret",
|
|
4036
|
-
};
|
|
4037
|
-
|
|
4038
|
-
const { auth: webhookAuth } = await getTestInstance(
|
|
4039
|
-
{
|
|
4040
|
-
database: memory,
|
|
4041
|
-
plugins: [stripe(testOptions)],
|
|
4042
|
-
},
|
|
4043
|
-
{ disableTestUser: true },
|
|
4044
|
-
);
|
|
4045
|
-
const webhookCtx = await webhookAuth.$context;
|
|
4046
|
-
|
|
4047
|
-
const response = await webhookAuth.handler(
|
|
4048
|
-
new Request("http://localhost:3000/api/auth/stripe/webhook", {
|
|
4049
|
-
method: "POST",
|
|
4050
|
-
headers: { "stripe-signature": "test_signature" },
|
|
4051
|
-
body: JSON.stringify(webhookEvent),
|
|
4052
|
-
}),
|
|
4053
|
-
);
|
|
4054
|
-
|
|
4055
|
-
expect(response.status).toBe(200);
|
|
4056
|
-
|
|
4057
|
-
const updatedSub = await webhookCtx.adapter.findOne<Subscription>({
|
|
4058
|
-
model: "subscription",
|
|
4059
|
-
where: [{ field: "id", value: subscriptionId }],
|
|
4060
|
-
});
|
|
4061
|
-
|
|
4062
|
-
expect(updatedSub).not.toBeNull();
|
|
4063
|
-
expect(updatedSub!.status).toBe("canceled");
|
|
4064
|
-
expect(updatedSub!.endedAt).not.toBeNull();
|
|
4065
|
-
});
|
|
4066
|
-
|
|
4067
|
-
it("should set endedAt when cancel_at_period_end subscription reaches period end", async () => {
|
|
4068
|
-
const { auth } = await getTestInstance(
|
|
4069
|
-
{
|
|
4070
|
-
database: memory,
|
|
4071
|
-
plugins: [stripe(stripeOptions)],
|
|
4072
|
-
},
|
|
4073
|
-
{ disableTestUser: true },
|
|
4074
|
-
);
|
|
4075
|
-
const ctx = await auth.$context;
|
|
4076
|
-
|
|
4077
|
-
const { id: userId } = await ctx.adapter.create({
|
|
4078
|
-
model: "user",
|
|
4079
|
-
data: { email: "period-end-reached@test.com" },
|
|
4080
|
-
});
|
|
4081
|
-
|
|
4082
|
-
const now = Math.floor(Date.now() / 1000);
|
|
4083
|
-
const canceledAt = now - 30 * 24 * 60 * 60; // Canceled 30 days ago
|
|
4084
|
-
|
|
4085
|
-
const { id: subscriptionId } = await ctx.adapter.create({
|
|
4086
|
-
model: "subscription",
|
|
4087
|
-
data: {
|
|
4088
|
-
referenceId: userId,
|
|
4089
|
-
stripeCustomerId: "cus_period_end_reached",
|
|
4090
|
-
stripeSubscriptionId: "sub_period_end_reached",
|
|
4091
|
-
status: "active",
|
|
4092
|
-
plan: "starter",
|
|
4093
|
-
cancelAtPeriodEnd: true,
|
|
4094
|
-
canceledAt: new Date(canceledAt * 1000),
|
|
4095
|
-
},
|
|
4096
|
-
});
|
|
4097
|
-
|
|
4098
|
-
// Simulate: Period ended, subscription is now deleted
|
|
4099
|
-
const webhookEvent = {
|
|
4100
|
-
type: "customer.subscription.deleted",
|
|
4101
|
-
data: {
|
|
4102
|
-
object: {
|
|
4103
|
-
id: "sub_period_end_reached",
|
|
4104
|
-
customer: "cus_period_end_reached",
|
|
4105
|
-
status: "canceled",
|
|
4106
|
-
cancel_at_period_end: true,
|
|
4107
|
-
cancel_at: null,
|
|
4108
|
-
canceled_at: canceledAt,
|
|
4109
|
-
ended_at: now,
|
|
4110
|
-
},
|
|
4111
|
-
},
|
|
4112
|
-
};
|
|
4113
|
-
|
|
4114
|
-
const stripeForTest = {
|
|
4115
|
-
...stripeOptions.stripeClient,
|
|
4116
|
-
webhooks: {
|
|
4117
|
-
constructEventAsync: vi.fn().mockResolvedValue(webhookEvent),
|
|
4118
|
-
},
|
|
4119
|
-
};
|
|
4120
|
-
|
|
4121
|
-
const testOptions = {
|
|
4122
|
-
...stripeOptions,
|
|
4123
|
-
stripeClient: stripeForTest as unknown as Stripe,
|
|
4124
|
-
stripeWebhookSecret: "test_secret",
|
|
4125
|
-
};
|
|
4126
|
-
|
|
4127
|
-
const { auth: webhookAuth } = await getTestInstance(
|
|
4128
|
-
{
|
|
4129
|
-
database: memory,
|
|
4130
|
-
plugins: [stripe(testOptions)],
|
|
4131
|
-
},
|
|
4132
|
-
{ disableTestUser: true },
|
|
4133
|
-
);
|
|
4134
|
-
const webhookCtx = await webhookAuth.$context;
|
|
4135
|
-
|
|
4136
|
-
const response = await webhookAuth.handler(
|
|
4137
|
-
new Request("http://localhost:3000/api/auth/stripe/webhook", {
|
|
4138
|
-
method: "POST",
|
|
4139
|
-
headers: { "stripe-signature": "test_signature" },
|
|
4140
|
-
body: JSON.stringify(webhookEvent),
|
|
4141
|
-
}),
|
|
4142
|
-
);
|
|
4143
|
-
|
|
4144
|
-
expect(response.status).toBe(200);
|
|
4145
|
-
|
|
4146
|
-
const updatedSub = await webhookCtx.adapter.findOne<Subscription>({
|
|
4147
|
-
model: "subscription",
|
|
4148
|
-
where: [{ field: "id", value: subscriptionId }],
|
|
4149
|
-
});
|
|
4150
|
-
|
|
4151
|
-
expect(updatedSub).not.toBeNull();
|
|
4152
|
-
expect(updatedSub!.status).toBe("canceled");
|
|
4153
|
-
expect(updatedSub!.cancelAtPeriodEnd).toBe(true);
|
|
4154
|
-
expect(updatedSub!.endedAt).not.toBeNull();
|
|
4155
|
-
|
|
4156
|
-
// endedAt should be the actual termination time (now), not the cancellation request time
|
|
4157
|
-
expect(updatedSub!.endedAt!.getTime()).toBe(now * 1000);
|
|
4158
|
-
});
|
|
4159
|
-
});
|
|
4160
|
-
|
|
4161
|
-
describe("restore subscription", () => {
|
|
4162
|
-
it("should clear cancelAtPeriodEnd when restoring a cancel_at_period_end subscription", async () => {
|
|
4163
|
-
const { client, auth, sessionSetter } = await getTestInstance(
|
|
4164
|
-
{
|
|
4165
|
-
database: memory,
|
|
4166
|
-
plugins: [stripe(stripeOptions)],
|
|
4167
|
-
},
|
|
4168
|
-
{
|
|
4169
|
-
disableTestUser: true,
|
|
4170
|
-
clientOptions: {
|
|
4171
|
-
plugins: [stripeClient({ subscription: true })],
|
|
4172
|
-
},
|
|
4173
|
-
},
|
|
4174
|
-
);
|
|
4175
|
-
const ctx = await auth.$context;
|
|
4176
|
-
|
|
4177
|
-
const userRes = await client.signUp.email(
|
|
4178
|
-
{
|
|
4179
|
-
email: "restore-period-end@test.com",
|
|
4180
|
-
password: "password",
|
|
4181
|
-
name: "Test",
|
|
4182
|
-
},
|
|
4183
|
-
{ throw: true },
|
|
4184
|
-
);
|
|
4185
|
-
|
|
4186
|
-
const headers = new Headers();
|
|
4187
|
-
await client.signIn.email(
|
|
4188
|
-
{ email: "restore-period-end@test.com", password: "password" },
|
|
4189
|
-
{ throw: true, onSuccess: sessionSetter(headers) },
|
|
4190
|
-
);
|
|
4191
|
-
|
|
4192
|
-
// Create subscription scheduled to cancel at period end
|
|
4193
|
-
await ctx.adapter.create({
|
|
4194
|
-
model: "subscription",
|
|
4195
|
-
data: {
|
|
4196
|
-
referenceId: userRes.user.id,
|
|
4197
|
-
stripeCustomerId: "cus_restore_test",
|
|
4198
|
-
stripeSubscriptionId: "sub_restore_period_end",
|
|
4199
|
-
status: "active",
|
|
4200
|
-
plan: "starter",
|
|
4201
|
-
cancelAtPeriodEnd: true,
|
|
4202
|
-
cancelAt: null,
|
|
4203
|
-
canceledAt: new Date(),
|
|
4204
|
-
},
|
|
4205
|
-
});
|
|
4206
|
-
|
|
4207
|
-
mockStripe.subscriptions.list.mockResolvedValueOnce({
|
|
4208
|
-
data: [
|
|
4209
|
-
{
|
|
4210
|
-
id: "sub_restore_period_end",
|
|
4211
|
-
status: "active",
|
|
4212
|
-
cancel_at_period_end: true,
|
|
4213
|
-
cancel_at: null,
|
|
4214
|
-
},
|
|
4215
|
-
],
|
|
4216
|
-
});
|
|
4217
|
-
|
|
4218
|
-
mockStripe.subscriptions.update.mockResolvedValueOnce({
|
|
4219
|
-
id: "sub_restore_period_end",
|
|
4220
|
-
status: "active",
|
|
4221
|
-
cancel_at_period_end: false,
|
|
4222
|
-
cancel_at: null,
|
|
4223
|
-
});
|
|
4224
|
-
|
|
4225
|
-
const restoreRes = await client.subscription.restore({
|
|
4226
|
-
fetchOptions: { headers },
|
|
4227
|
-
});
|
|
4228
|
-
|
|
4229
|
-
expect(restoreRes.data).toBeDefined();
|
|
4230
|
-
|
|
4231
|
-
// Verify Stripe was called with correct params (cancel_at_period_end: false)
|
|
4232
|
-
expect(mockStripe.subscriptions.update).toHaveBeenCalledWith(
|
|
4233
|
-
"sub_restore_period_end",
|
|
4234
|
-
{ cancel_at_period_end: false },
|
|
4235
|
-
);
|
|
4236
|
-
|
|
4237
|
-
const updatedSub = await ctx.adapter.findOne<Subscription>({
|
|
4238
|
-
model: "subscription",
|
|
4239
|
-
where: [{ field: "referenceId", value: userRes.user.id }],
|
|
4240
|
-
});
|
|
4241
|
-
|
|
4242
|
-
expect(updatedSub).toMatchObject({
|
|
4243
|
-
cancelAtPeriodEnd: false,
|
|
4244
|
-
cancelAt: null,
|
|
4245
|
-
canceledAt: null,
|
|
4246
|
-
});
|
|
4247
|
-
});
|
|
4248
|
-
|
|
4249
|
-
it("should clear cancelAt when restoring a cancel_at (specific date) subscription", async () => {
|
|
4250
|
-
const { client, auth, sessionSetter } = await getTestInstance(
|
|
4251
|
-
{
|
|
4252
|
-
database: memory,
|
|
4253
|
-
plugins: [stripe(stripeOptions)],
|
|
4254
|
-
},
|
|
4255
|
-
{
|
|
4256
|
-
disableTestUser: true,
|
|
4257
|
-
clientOptions: {
|
|
4258
|
-
plugins: [stripeClient({ subscription: true })],
|
|
4259
|
-
},
|
|
4260
|
-
},
|
|
4261
|
-
);
|
|
4262
|
-
const ctx = await auth.$context;
|
|
4263
|
-
|
|
4264
|
-
const userRes = await client.signUp.email(
|
|
4265
|
-
{
|
|
4266
|
-
email: "restore-cancel-at@test.com",
|
|
4267
|
-
password: "password",
|
|
4268
|
-
name: "Test",
|
|
4269
|
-
},
|
|
4270
|
-
{ throw: true },
|
|
4271
|
-
);
|
|
4272
|
-
|
|
4273
|
-
const headers = new Headers();
|
|
4274
|
-
await client.signIn.email(
|
|
4275
|
-
{ email: "restore-cancel-at@test.com", password: "password" },
|
|
4276
|
-
{ throw: true, onSuccess: sessionSetter(headers) },
|
|
4277
|
-
);
|
|
4278
|
-
|
|
4279
|
-
const cancelAt = new Date(Date.now() + 15 * 24 * 60 * 60 * 1000);
|
|
4280
|
-
|
|
4281
|
-
// Create subscription scheduled to cancel at specific date
|
|
4282
|
-
await ctx.adapter.create({
|
|
4283
|
-
model: "subscription",
|
|
4284
|
-
data: {
|
|
4285
|
-
referenceId: userRes.user.id,
|
|
4286
|
-
stripeCustomerId: "cus_restore_cancel_at",
|
|
4287
|
-
stripeSubscriptionId: "sub_restore_cancel_at",
|
|
4288
|
-
status: "active",
|
|
4289
|
-
plan: "starter",
|
|
4290
|
-
cancelAtPeriodEnd: false,
|
|
4291
|
-
cancelAt: cancelAt,
|
|
4292
|
-
canceledAt: new Date(),
|
|
4293
|
-
},
|
|
4294
|
-
});
|
|
4295
|
-
|
|
4296
|
-
mockStripe.subscriptions.list.mockResolvedValueOnce({
|
|
4297
|
-
data: [
|
|
4298
|
-
{
|
|
4299
|
-
id: "sub_restore_cancel_at",
|
|
4300
|
-
status: "active",
|
|
4301
|
-
cancel_at_period_end: false,
|
|
4302
|
-
cancel_at: Math.floor(cancelAt.getTime() / 1000),
|
|
4303
|
-
},
|
|
4304
|
-
],
|
|
4305
|
-
});
|
|
4306
|
-
|
|
4307
|
-
mockStripe.subscriptions.update.mockResolvedValueOnce({
|
|
4308
|
-
id: "sub_restore_cancel_at",
|
|
4309
|
-
status: "active",
|
|
4310
|
-
cancel_at_period_end: false,
|
|
4311
|
-
cancel_at: null,
|
|
4312
|
-
});
|
|
4313
|
-
|
|
4314
|
-
const restoreRes = await client.subscription.restore({
|
|
4315
|
-
fetchOptions: { headers },
|
|
4316
|
-
});
|
|
4317
|
-
|
|
4318
|
-
expect(restoreRes.data).toBeDefined();
|
|
4319
|
-
|
|
4320
|
-
// Verify Stripe was called with correct params (cancel_at: "" to clear)
|
|
4321
|
-
expect(mockStripe.subscriptions.update).toHaveBeenCalledWith(
|
|
4322
|
-
"sub_restore_cancel_at",
|
|
4323
|
-
{ cancel_at: "" },
|
|
4324
|
-
);
|
|
4325
|
-
|
|
4326
|
-
const updatedSub = await ctx.adapter.findOne<Subscription>({
|
|
4327
|
-
model: "subscription",
|
|
4328
|
-
where: [{ field: "referenceId", value: userRes.user.id }],
|
|
4329
|
-
});
|
|
4330
|
-
|
|
4331
|
-
expect(updatedSub).toMatchObject({
|
|
4332
|
-
cancelAtPeriodEnd: false,
|
|
4333
|
-
cancelAt: null,
|
|
4334
|
-
canceledAt: null,
|
|
4335
|
-
});
|
|
4336
|
-
});
|
|
4337
|
-
});
|
|
4338
|
-
|
|
4339
|
-
describe("cancel subscription fallback (missed webhook)", () => {
|
|
4340
|
-
it("should sync from Stripe when cancel request fails because subscription is already canceled", async () => {
|
|
4341
|
-
const { client, auth, sessionSetter } = await getTestInstance(
|
|
4342
|
-
{
|
|
4343
|
-
database: memory,
|
|
4344
|
-
plugins: [stripe(stripeOptions)],
|
|
4345
|
-
},
|
|
4346
|
-
{
|
|
4347
|
-
disableTestUser: true,
|
|
4348
|
-
clientOptions: {
|
|
4349
|
-
plugins: [stripeClient({ subscription: true })],
|
|
4350
|
-
},
|
|
4351
|
-
},
|
|
4352
|
-
);
|
|
4353
|
-
const ctx = await auth.$context;
|
|
4354
|
-
|
|
4355
|
-
const userRes = await client.signUp.email(
|
|
4356
|
-
{
|
|
4357
|
-
email: "missed-webhook@test.com",
|
|
4358
|
-
password: "password",
|
|
4359
|
-
name: "Test",
|
|
4360
|
-
},
|
|
4361
|
-
{ throw: true },
|
|
4362
|
-
);
|
|
4363
|
-
|
|
4364
|
-
const headers = new Headers();
|
|
4365
|
-
await client.signIn.email(
|
|
4366
|
-
{ email: "missed-webhook@test.com", password: "password" },
|
|
4367
|
-
{ throw: true, onSuccess: sessionSetter(headers) },
|
|
4368
|
-
);
|
|
4369
|
-
|
|
4370
|
-
const now = Math.floor(Date.now() / 1000);
|
|
4371
|
-
const cancelAt = now + 15 * 24 * 60 * 60;
|
|
4372
|
-
|
|
4373
|
-
// Create subscription in DB (not synced - missed webhook)
|
|
4374
|
-
const { id: subscriptionId } = await ctx.adapter.create({
|
|
4375
|
-
model: "subscription",
|
|
4376
|
-
data: {
|
|
4377
|
-
referenceId: userRes.user.id,
|
|
4378
|
-
stripeCustomerId: "cus_missed_webhook",
|
|
4379
|
-
stripeSubscriptionId: "sub_missed_webhook",
|
|
4380
|
-
status: "active",
|
|
4381
|
-
plan: "starter",
|
|
4382
|
-
cancelAtPeriodEnd: false, // DB thinks it's not canceling
|
|
4383
|
-
cancelAt: null,
|
|
4384
|
-
canceledAt: null,
|
|
4385
|
-
},
|
|
4386
|
-
});
|
|
4387
|
-
|
|
4388
|
-
// Stripe has the subscription already scheduled to cancel with cancel_at
|
|
4389
|
-
mockStripe.subscriptions.list.mockResolvedValueOnce({
|
|
4390
|
-
data: [
|
|
4391
|
-
{
|
|
4392
|
-
id: "sub_missed_webhook",
|
|
4393
|
-
status: "active",
|
|
4394
|
-
cancel_at_period_end: false,
|
|
4395
|
-
cancel_at: cancelAt,
|
|
4396
|
-
},
|
|
4397
|
-
],
|
|
4398
|
-
});
|
|
4399
|
-
|
|
4400
|
-
// Billing portal returns error because subscription is already set to cancel
|
|
4401
|
-
mockStripe.billingPortal.sessions.create.mockRejectedValueOnce(
|
|
4402
|
-
new Error("This subscription is already set to be canceled"),
|
|
4403
|
-
);
|
|
4404
|
-
|
|
4405
|
-
// When fallback kicks in, it retrieves from Stripe
|
|
4406
|
-
mockStripe.subscriptions.retrieve.mockResolvedValueOnce({
|
|
4407
|
-
id: "sub_missed_webhook",
|
|
4408
|
-
status: "active",
|
|
4409
|
-
cancel_at_period_end: false,
|
|
4410
|
-
cancel_at: cancelAt,
|
|
4411
|
-
canceled_at: now,
|
|
4412
|
-
});
|
|
4413
|
-
|
|
4414
|
-
// Try to cancel - should fail but trigger sync
|
|
4415
|
-
const cancelRes = await client.subscription.cancel({
|
|
4416
|
-
returnUrl: "/account",
|
|
4417
|
-
fetchOptions: { headers },
|
|
4418
|
-
});
|
|
4419
|
-
|
|
4420
|
-
// Should have error because portal creation failed
|
|
4421
|
-
expect(cancelRes.error).toBeDefined();
|
|
4422
|
-
|
|
4423
|
-
// But DB should now be synced with Stripe's actual state
|
|
4424
|
-
const updatedSub = await ctx.adapter.findOne<Subscription>({
|
|
4425
|
-
model: "subscription",
|
|
4426
|
-
where: [{ field: "id", value: subscriptionId }],
|
|
4427
|
-
});
|
|
4428
|
-
|
|
4429
|
-
expect(updatedSub).toMatchObject({
|
|
4430
|
-
cancelAtPeriodEnd: false,
|
|
4431
|
-
cancelAt: expect.any(Date),
|
|
4432
|
-
canceledAt: expect.any(Date),
|
|
4433
|
-
});
|
|
4434
|
-
|
|
4435
|
-
// Verify it's the correct cancel_at date from Stripe
|
|
4436
|
-
expect(updatedSub!.cancelAt!.getTime()).toBe(cancelAt * 1000);
|
|
4437
|
-
});
|
|
4438
|
-
});
|
|
4439
|
-
|
|
4440
|
-
describe("referenceMiddleware", () => {
|
|
4441
|
-
describe("referenceMiddleware - user subscription", () => {
|
|
4442
|
-
it("should pass when no explicit referenceId is provided", async () => {
|
|
4443
|
-
const { client, sessionSetter } = await getTestInstance(
|
|
4444
|
-
{
|
|
4445
|
-
plugins: [stripe(stripeOptions)],
|
|
4446
|
-
},
|
|
4447
|
-
{
|
|
4448
|
-
disableTestUser: true,
|
|
4449
|
-
clientOptions: {
|
|
4450
|
-
plugins: [stripeClient({ subscription: true })],
|
|
4451
|
-
},
|
|
4452
|
-
},
|
|
4453
|
-
);
|
|
4454
|
-
|
|
4455
|
-
await client.signUp.email(testUser, { throw: true });
|
|
4456
|
-
const headers = new Headers();
|
|
4457
|
-
await client.signIn.email(testUser, {
|
|
4458
|
-
throw: true,
|
|
4459
|
-
onSuccess: sessionSetter(headers),
|
|
4460
|
-
});
|
|
4461
|
-
|
|
4462
|
-
const res = await client.subscription.upgrade({
|
|
4463
|
-
plan: "starter",
|
|
4464
|
-
fetchOptions: { headers },
|
|
4465
|
-
});
|
|
4466
|
-
|
|
4467
|
-
expect(res.error).toBeNull();
|
|
4468
|
-
expect(res.data?.url).toBeDefined();
|
|
4469
|
-
});
|
|
4470
|
-
|
|
4471
|
-
it("should pass when referenceId equals user id", async () => {
|
|
4472
|
-
const { client, sessionSetter } = await getTestInstance(
|
|
4473
|
-
{
|
|
4474
|
-
plugins: [stripe(stripeOptions)],
|
|
4475
|
-
},
|
|
4476
|
-
{
|
|
4477
|
-
disableTestUser: true,
|
|
4478
|
-
clientOptions: {
|
|
4479
|
-
plugins: [stripeClient({ subscription: true })],
|
|
4480
|
-
},
|
|
4481
|
-
},
|
|
4482
|
-
);
|
|
4483
|
-
|
|
4484
|
-
const signUpRes = await client.signUp.email(
|
|
4485
|
-
{ ...testUser, email: "ref-test-2@example.com" },
|
|
4486
|
-
{ throw: true },
|
|
4487
|
-
);
|
|
4488
|
-
const headers = new Headers();
|
|
4489
|
-
await client.signIn.email(
|
|
4490
|
-
{ ...testUser, email: "ref-test-2@example.com" },
|
|
4491
|
-
{
|
|
4492
|
-
throw: true,
|
|
4493
|
-
onSuccess: sessionSetter(headers),
|
|
4494
|
-
},
|
|
4495
|
-
);
|
|
4496
|
-
|
|
4497
|
-
const res = await client.subscription.upgrade({
|
|
4498
|
-
plan: "starter",
|
|
4499
|
-
referenceId: signUpRes.user.id,
|
|
4500
|
-
fetchOptions: { headers },
|
|
4501
|
-
});
|
|
4502
|
-
|
|
4503
|
-
expect(res.error).toBeNull();
|
|
4504
|
-
expect(res.data?.url).toBeDefined();
|
|
4505
|
-
});
|
|
4506
|
-
|
|
4507
|
-
it("should reject when authorizeReference is not defined but other referenceId is provided", async () => {
|
|
4508
|
-
const { client, sessionSetter } = await getTestInstance(
|
|
4509
|
-
{
|
|
4510
|
-
plugins: [stripe(stripeOptions)],
|
|
4511
|
-
},
|
|
4512
|
-
{
|
|
4513
|
-
disableTestUser: true,
|
|
4514
|
-
clientOptions: {
|
|
4515
|
-
plugins: [stripeClient({ subscription: true })],
|
|
4516
|
-
},
|
|
4517
|
-
},
|
|
4518
|
-
);
|
|
4519
|
-
|
|
4520
|
-
await client.signUp.email(
|
|
4521
|
-
{ ...testUser, email: "ref-test-3@example.com" },
|
|
4522
|
-
{ throw: true },
|
|
4523
|
-
);
|
|
4524
|
-
const headers = new Headers();
|
|
4525
|
-
await client.signIn.email(
|
|
4526
|
-
{ ...testUser, email: "ref-test-3@example.com" },
|
|
4527
|
-
{
|
|
4528
|
-
throw: true,
|
|
4529
|
-
onSuccess: sessionSetter(headers),
|
|
4530
|
-
},
|
|
4531
|
-
);
|
|
4532
|
-
|
|
4533
|
-
const res = await client.subscription.upgrade({
|
|
4534
|
-
plan: "starter",
|
|
4535
|
-
referenceId: "some-other-id",
|
|
4536
|
-
fetchOptions: { headers },
|
|
4537
|
-
});
|
|
4538
|
-
|
|
4539
|
-
expect(res.error?.code).toBe("REFERENCE_ID_NOT_ALLOWED");
|
|
4540
|
-
});
|
|
4541
|
-
|
|
4542
|
-
it("should reject when authorizeReference returns false", async () => {
|
|
4543
|
-
const stripeOptionsWithAuth: StripeOptions = {
|
|
4544
|
-
...stripeOptions,
|
|
4545
|
-
subscription: {
|
|
4546
|
-
...stripeOptions.subscription,
|
|
4547
|
-
authorizeReference: async () => false,
|
|
4548
|
-
},
|
|
4549
|
-
};
|
|
4550
|
-
|
|
4551
|
-
const { client, sessionSetter } = await getTestInstance(
|
|
4552
|
-
{
|
|
4553
|
-
plugins: [stripe(stripeOptionsWithAuth)],
|
|
4554
|
-
},
|
|
4555
|
-
{
|
|
4556
|
-
disableTestUser: true,
|
|
4557
|
-
clientOptions: {
|
|
4558
|
-
plugins: [stripeClient({ subscription: true })],
|
|
4559
|
-
},
|
|
4560
|
-
},
|
|
4561
|
-
);
|
|
4562
|
-
|
|
4563
|
-
await client.signUp.email(
|
|
4564
|
-
{ ...testUser, email: "ref-test-4@example.com" },
|
|
4565
|
-
{ throw: true },
|
|
4566
|
-
);
|
|
4567
|
-
const headers = new Headers();
|
|
4568
|
-
await client.signIn.email(
|
|
4569
|
-
{ ...testUser, email: "ref-test-4@example.com" },
|
|
4570
|
-
{
|
|
4571
|
-
throw: true,
|
|
4572
|
-
onSuccess: sessionSetter(headers),
|
|
4573
|
-
},
|
|
4574
|
-
);
|
|
4575
|
-
|
|
4576
|
-
const res = await client.subscription.upgrade({
|
|
4577
|
-
plan: "starter",
|
|
4578
|
-
referenceId: "some-other-id",
|
|
4579
|
-
fetchOptions: { headers },
|
|
4580
|
-
});
|
|
4581
|
-
|
|
4582
|
-
expect(res.error?.code).toBe("UNAUTHORIZED");
|
|
4583
|
-
});
|
|
4584
|
-
|
|
4585
|
-
it("should pass when authorizeReference returns true", async () => {
|
|
4586
|
-
const stripeOptionsWithAuth: StripeOptions = {
|
|
4587
|
-
...stripeOptions,
|
|
4588
|
-
subscription: {
|
|
4589
|
-
...stripeOptions.subscription,
|
|
4590
|
-
authorizeReference: async () => true,
|
|
4591
|
-
},
|
|
4592
|
-
};
|
|
4593
|
-
|
|
4594
|
-
const { client, sessionSetter } = await getTestInstance(
|
|
4595
|
-
{
|
|
4596
|
-
plugins: [stripe(stripeOptionsWithAuth)],
|
|
4597
|
-
},
|
|
4598
|
-
{
|
|
4599
|
-
disableTestUser: true,
|
|
4600
|
-
clientOptions: {
|
|
4601
|
-
plugins: [stripeClient({ subscription: true })],
|
|
4602
|
-
},
|
|
4603
|
-
},
|
|
4604
|
-
);
|
|
4605
|
-
|
|
4606
|
-
await client.signUp.email(
|
|
4607
|
-
{ ...testUser, email: "ref-test-5@example.com" },
|
|
4608
|
-
{ throw: true },
|
|
4609
|
-
);
|
|
4610
|
-
const headers = new Headers();
|
|
4611
|
-
await client.signIn.email(
|
|
4612
|
-
{ ...testUser, email: "ref-test-5@example.com" },
|
|
4613
|
-
{
|
|
4614
|
-
throw: true,
|
|
4615
|
-
onSuccess: sessionSetter(headers),
|
|
4616
|
-
},
|
|
4617
|
-
);
|
|
4618
|
-
|
|
4619
|
-
const res = await client.subscription.upgrade({
|
|
4620
|
-
plan: "starter",
|
|
4621
|
-
referenceId: "some-other-id",
|
|
4622
|
-
fetchOptions: { headers },
|
|
4623
|
-
});
|
|
4624
|
-
|
|
4625
|
-
expect(res.error).toBeNull();
|
|
4626
|
-
expect(res.data?.url).toBeDefined();
|
|
4627
|
-
});
|
|
4628
|
-
});
|
|
4629
|
-
|
|
4630
|
-
describe("referenceMiddleware - organization subscription", () => {
|
|
4631
|
-
it("should reject when authorizeReference is not defined", async () => {
|
|
4632
|
-
const { client, sessionSetter } = await getTestInstance(
|
|
4633
|
-
{
|
|
4634
|
-
plugins: [stripe(stripeOptions)],
|
|
4635
|
-
},
|
|
4636
|
-
{
|
|
4637
|
-
disableTestUser: true,
|
|
4638
|
-
clientOptions: {
|
|
4639
|
-
plugins: [stripeClient({ subscription: true })],
|
|
4640
|
-
},
|
|
4641
|
-
},
|
|
4642
|
-
);
|
|
4643
|
-
|
|
4644
|
-
await client.signUp.email(
|
|
4645
|
-
{ ...testUser, email: "org-test-1@example.com" },
|
|
4646
|
-
{ throw: true },
|
|
4647
|
-
);
|
|
4648
|
-
const headers = new Headers();
|
|
4649
|
-
await client.signIn.email(
|
|
4650
|
-
{ ...testUser, email: "org-test-1@example.com" },
|
|
4651
|
-
{
|
|
4652
|
-
throw: true,
|
|
4653
|
-
onSuccess: sessionSetter(headers),
|
|
4654
|
-
},
|
|
4655
|
-
);
|
|
4656
|
-
|
|
4657
|
-
const res = await client.subscription.upgrade({
|
|
4658
|
-
plan: "starter",
|
|
4659
|
-
customerType: "organization",
|
|
4660
|
-
referenceId: "org_123",
|
|
4661
|
-
fetchOptions: { headers },
|
|
4662
|
-
});
|
|
4663
|
-
|
|
4664
|
-
expect(res.error?.code).toBe("ORGANIZATION_SUBSCRIPTION_NOT_ENABLED");
|
|
4665
|
-
});
|
|
4666
|
-
|
|
4667
|
-
it("should reject when no referenceId or activeOrganizationId", async () => {
|
|
4668
|
-
const stripeOptionsWithAuth: StripeOptions = {
|
|
4669
|
-
...stripeOptions,
|
|
4670
|
-
subscription: {
|
|
4671
|
-
...stripeOptions.subscription,
|
|
4672
|
-
authorizeReference: async () => true,
|
|
4673
|
-
},
|
|
4674
|
-
};
|
|
4675
|
-
|
|
4676
|
-
const { client, sessionSetter } = await getTestInstance(
|
|
4677
|
-
{
|
|
4678
|
-
plugins: [stripe(stripeOptionsWithAuth)],
|
|
4679
|
-
},
|
|
4680
|
-
{
|
|
4681
|
-
disableTestUser: true,
|
|
4682
|
-
clientOptions: {
|
|
4683
|
-
plugins: [stripeClient({ subscription: true })],
|
|
4684
|
-
},
|
|
4685
|
-
},
|
|
4686
|
-
);
|
|
4687
|
-
|
|
4688
|
-
await client.signUp.email(
|
|
4689
|
-
{ ...testUser, email: "org-test-2@example.com" },
|
|
4690
|
-
{ throw: true },
|
|
4691
|
-
);
|
|
4692
|
-
const headers = new Headers();
|
|
4693
|
-
await client.signIn.email(
|
|
4694
|
-
{ ...testUser, email: "org-test-2@example.com" },
|
|
4695
|
-
{
|
|
4696
|
-
throw: true,
|
|
4697
|
-
onSuccess: sessionSetter(headers),
|
|
4698
|
-
},
|
|
4699
|
-
);
|
|
4700
|
-
|
|
4701
|
-
const res = await client.subscription.upgrade({
|
|
4702
|
-
plan: "starter",
|
|
4703
|
-
customerType: "organization",
|
|
4704
|
-
fetchOptions: { headers },
|
|
4705
|
-
});
|
|
4706
|
-
|
|
4707
|
-
expect(res.error?.code).toBe("ORGANIZATION_REFERENCE_ID_REQUIRED");
|
|
4708
|
-
});
|
|
4709
|
-
|
|
4710
|
-
it("should reject when authorizeReference returns false", async () => {
|
|
4711
|
-
const stripeOptionsWithAuth: StripeOptions = {
|
|
4712
|
-
...stripeOptions,
|
|
4713
|
-
subscription: {
|
|
4714
|
-
...stripeOptions.subscription,
|
|
4715
|
-
authorizeReference: async () => false,
|
|
4716
|
-
},
|
|
4717
|
-
};
|
|
4718
|
-
|
|
4719
|
-
const { client, sessionSetter } = await getTestInstance(
|
|
4720
|
-
{
|
|
4721
|
-
plugins: [stripe(stripeOptionsWithAuth)],
|
|
4722
|
-
},
|
|
4723
|
-
{
|
|
4724
|
-
disableTestUser: true,
|
|
4725
|
-
clientOptions: {
|
|
4726
|
-
plugins: [stripeClient({ subscription: true })],
|
|
4727
|
-
},
|
|
4728
|
-
},
|
|
4729
|
-
);
|
|
4730
|
-
|
|
4731
|
-
await client.signUp.email(
|
|
4732
|
-
{ ...testUser, email: "org-test-3@example.com" },
|
|
4733
|
-
{ throw: true },
|
|
4734
|
-
);
|
|
4735
|
-
const headers = new Headers();
|
|
4736
|
-
await client.signIn.email(
|
|
4737
|
-
{ ...testUser, email: "org-test-3@example.com" },
|
|
4738
|
-
{
|
|
4739
|
-
throw: true,
|
|
4740
|
-
onSuccess: sessionSetter(headers),
|
|
4741
|
-
},
|
|
4742
|
-
);
|
|
4743
|
-
|
|
4744
|
-
const res = await client.subscription.upgrade({
|
|
4745
|
-
plan: "starter",
|
|
4746
|
-
customerType: "organization",
|
|
4747
|
-
referenceId: "org_123",
|
|
4748
|
-
fetchOptions: { headers },
|
|
4749
|
-
});
|
|
4750
|
-
|
|
4751
|
-
expect(res.error?.code).toBe("UNAUTHORIZED");
|
|
4752
|
-
});
|
|
4753
|
-
|
|
4754
|
-
it("should pass when authorizeReference returns true", async () => {
|
|
4755
|
-
const stripeOptionsWithAuth: StripeOptions = {
|
|
4756
|
-
...stripeOptions,
|
|
4757
|
-
organization: {
|
|
4758
|
-
enabled: true,
|
|
4759
|
-
},
|
|
4760
|
-
subscription: {
|
|
4761
|
-
...stripeOptions.subscription,
|
|
4762
|
-
authorizeReference: async () => true,
|
|
4763
|
-
},
|
|
4764
|
-
};
|
|
4765
|
-
|
|
4766
|
-
const { client, sessionSetter } = await getTestInstance(
|
|
4767
|
-
{
|
|
4768
|
-
plugins: [stripe(stripeOptionsWithAuth)],
|
|
4769
|
-
},
|
|
4770
|
-
{
|
|
4771
|
-
disableTestUser: true,
|
|
4772
|
-
clientOptions: {
|
|
4773
|
-
plugins: [stripeClient({ subscription: true })],
|
|
4774
|
-
},
|
|
4775
|
-
},
|
|
4776
|
-
);
|
|
4777
|
-
|
|
4778
|
-
await client.signUp.email(
|
|
4779
|
-
{ ...testUser, email: "org-test-4@example.com" },
|
|
4780
|
-
{ throw: true },
|
|
4781
|
-
);
|
|
4782
|
-
const headers = new Headers();
|
|
4783
|
-
await client.signIn.email(
|
|
4784
|
-
{ ...testUser, email: "org-test-4@example.com" },
|
|
4785
|
-
{
|
|
4786
|
-
throw: true,
|
|
4787
|
-
onSuccess: sessionSetter(headers),
|
|
4788
|
-
},
|
|
4789
|
-
);
|
|
4790
|
-
|
|
4791
|
-
const res = await client.subscription.upgrade({
|
|
4792
|
-
plan: "starter",
|
|
4793
|
-
customerType: "organization",
|
|
4794
|
-
referenceId: "org_123",
|
|
4795
|
-
fetchOptions: { headers },
|
|
4796
|
-
});
|
|
4797
|
-
|
|
4798
|
-
// Should pass middleware but may fail later due to org not existing
|
|
4799
|
-
// We're testing middleware authorization, not the full flow
|
|
4800
|
-
expect(res.error?.code).not.toBe(
|
|
4801
|
-
"ORGANIZATION_SUBSCRIPTION_NOT_ENABLED",
|
|
4802
|
-
);
|
|
4803
|
-
expect(res.error?.code).not.toBe("UNAUTHORIZED");
|
|
4804
|
-
});
|
|
4805
|
-
});
|
|
4806
|
-
});
|
|
4807
|
-
});
|