diff --git a/packages/stripe/src/stripe.test.ts b/packages/stripe/src/stripe.test.ts index 7e6c5cd523..24af1ebc0b 100644 --- a/packages/stripe/src/stripe.test.ts +++ b/packages/stripe/src/stripe.test.ts @@ -1,11 +1,7 @@ -import type { GenericEndpointContext } from "@better-auth/core"; import { runWithEndpointContext } from "@better-auth/core/context"; import type { Auth, User } from "better-auth"; -import { betterAuth } from "better-auth"; import { memoryAdapter } from "better-auth/adapters/memory"; -import { createAuthClient } from "better-auth/client"; -import { setCookieToHeader } from "better-auth/cookies"; -import { bearer } from "better-auth/plugins"; +import { getTestInstance } from "better-auth/test"; import type Stripe from "stripe"; import { beforeEach, describe, expect, expectTypeOf, it, vi } from "vitest"; import type { StripePlugin } from "."; @@ -50,10 +46,12 @@ describe("stripe type", () => { expectTypeOf().toBeFunction(); expectTypeOf().toBeFunction(); expectTypeOf().toBeFunction(); + expectTypeOf().toBeFunction(); + expectTypeOf().toBeFunction(); }); }); -describe("stripe", async () => { +describe("stripe", () => { const mockStripe = { prices: { list: vi.fn().mockResolvedValue({ data: [{ id: "price_lookup_123" }] }), @@ -95,17 +93,7 @@ describe("stripe", async () => { constructEventAsync: vi.fn(), }, }; - const _stripe = mockStripe as unknown as Stripe; - const data = { - user: [], - session: [], - verification: [], - account: [], - customer: [], - subscription: [], - }; - const memory = memoryAdapter(data); const stripeOptions = { stripeClient: _stripe, stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!, @@ -126,36 +114,20 @@ describe("stripe", async () => { ], }, } satisfies StripeOptions; - const auth = betterAuth({ - database: memory, - baseURL: "http://localhost:3000", - // database: new Database(":memory:"), - emailAndPassword: { - enabled: true, - }, - plugins: [stripe(stripeOptions)], - }); - const ctx = await auth.$context; - const authClient = createAuthClient({ - baseURL: "http://localhost:3000", - plugins: [ - bearer(), - stripeClient({ - subscription: true, - }), - ], - fetchOptions: { - customFetchImpl: async (url, init) => { - return auth.handler(new Request(url, init)); - }, - }, - }); const testUser = { email: "test@email.com", password: "password", name: "Test User", }; + const data = { + user: [], + session: [], + verification: [], + account: [], + customer: [], + subscription: [], + }; beforeEach(() => { data.user = []; @@ -168,8 +140,24 @@ describe("stripe", async () => { vi.clearAllMocks(); }); + const memory = memoryAdapter(data); + it("should create a customer on sign up", async () => { - const userRes = await authClient.signUp.email(testUser, { + const { client, auth } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); + const ctx = await auth.$context; + + const userRes = await client.signUp.email(testUser, { throw: true, }); const res = await ctx.adapter.findOne({ @@ -188,17 +176,31 @@ describe("stripe", async () => { }); it("should create a subscription", async () => { - const userRes = await authClient.signUp.email(testUser, { + const { client, auth, sessionSetter } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); + const ctx = await auth.$context; + + const userRes = await client.signUp.email(testUser, { throw: true, }); const headers = new Headers(); - await authClient.signIn.email(testUser, { + await client.signIn.email(testUser, { throw: true, - onSuccess: setCookieToHeader(headers), + onSuccess: sessionSetter(headers), }); - const res = await authClient.subscription.upgrade({ + const res = await client.subscription.upgrade({ plan: "starter", fetchOptions: { headers, @@ -228,19 +230,33 @@ describe("stripe", async () => { }); it("should not allow cross-user subscriptionId operations (upgrade/cancel/restore)", async () => { + const { client, auth, sessionSetter } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); + const ctx = await auth.$context; + const userA = { email: "user-a@email.com", password: "password", name: "User A", }; - const userARes = await authClient.signUp.email(userA, { throw: true }); + const userARes = await client.signUp.email(userA, { throw: true }); const userAHeaders = new Headers(); - await authClient.signIn.email(userA, { + await client.signIn.email(userA, { throw: true, - onSuccess: setCookieToHeader(userAHeaders), + onSuccess: sessionSetter(userAHeaders), }); - await authClient.subscription.upgrade({ + await client.subscription.upgrade({ plan: "starter", fetchOptions: { headers: userAHeaders }, }); @@ -256,11 +272,11 @@ describe("stripe", async () => { password: "password", name: "User B", }; - await authClient.signUp.email(userB, { throw: true }); + await client.signUp.email(userB, { throw: true }); const userBHeaders = new Headers(); - await authClient.signIn.email(userB, { + await client.signIn.email(userB, { throw: true, - onSuccess: setCookieToHeader(userBHeaders), + onSuccess: sessionSetter(userBHeaders), }); mockStripe.checkout.sessions.create.mockClear(); @@ -268,7 +284,7 @@ describe("stripe", async () => { mockStripe.subscriptions.list.mockClear(); mockStripe.subscriptions.update.mockClear(); - const upgradeRes = await authClient.subscription.upgrade({ + const upgradeRes = await client.subscription.upgrade({ plan: "premium", subscriptionId: userASub!.id, fetchOptions: { headers: userBHeaders }, @@ -314,7 +330,21 @@ describe("stripe", async () => { }); it("should list active subscriptions", async () => { - const userRes = await authClient.signUp.email( + const { client, auth, sessionSetter } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); + const ctx = await auth.$context; + + const userRes = await client.signUp.email( { ...testUser, email: "list-test@email.com", @@ -326,18 +356,18 @@ describe("stripe", async () => { const userId = userRes.user.id; const headers = new Headers(); - await authClient.signIn.email( + await client.signIn.email( { ...testUser, email: "list-test@email.com", }, { throw: true, - onSuccess: setCookieToHeader(headers), + onSuccess: sessionSetter(headers), }, ); - const listRes = await authClient.subscription.list({ + const listRes = await client.subscription.list({ fetchOptions: { headers, }, @@ -345,13 +375,13 @@ describe("stripe", async () => { expect(Array.isArray(listRes.data)).toBe(true); - await authClient.subscription.upgrade({ + await client.subscription.upgrade({ plan: "starter", fetchOptions: { headers, }, }); - const listBeforeActive = await authClient.subscription.list({ + const listBeforeActive = await client.subscription.list({ fetchOptions: { headers, }, @@ -370,7 +400,7 @@ describe("stripe", async () => { }, ], }); - const listAfterRes = await authClient.subscription.list({ + const listAfterRes = await client.subscription.list({ fetchOptions: { headers, }, @@ -379,13 +409,24 @@ describe("stripe", async () => { }); it("should handle subscription webhook events", async () => { - const { id: testReferenceId } = await ctx.adapter.create({ + const { auth: testAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + }, + ); + const testCtx = await testAuth.$context; + + const { id: testReferenceId } = await testCtx.adapter.create({ model: "user", data: { email: "test@email.com", }, }); - const { id: testSubscriptionId } = await ctx.adapter.create({ + const { id: testSubscriptionId } = await testCtx.adapter.create({ model: "subscription", data: { referenceId: testReferenceId, @@ -442,16 +483,17 @@ describe("stripe", async () => { stripeWebhookSecret: "test_secret", }; - const testAuth = betterAuth({ - baseURL: "http://localhost:3000", - database: memory, - emailAndPassword: { - enabled: true, + const { auth: webhookTestAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptions)], }, - plugins: [stripe(testOptions)], - }); + { + disableTestUser: true, + }, + ); - const testCtx = await testAuth.$context; + const webhookTestCtx = await webhookTestAuth.$context; const mockRequest = new Request( "http://localhost:3000/api/auth/stripe/webhook", @@ -463,18 +505,19 @@ describe("stripe", async () => { body: JSON.stringify(mockCheckoutSessionEvent), }, ); - const response = await testAuth.handler(mockRequest); + const response = await webhookTestAuth.handler(mockRequest); expect(response.status).toBe(200); - const updatedSubscription = await testCtx.adapter.findOne({ - model: "subscription", - where: [ - { - field: "id", - value: testSubscriptionId, - }, - ], - }); + const updatedSubscription = + await webhookTestCtx.adapter.findOne({ + model: "subscription", + where: [ + { + field: "id", + value: testSubscriptionId, + }, + ], + }); expect(updatedSubscription).toMatchObject({ id: testSubscriptionId, @@ -486,13 +529,24 @@ describe("stripe", async () => { }); it("should handle subscription webhook events with trial", async () => { - const { id: testReferenceId } = await ctx.adapter.create({ + const { auth: testAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + }, + ); + const testCtx = await testAuth.$context; + + const { id: testReferenceId } = await testCtx.adapter.create({ model: "user", data: { email: "test@email.com", }, }); - const { id: testSubscriptionId } = await ctx.adapter.create({ + const { id: testSubscriptionId } = await testCtx.adapter.create({ model: "subscription", data: { referenceId: testReferenceId, @@ -551,16 +605,17 @@ describe("stripe", async () => { stripeWebhookSecret: "test_secret", }; - const testAuth = betterAuth({ - baseURL: "http://localhost:3000", - database: memory, - emailAndPassword: { - enabled: true, + const { auth: webhookTestAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptions)], }, - plugins: [stripe(testOptions)], - }); + { + disableTestUser: true, + }, + ); - const testCtx = await testAuth.$context; + const webhookTestCtx = await webhookTestAuth.$context; const mockRequest = new Request( "http://localhost:3000/api/auth/stripe/webhook", @@ -572,18 +627,19 @@ describe("stripe", async () => { body: JSON.stringify(mockCheckoutSessionEvent), }, ); - const response = await testAuth.handler(mockRequest); + const response = await webhookTestAuth.handler(mockRequest); expect(response.status).toBe(200); - const updatedSubscription = await testCtx.adapter.findOne({ - model: "subscription", - where: [ - { - field: "id", - value: testSubscriptionId, - }, - ], - }); + const updatedSubscription = + await webhookTestCtx.adapter.findOne({ + model: "subscription", + where: [ + { + field: "id", + value: testSubscriptionId, + }, + ], + }); expect(updatedSubscription).toMatchObject({ id: testSubscriptionId, @@ -596,17 +652,28 @@ describe("stripe", async () => { }); }); - const { id: userId } = await ctx.adapter.create({ - model: "user", - data: { - email: "delete-test@email.com", - }, - }); - it("should handle subscription deletion webhook", async () => { + const { auth: testAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + }, + ); + const testCtx = await testAuth.$context; + + const { id: userId } = await testCtx.adapter.create({ + model: "user", + data: { + email: "delete-test@email.com", + }, + }); + const subId = "test_sub_delete"; - await ctx.adapter.create({ + await testCtx.adapter.create({ model: "subscription", data: { referenceId: userId, @@ -617,7 +684,7 @@ describe("stripe", async () => { }, }); - const subscription = await ctx.adapter.findOne({ + const subscription = await testCtx.adapter.findOne({ model: "subscription", where: [ { @@ -661,14 +728,17 @@ describe("stripe", async () => { stripeWebhookSecret: "test_secret", }; - const testAuth = betterAuth({ - baseURL: "http://localhost:3000", - emailAndPassword: { - enabled: true, + const { auth: webhookTestAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptions)], }, - database: memory, - plugins: [stripe(testOptions)], - }); + { + disableTestUser: true, + }, + ); + + const webhookTestCtx = await webhookTestAuth.$context; const mockRequest = new Request( "http://localhost:3000/api/auth/stripe/webhook", @@ -681,24 +751,43 @@ describe("stripe", async () => { }, ); - const response = await testAuth.handler(mockRequest); + const response = await webhookTestAuth.handler(mockRequest); expect(response.status).toBe(200); if (subscription) { - const updatedSubscription = await ctx.adapter.findOne({ - model: "subscription", - where: [ - { - field: "id", - value: subscription.id, - }, - ], - }); + const updatedSubscription = + await webhookTestCtx.adapter.findOne({ + model: "subscription", + where: [ + { + field: "id", + value: subscription.id, + }, + ], + }); expect(updatedSubscription?.status).toBe("canceled"); } }); it("should execute subscription event handlers", async () => { + const { auth: testAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + }, + ); + const testCtx = await testAuth.$context; + + const { id: userId } = await testCtx.adapter.create({ + model: "user", + data: { + email: "event-handler-test@email.com", + }, + }); + const onSubscriptionComplete = vi.fn(); const onSubscriptionUpdate = vi.fn(); const onSubscriptionCancel = vi.fn(); @@ -755,14 +844,19 @@ describe("stripe", async () => { stripeClient: mockStripeForEvents as unknown as Stripe, }; - const eventTestAuth = betterAuth({ - baseURL: "http://localhost:3000", - database: memory, - emailAndPassword: { enabled: true }, - plugins: [stripe(eventTestOptions)], - }); + const { auth: eventTestAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(eventTestOptions)], + }, + { + disableTestUser: true, + }, + ); - const { id: testSubscriptionId } = await ctx.adapter.create({ + const eventTestCtx = await eventTestAuth.$context; + + const { id: testSubscriptionId } = await eventTestCtx.adapter.create({ model: "subscription", data: { referenceId: userId, @@ -945,25 +1039,6 @@ describe("stripe", async () => { it("should return updated subscription in onSubscriptionUpdate callback", async () => { const onSubscriptionUpdate = vi.fn(); - const { id: testReferenceId } = await ctx.adapter.create({ - model: "user", - data: { - email: "update-callback@email.com", - }, - }); - - const { id: testSubscriptionId } = await ctx.adapter.create({ - model: "subscription", - data: { - referenceId: testReferenceId, - stripeCustomerId: "cus_update_test", - stripeSubscriptionId: "sub_update_test", - status: "active", - plan: "starter", - seats: 1, - }, - }); - // Simulate subscription update event (e.g., seat change from 1 to 5) const updateEvent = { type: "customer.subscription.updated", @@ -1006,13 +1081,35 @@ describe("stripe", async () => { }, } as unknown as StripeOptions; - const testAuth = betterAuth({ - baseURL: "http://localhost:3000", - database: memory, - emailAndPassword: { - enabled: true, + const { auth: testAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptions)], + }, + { + disableTestUser: true, + }, + ); + + const ctx = await testAuth.$context; + + const { id: testReferenceId } = await ctx.adapter.create({ + model: "user", + data: { + email: "update-callback@email.com", + }, + }); + + const { id: testSubscriptionId } = await ctx.adapter.create({ + model: "subscription", + data: { + referenceId: testReferenceId, + stripeCustomerId: "cus_update_test", + stripeSubscriptionId: "sub_update_test", + status: "active", + plan: "starter", + seats: 1, }, - plugins: [stripe(testOptions)], }); const mockRequest = new Request( @@ -1050,7 +1147,21 @@ describe("stripe", async () => { }); it("should allow seat upgrades for the same plan", async () => { - const userRes = await authClient.signUp.email( + const { client, auth, sessionSetter } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); + const ctx = await auth.$context; + + const userRes = await client.signUp.email( { ...testUser, email: "seat-upgrade@email.com", @@ -1061,18 +1172,18 @@ describe("stripe", async () => { ); const headers = new Headers(); - await authClient.signIn.email( + await client.signIn.email( { ...testUser, email: "seat-upgrade@email.com", }, { throw: true, - onSuccess: setCookieToHeader(headers), + onSuccess: sessionSetter(headers), }, ); - await authClient.subscription.upgrade({ + await client.subscription.upgrade({ plan: "starter", seats: 1, fetchOptions: { @@ -1093,7 +1204,7 @@ describe("stripe", async () => { ], }); - const upgradeRes = await authClient.subscription.upgrade({ + const upgradeRes = await client.subscription.upgrade({ plan: "starter", seats: 5, fetchOptions: { @@ -1105,7 +1216,21 @@ describe("stripe", async () => { }); it("should prevent duplicate subscriptions with same plan and same seats", async () => { - const userRes = await authClient.signUp.email( + const { client, auth, sessionSetter } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); + const ctx = await auth.$context; + + const userRes = await client.signUp.email( { ...testUser, email: "duplicate-prevention@email.com", @@ -1116,18 +1241,18 @@ describe("stripe", async () => { ); const headers = new Headers(); - await authClient.signIn.email( + await client.signIn.email( { ...testUser, email: "duplicate-prevention@email.com", }, { throw: true, - onSuccess: setCookieToHeader(headers), + onSuccess: sessionSetter(headers), }, ); - await authClient.subscription.upgrade({ + await client.subscription.upgrade({ plan: "starter", seats: 3, fetchOptions: { @@ -1149,7 +1274,7 @@ describe("stripe", async () => { ], }); - const upgradeRes = await authClient.subscription.upgrade({ + const upgradeRes = await client.subscription.upgrade({ plan: "starter", seats: 3, fetchOptions: { @@ -1162,21 +1287,34 @@ describe("stripe", async () => { }); it("should only call Stripe customers.create once for signup and upgrade", async () => { - await authClient.signUp.email( + const { client, sessionSetter } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); + + await client.signUp.email( { ...testUser, email: "single-create@email.com" }, { throw: true }, ); const headers = new Headers(); - await authClient.signIn.email( + await client.signIn.email( { ...testUser, email: "single-create@email.com" }, { throw: true, - onSuccess: setCookieToHeader(headers), + onSuccess: sessionSetter(headers), }, ); - await authClient.subscription.upgrade({ + await client.subscription.upgrade({ plan: "starter", fetchOptions: { headers }, }); @@ -1185,7 +1323,20 @@ describe("stripe", async () => { }); it("should create billing portal session", async () => { - await authClient.signUp.email( + const { client, sessionSetter } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); + + await client.signUp.email( { ...testUser, email: "billing-portal@email.com", @@ -1196,17 +1347,17 @@ describe("stripe", async () => { ); const headers = new Headers(); - await authClient.signIn.email( + await client.signIn.email( { ...testUser, email: "billing-portal@email.com", }, { throw: true, - onSuccess: setCookieToHeader(headers), + onSuccess: sessionSetter(headers), }, ); - const billingPortalRes = await authClient.subscription.billingPortal({ + const billingPortalRes = await client.subscription.billingPortal({ returnUrl: "/dashboard", fetchOptions: { headers, @@ -1233,36 +1384,37 @@ describe("stripe", async () => { }, } as unknown as StripeOptions; - const testAuth = betterAuth({ - baseURL: "http://localhost:3000", - database: memory, - emailAndPassword: { enabled: true }, - plugins: [stripe(testOptions)], - }); + const { + auth: testAuth, + client: testClient, + sessionSetter: testSessionSetter, + } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptions)], + }, + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); const testCtx = await testAuth.$context; - const testAuthClient = createAuthClient({ - baseURL: "http://localhost:3000", - plugins: [bearer(), stripeClient({ subscription: true })], - fetchOptions: { - customFetchImpl: async (url, init) => - testAuth.handler(new Request(url, init)), - }, - }); - // Sign up and sign in the user - const userRes = await testAuthClient.signUp.email( + const userRes = await testClient.signUp.email( { ...testUser, email: "org-ref@email.com" }, { throw: true }, ); const headers = new Headers(); - await testAuthClient.signIn.email( + await testClient.signIn.email( { ...testUser, email: "org-ref@email.com" }, - { throw: true, onSuccess: setCookieToHeader(headers) }, + { throw: true, onSuccess: testSessionSetter(headers) }, ); // Create a personal subscription (referenceId = user id) - await testAuthClient.subscription.upgrade({ + await testClient.subscription.upgrade({ plan: "starter", fetchOptions: { headers }, }); @@ -1301,14 +1453,12 @@ describe("stripe", async () => { }); // Attempt to upgrade using an org referenceId - const upgradeRes = await testAuthClient.subscription.upgrade({ + const upgradeRes = await testClient.subscription.upgrade({ plan: "starter", referenceId: orgId, fetchOptions: { headers }, }); - console.log(upgradeRes); - - // // It should NOT go through billing portal (which would update the personal sub) + // It should NOT go through billing portal (which would update the personal sub) expect(mockStripe.billingPortal.sessions.create).not.toHaveBeenCalled(); expect(upgradeRes.data?.url).toBeDefined(); @@ -1330,23 +1480,37 @@ describe("stripe", async () => { }); it("should prevent multiple free trials for the same user", async () => { + const { client, auth, sessionSetter } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); + const ctx = await auth.$context; + // Create a user - const userRes = await authClient.signUp.email( + const userRes = await client.signUp.email( { ...testUser, email: "trial-prevention@email.com" }, { throw: true }, ); const headers = new Headers(); - await authClient.signIn.email( + await client.signIn.email( { ...testUser, email: "trial-prevention@email.com" }, { throw: true, - onSuccess: setCookieToHeader(headers), + onSuccess: sessionSetter(headers), }, ); // First subscription with trial - const firstUpgradeRes = await authClient.subscription.upgrade({ + const firstUpgradeRes = await client.subscription.upgrade({ plan: "starter", fetchOptions: { headers }, }); @@ -1384,7 +1548,7 @@ describe("stripe", async () => { }); // Try to subscribe again - should NOT get a trial - const secondUpgradeRes = await authClient.subscription.upgrade({ + const secondUpgradeRes = await client.subscription.upgrade({ plan: "starter", fetchOptions: { headers }, }); @@ -1418,18 +1582,32 @@ describe("stripe", async () => { // Reset mocks for this test vi.clearAllMocks(); + const { client, auth, sessionSetter } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); + const ctx = await auth.$context; + // Create a user - const userRes = await authClient.signUp.email( + const userRes = await client.signUp.email( { ...testUser, email: "upgrade-existing@email.com" }, { throw: true }, ); const headers = new Headers(); - await authClient.signIn.email( + await client.signIn.email( { ...testUser, email: "upgrade-existing@email.com" }, { throw: true, - onSuccess: setCookieToHeader(headers), + onSuccess: sessionSetter(headers), }, ); @@ -1439,7 +1617,7 @@ describe("stripe", async () => { }); // First create a starter subscription - await authClient.subscription.upgrade({ + await client.subscription.upgrade({ plan: "starter", fetchOptions: { headers }, }); @@ -1511,7 +1689,7 @@ describe("stripe", async () => { mockStripe.billingPortal.sessions.create.mockClear(); // Now upgrade to premium plan - should use billing portal to update existing subscription - const upgradeRes = await authClient.subscription.upgrade({ + const upgradeRes = await client.subscription.upgrade({ plan: "premium", fetchOptions: { headers }, }); @@ -1550,23 +1728,37 @@ describe("stripe", async () => { }); it("should prevent multiple free trials across different plans", async () => { + const { client, auth, sessionSetter } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); + const ctx = await auth.$context; + // Create a user - const userRes = await authClient.signUp.email( + const userRes = await client.signUp.email( { ...testUser, email: "cross-plan-trial@email.com" }, { throw: true }, ); const headers = new Headers(); - await authClient.signIn.email( + await client.signIn.email( { ...testUser, email: "cross-plan-trial@email.com" }, { throw: true, - onSuccess: setCookieToHeader(headers), + onSuccess: sessionSetter(headers), }, ); // First subscription with trial on starter plan - const firstUpgradeRes = await authClient.subscription.upgrade({ + const firstUpgradeRes = await client.subscription.upgrade({ plan: "starter", fetchOptions: { headers }, }); @@ -1604,7 +1796,7 @@ describe("stripe", async () => { }); // Try to subscribe to a different plan - should NOT get a trial - const secondUpgradeRes = await authClient.subscription.upgrade({ + const secondUpgradeRes = await client.subscription.upgrade({ plan: "premium", fetchOptions: { headers }, }); @@ -1643,6 +1835,20 @@ describe("stripe", async () => { }); it("should update stripe customer email when user email changes", async () => { + const { client, auth } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); + const ctx = await auth.$context; + // Setup mock for customer retrieve and update mockStripe.customers.retrieve = vi.fn().mockResolvedValue({ id: "cus_mock123", @@ -1655,7 +1861,7 @@ describe("stripe", async () => { }); // Sign up a user - const userRes = await authClient.signUp.email(testUser, { + const userRes = await client.signUp.email(testUser, { throw: true, }); @@ -1685,11 +1891,14 @@ describe("stripe", async () => { }); // Update the user's email using internal adapter (which triggers hooks) - const endpointCtx = { context: ctx } as GenericEndpointContext; - await runWithEndpointContext(endpointCtx, () => - ctx.internalAdapter.updateUserByEmail(testUser.email, { - email: "newemail@example.com", - }), + await runWithEndpointContext( + { + context: ctx, + }, + () => + ctx.internalAdapter.updateUserByEmail(testUser.email, { + email: "newemail@example.com", + }), ); // Verify that Stripe customer.retrieve was called @@ -1713,26 +1922,21 @@ describe("stripe", async () => { getCustomerCreateParams: getCustomerCreateParamsMock, } satisfies StripeOptions; - const testAuth = betterAuth({ - database: memory, - baseURL: "http://localhost:3000", - emailAndPassword: { - enabled: true, + const { client: testClient } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptions)], }, - plugins: [stripe(testOptions)], - }); - - const testAuthClient = createAuthClient({ - baseURL: "http://localhost:3000", - plugins: [bearer(), stripeClient({ subscription: true })], - fetchOptions: { - customFetchImpl: async (url, init) => - testAuth.handler(new Request(url, init)), + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, }, - }); + ); // Sign up a user - const userRes = await testAuthClient.signUp.email( + const userRes = await testClient.signUp.email( { email: "custom-params@email.com", password: "password", @@ -1785,23 +1989,18 @@ describe("stripe", async () => { getCustomerCreateParams: getCustomerCreateParamsMock, } satisfies StripeOptions; - const testAuth = betterAuth({ - database: memory, - baseURL: "http://localhost:3000", - emailAndPassword: { - enabled: true, + const { client: testAuthClient } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptions)], }, - plugins: [stripe(testOptions)], - }); - - const testAuthClient = createAuthClient({ - baseURL: "http://localhost:3000", - plugins: [bearer(), stripeClient({ subscription: true })], - fetchOptions: { - customFetchImpl: async (url, init) => - testAuth.handler(new Request(url, init)), + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, }, - }); + ); // Sign up a user await testAuthClient.signUp.email( @@ -1849,23 +2048,18 @@ describe("stripe", async () => { getCustomerCreateParams: getCustomerCreateParamsMock, } satisfies StripeOptions; - const testAuth = betterAuth({ - database: memory, - baseURL: "http://localhost:3000", - emailAndPassword: { - enabled: true, + const { client: testAuthClient } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptions)], }, - plugins: [stripe(testOptions)], - }); - - const testAuthClient = createAuthClient({ - baseURL: "http://localhost:3000", - plugins: [bearer(), stripeClient({ subscription: true })], - fetchOptions: { - customFetchImpl: async (url, init) => - testAuth.handler(new Request(url, init)), + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, }, - }); + ); // Sign up a user const userRes = await testAuthClient.signUp.email( @@ -1903,23 +2097,18 @@ describe("stripe", async () => { // No getCustomerCreateParams provided } satisfies StripeOptions; - const testAuth = betterAuth({ - database: memory, - baseURL: "http://localhost:3000", - emailAndPassword: { - enabled: true, + const { client: testAuthClient } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptions)], }, - plugins: [stripe(testOptions)], - }); - - const testAuthClient = createAuthClient({ - baseURL: "http://localhost:3000", - plugins: [bearer(), stripeClient({ subscription: true })], - fetchOptions: { - customFetchImpl: async (url, init) => - testAuth.handler(new Request(url, init)), + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, }, - }); + ); // Sign up a user const userRes = await testAuthClient.signUp.email( @@ -1960,12 +2149,15 @@ describe("stripe", async () => { stripeWebhookSecret: "test_secret", }; - const testAuth = betterAuth({ - baseURL: "http://localhost:3000", - database: memory, - emailAndPassword: { enabled: true }, - plugins: [stripe(testOptions)], - }); + const { auth: testAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptions)], + }, + { + disableTestUser: true, + }, + ); const mockRequest = new Request( "http://localhost:3000/api/auth/stripe/webhook", @@ -1985,12 +2177,15 @@ describe("stripe", async () => { }); it("should reject webhook request without stripe-signature header", async () => { - const testAuth = betterAuth({ - baseURL: "http://localhost:3000", - database: memory, - emailAndPassword: { enabled: true }, - plugins: [stripe(stripeOptions)], - }); + const { auth: testAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + }, + ); const mockRequest = new Request( "http://localhost:3000/api/auth/stripe/webhook", @@ -2023,12 +2218,15 @@ describe("stripe", async () => { stripeWebhookSecret: "test_secret", }; - const testAuth = betterAuth({ - baseURL: "http://localhost:3000", - database: memory, - emailAndPassword: { enabled: true }, - plugins: [stripe(testOptions)], - }); + const { auth: testAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptions)], + }, + { + disableTestUser: true, + }, + ); const mockRequest = new Request( "http://localhost:3000/api/auth/stripe/webhook", @@ -2086,14 +2284,18 @@ describe("stripe", async () => { }, }; - const testAuth = betterAuth({ - baseURL: "http://localhost:3000", - database: memory, - emailAndPassword: { enabled: true }, - plugins: [stripe(testOptions as StripeOptions)], - }); + const { auth: testAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptions as StripeOptions)], + }, + { + disableTestUser: true, + }, + ); + const testCtx = await testAuth.$context; - await ctx.adapter.create({ + await testCtx.adapter.create({ model: "subscription", data: { referenceId: "user_123", @@ -2165,17 +2367,28 @@ describe("stripe", async () => { stripeWebhookSecret: "test_secret_async", }; - const testAuth = betterAuth({ - baseURL: "http://localhost:3000", - database: memory, - emailAndPassword: { enabled: true }, - plugins: [stripe(testOptions)], + const { auth: testAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptions)], + }, + { + disableTestUser: true, + }, + ); + const testCtx = await testAuth.$context; + + const { id: testUserId } = await testCtx.adapter.create({ + model: "user", + data: { + email: "async-test@email.com", + }, }); - await ctx.adapter.create({ + await testCtx.adapter.create({ model: "subscription", data: { - referenceId: userId, + referenceId: testUserId, stripeCustomerId: "cus_test_async", stripeSubscriptionId: "sub_test_async", status: "incomplete", @@ -2231,12 +2444,15 @@ describe("stripe", async () => { stripeWebhookSecret: "test_secret_params", }; - const testAuth = betterAuth({ - baseURL: "http://localhost:3000", - database: memory, - emailAndPassword: { enabled: true }, - plugins: [stripe(testOptions)], - }); + const { auth: testAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptions)], + }, + { + disableTestUser: true, + }, + ); const mockRequest = new Request( "http://localhost:3000/api/auth/stripe/webhook", @@ -2307,17 +2523,28 @@ describe("stripe", async () => { stripeWebhookSecret: "test_secret_v18", }; - const testAuth = betterAuth({ - baseURL: "http://localhost:3000", - database: memory, - emailAndPassword: { enabled: true }, - plugins: [stripe(testOptions)], + const { auth: testAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptions)], + }, + { + disableTestUser: true, + }, + ); + const testCtx = await testAuth.$context; + + const { id: testUserId } = await testCtx.adapter.create({ + model: "user", + data: { + email: "v18-test@email.com", + }, }); - await ctx.adapter.create({ + await testCtx.adapter.create({ model: "subscription", data: { - referenceId: userId, + referenceId: testUserId, stripeCustomerId: "cus_test_v18", stripeSubscriptionId: "sub_test_v18", status: "incomplete", @@ -2376,30 +2603,31 @@ describe("stripe", async () => { }, ]; - const testAuth = betterAuth({ - baseURL: "http://localhost:3000", - database: memory, - emailAndPassword: { enabled: true }, - plugins: [ - stripe({ - ...stripeOptions, - subscription: { - enabled: true, - plans: flexiblePlans, - }, - }), - ], - }); - - const testClient = createAuthClient({ - baseURL: "http://localhost:3000", - plugins: [bearer(), stripeClient({ subscription: true })], - fetchOptions: { - customFetchImpl: async (url, init) => { - return testAuth.handler(new Request(url, init)); + const { + client: testClient, + auth: testAuth, + sessionSetter: testSessionSetter, + } = await getTestInstance( + { + database: memory, + plugins: [ + stripe({ + ...stripeOptions, + subscription: { + enabled: true, + plans: flexiblePlans, + }, + }), + ], + }, + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], }, }, - }); + ); + const testCtx = await testAuth.$context; // Create user and sign in const headers = new Headers(); @@ -2407,18 +2635,18 @@ describe("stripe", async () => { { email: "limits@test.com", password: "password", name: "Test" }, { throw: true }, ); - const userId = userRes.user.id; + const limitUserId = userRes.user.id; await testClient.signIn.email( { email: "limits@test.com", password: "password" }, - { throw: true, onSuccess: setCookieToHeader(headers) }, + { throw: true, onSuccess: testSessionSetter(headers) }, ); // Create subscription - await ctx.adapter.create({ + await testCtx.adapter.create({ model: "subscription", data: { - referenceId: userId, + referenceId: limitUserId, stripeCustomerId: "cus_limits_test", stripeSubscriptionId: "sub_limits_test", status: "active", @@ -2477,21 +2705,19 @@ describe("stripe", async () => { createCustomerOnSignUp: true, } satisfies StripeOptions; - const testAuth = betterAuth({ - database: memory, - baseURL: "http://localhost:3000", - emailAndPassword: { enabled: true }, - plugins: [stripe(testOptionsWithHook)], - }); - - const testAuthClient = createAuthClient({ - baseURL: "http://localhost:3000", - plugins: [bearer(), stripeClient({ subscription: true })], - fetchOptions: { - customFetchImpl: async (url, init) => - testAuth.handler(new Request(url, init)), + const { client: testAuthClient, auth: testAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptionsWithHook)], }, - }); + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); + const testCtx = await testAuth.$context; vi.clearAllMocks(); @@ -2515,7 +2741,7 @@ describe("stripe", async () => { expect(mockStripe.customers.create).not.toHaveBeenCalled(); // Verify user has the EXISTING Stripe customer ID (not new duplicate) - const user = await ctx.adapter.findOne< + const user = await testCtx.adapter.findOne< User & { stripeCustomerId?: string } >({ model: "user", @@ -2541,21 +2767,19 @@ describe("stripe", async () => { createCustomerOnSignUp: true, } satisfies StripeOptions; - const testAuth = betterAuth({ - database: memory, - baseURL: "http://localhost:3000", - emailAndPassword: { enabled: true }, - plugins: [stripe(testOptionsWithHook)], - }); - - const testAuthClient = createAuthClient({ - baseURL: "http://localhost:3000", - plugins: [bearer(), stripeClient({ subscription: true })], - fetchOptions: { - customFetchImpl: async (url, init) => - testAuth.handler(new Request(url, init)), + const { client: testAuthClient, auth: testAuth } = await getTestInstance( + { + database: memory, + plugins: [stripe(testOptionsWithHook)], }, - }); + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); + const testCtx = await testAuth.$context; vi.clearAllMocks(); @@ -2586,7 +2810,7 @@ describe("stripe", async () => { }); // Verify user has the new Stripe customer ID - const user = await ctx.adapter.findOne< + const user = await testCtx.adapter.findOne< User & { stripeCustomerId?: string } >({ model: "user",