diff --git a/packages/stripe/src/__test__/plugin.test.ts b/packages/stripe/src/__test__/plugin.test.ts index 5149be3fb..4a6c69be7 100644 --- a/packages/stripe/src/__test__/plugin.test.ts +++ b/packages/stripe/src/__test__/plugin.test.ts @@ -43,18 +43,6 @@ describe("stripePlugin — missing configuration", async () => { "Missing stripe configuration. Did you forget to pass it to the stripe plugin?", ); }); - - it("throws when register is called without options even if fastify.config.stripe is set", async () => { - const app = Fastify({ logger: { level: "silent" } }); - app.decorate("config", { - stripe: createStripeConfig({ enablePaymentWebhook: true }), - } as unknown as FastifyInstance["config"]); - - await expect(app.register(plugin)).rejects.toThrow( - "Missing stripe configuration. Did you forget to pass it to the stripe plugin?", - ); - await app.close(); - }); }); describe("stripePlugin — configuration present", async () => { @@ -65,6 +53,7 @@ describe("stripePlugin — configuration present", async () => { beforeEach(() => { vi.clearAllMocks(); fastify = Fastify({ logger: { level: "silent" } }); + fastify.decorate("config", {} as unknown as FastifyInstance["config"]); }); afterEach(async () => { @@ -74,20 +63,16 @@ describe("stripePlugin — configuration present", async () => { it("logs 'Registering Stripe plugin' at info level when config is passed", async () => { const infoSpy = vi.spyOn(fastify.log, "info"); - await fastify.register( - plugin, - createStripeConfig({ enablePaymentWebhook: false }), - ); + fastify.config.stripe = createStripeConfig({ enablePaymentWebhook: false }); + await fastify.register(plugin); await fastify.ready(); expect(infoSpy).toHaveBeenCalledWith("Registering Stripe plugin"); }); it("does not register the webhook route when enablePaymentWebhook is false", async () => { - await fastify.register( - plugin, - createStripeConfig({ enablePaymentWebhook: false }), - ); + fastify.config.stripe = createStripeConfig({ enablePaymentWebhook: false }); + await fastify.register(plugin); await fastify.ready(); expect(fastify.hasRoute({ method: "POST", url: "/payment/webhook" })).toBe( @@ -96,10 +81,8 @@ describe("stripePlugin — configuration present", async () => { }); it("registers the webhook route when enablePaymentWebhook is true", async () => { - await fastify.register( - plugin, - createStripeConfig({ enablePaymentWebhook: true }), - ); + fastify.config.stripe = createStripeConfig({ enablePaymentWebhook: true }); + await fastify.register(plugin); await fastify.ready(); expect(fastify.hasRoute({ method: "POST", url: "/payment/webhook" })).toBe( @@ -116,6 +99,7 @@ describe("stripePlugin — fastify-plugin wrapping", async () => { beforeEach(() => { vi.clearAllMocks(); fastify = Fastify({ logger: false }); + fastify.decorate("config", {} as unknown as FastifyInstance["config"]); }); afterEach(async () => { @@ -123,10 +107,8 @@ describe("stripePlugin — fastify-plugin wrapping", async () => { }); it("registers without encapsulation so the route is reachable on the top-level instance", async () => { - await fastify.register( - plugin, - createStripeConfig({ enablePaymentWebhook: true }), - ); + fastify.config.stripe = createStripeConfig({ enablePaymentWebhook: true }); + await fastify.register(plugin); await fastify.ready(); expect(fastify.hasRoute({ method: "POST", url: "/payment/webhook" })).toBe( diff --git a/packages/stripe/src/__test__/stripeRawBodyParser.test.ts b/packages/stripe/src/__test__/stripeRawBodyParser.test.ts index f7ac1a634..56f07ca85 100644 --- a/packages/stripe/src/__test__/stripeRawBodyParser.test.ts +++ b/packages/stripe/src/__test__/stripeRawBodyParser.test.ts @@ -5,6 +5,10 @@ import "../index"; import registerRawBodyParser from "../utils/stripeRawBodyParser"; import createStripeConfig from "./helpers/createStripeConfig"; +function decorateConfig(fastify: FastifyInstance) { + fastify.decorate("config", {} as unknown as FastifyInstance["config"]); +} + const { stripeMock } = vi.hoisted(() => { const stripeMock = vi.fn().mockImplementation(() => ({ webhooks: { constructEvent: vi.fn() }, @@ -97,10 +101,9 @@ describe("stripeRawBodyParser — scoping when installed by the webhook controll // content-type parser is encapsulated to the controller's plugin scope // and does NOT bleed into the parent instance. fastify = Fastify({ logger: false }); - await fastify.register( - plugin, - createStripeConfig({ enablePaymentWebhook: true }), - ); + decorateConfig(fastify); + fastify.config.stripe = createStripeConfig({ enablePaymentWebhook: true }); + await fastify.register(plugin); let unrelatedRawBody: unknown; fastify.post("/some/other/route", async (request) => { diff --git a/packages/stripe/src/__test__/verifyStripeSignature.test.ts b/packages/stripe/src/__test__/verifyStripeSignature.test.ts index 840289661..f68034e2d 100644 --- a/packages/stripe/src/__test__/verifyStripeSignature.test.ts +++ b/packages/stripe/src/__test__/verifyStripeSignature.test.ts @@ -6,6 +6,10 @@ import type { StripeConfig } from "../types"; import "../index"; import createStripeConfig from "./helpers/createStripeConfig"; +function decorateConfig(fastify: FastifyInstance) { + fastify.decorate("config", {} as unknown as FastifyInstance["config"]); +} + const { constructEventMock, stripeMock } = vi.hoisted(() => { const constructEventMock = vi.fn(); const stripeMock = Object.assign(vi.fn(), { @@ -27,10 +31,11 @@ const registerWithStripe = async ( plugin: typeof import("../plugin").default, overrides: Partial = {}, ) => { - await fastify.register( - plugin, - createStripeConfig({ enablePaymentWebhook: true, ...overrides }), - ); + fastify.config.stripe = createStripeConfig({ + enablePaymentWebhook: true, + ...overrides, + }); + await fastify.register(plugin); await fastify.ready(); }; @@ -50,6 +55,7 @@ describe("verifyStripeSignature — webhookSecret missing", async () => { it("responds with 400 and 'Webhook secret not configured' when webhookSecret is unset", async () => { fastify = Fastify({ logger: { level: "silent" } }); + decorateConfig(fastify); await registerWithStripe(fastify, plugin, { webhookSecret: undefined, @@ -71,6 +77,7 @@ describe("verifyStripeSignature — webhookSecret missing", async () => { it("logs an error when webhookSecret is unset", async () => { fastify = Fastify({ logger: { level: "silent" } }); + decorateConfig(fastify); const errorSpy = vi.spyOn(fastify.log, "error"); await registerWithStripe(fastify, plugin, { @@ -109,6 +116,7 @@ describe("verifyStripeSignature — signature header missing", async () => { }); it("responds with 400 and 'Missing stripe-signature header' when the header is absent", async () => { + decorateConfig(fastify); await registerWithStripe(fastify, plugin); const res = await fastify.inject({ @@ -123,6 +131,7 @@ describe("verifyStripeSignature — signature header missing", async () => { }); it("does not invoke stripe.webhooks.constructEvent when the signature header is missing", async () => { + decorateConfig(fastify); await registerWithStripe(fastify, plugin); await fastify.inject({ @@ -155,6 +164,7 @@ describe("verifyStripeSignature — signature verification failure", async () => throw new Error("invalid signature"); }); + decorateConfig(fastify); await registerWithStripe(fastify, plugin); const res = await fastify.inject({ @@ -180,6 +190,7 @@ describe("verifyStripeSignature — signature verification failure", async () => }); const errorSpy = vi.spyOn(fastify.log, "error"); + decorateConfig(fastify); await registerWithStripe(fastify, plugin); await fastify.inject({ @@ -221,6 +232,7 @@ describe("verifyStripeSignature — success", async () => { }); it("attaches the verified Stripe.Event to the request before the route handler runs", async () => { + decorateConfig(fastify); await registerWithStripe(fastify, plugin, { handlers: { webhook: webhookHandlerMock }, }); @@ -240,6 +252,7 @@ describe("verifyStripeSignature — success", async () => { }); it("calls stripe.webhooks.constructEvent with the raw body, signature, and configured secret", async () => { + decorateConfig(fastify); await registerWithStripe(fastify, plugin, { handlers: { webhook: webhookHandlerMock }, }); diff --git a/packages/stripe/src/__test__/webhookController.test.ts b/packages/stripe/src/__test__/webhookController.test.ts index 59ea30d3a..91b5ab656 100644 --- a/packages/stripe/src/__test__/webhookController.test.ts +++ b/packages/stripe/src/__test__/webhookController.test.ts @@ -1,6 +1,8 @@ import Fastify, { type FastifyInstance } from "fastify"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { StripeConfig } from "../types"; + import "../index"; import createStripeConfig from "./helpers/createStripeConfig"; @@ -36,6 +38,20 @@ const injectWebhook = ( url, }); +function decorateConfig(fastify: FastifyInstance) { + fastify.decorate("config", {} as unknown as FastifyInstance["config"]); +} + +function setStripeConfig( + fastify: FastifyInstance, + overrides: Partial = {}, +) { + fastify.config.stripe = createStripeConfig({ + enablePaymentWebhook: true, + ...overrides, + }); +} + describe("webhookController — route registration", async () => { const { default: plugin } = await import("../plugin"); @@ -52,11 +68,10 @@ describe("webhookController — route registration", async () => { it("registers POST at /payment/webhook by default when webhookPath is unset", async () => { fastify = Fastify({ logger: false }); + decorateConfig(fastify); - await fastify.register( - plugin, - createStripeConfig({ enablePaymentWebhook: true }), - ); + setStripeConfig(fastify); + await fastify.register(plugin); await fastify.ready(); expect(fastify.hasRoute({ method: "POST", url: "/payment/webhook" })).toBe( @@ -66,14 +81,10 @@ describe("webhookController — route registration", async () => { it("registers POST at the configured webhookPath when set", async () => { fastify = Fastify({ logger: false }); + decorateConfig(fastify); - await fastify.register( - plugin, - createStripeConfig({ - enablePaymentWebhook: true, - webhookPath: "/custom/webhook", - }), - ); + setStripeConfig(fastify, { webhookPath: "/custom/webhook" }); + await fastify.register(plugin); await fastify.ready(); expect(fastify.hasRoute({ method: "POST", url: "/custom/webhook" })).toBe( @@ -83,12 +94,11 @@ describe("webhookController — route registration", async () => { it("logs 'Registering Stripe webhook route' at info level", async () => { fastify = Fastify({ logger: { level: "silent" } }); + decorateConfig(fastify); const infoSpy = vi.spyOn(fastify.log, "info"); - await fastify.register( - plugin, - createStripeConfig({ enablePaymentWebhook: true }), - ); + setStripeConfig(fastify); + await fastify.register(plugin); await fastify.ready(); expect(infoSpy).toHaveBeenCalledWith("Registering Stripe webhook route"); @@ -112,14 +122,10 @@ describe("webhookController — dispatch", async () => { it("invokes config.stripe.handlers.webhook with request and verified event", async () => { const webhookHandlerMock = vi.fn().mockResolvedValue(); fastify = Fastify({ logger: false }); + decorateConfig(fastify); - await fastify.register( - plugin, - createStripeConfig({ - enablePaymentWebhook: true, - handlers: { webhook: webhookHandlerMock }, - }), - ); + setStripeConfig(fastify, { handlers: { webhook: webhookHandlerMock } }); + await fastify.register(plugin); await fastify.ready(); const res = await injectWebhook(fastify, "/payment/webhook"); @@ -131,11 +137,10 @@ describe("webhookController — dispatch", async () => { it("responds 200 with the default fallback handler when no custom handler is configured (to suppress Stripe retries)", async () => { fastify = Fastify({ logger: false }); + decorateConfig(fastify); - await fastify.register( - plugin, - createStripeConfig({ enablePaymentWebhook: true }), - ); + setStripeConfig(fastify); + await fastify.register(plugin); await fastify.ready(); const res = await injectWebhook(fastify, "/payment/webhook"); @@ -145,12 +150,11 @@ describe("webhookController — dispatch", async () => { it("warns at registration time when enablePaymentWebhook is true but handlers.webhook is unset", async () => { fastify = Fastify({ logger: { level: "silent" } }); + decorateConfig(fastify); const warnSpy = vi.spyOn(fastify.log, "warn"); - await fastify.register( - plugin, - createStripeConfig({ enablePaymentWebhook: true }), - ); + setStripeConfig(fastify); + await fastify.register(plugin); await fastify.ready(); expect(warnSpy).toHaveBeenCalledWith( @@ -161,15 +165,11 @@ describe("webhookController — dispatch", async () => { it("does NOT warn at registration time when handlers.webhook is configured", async () => { const webhookHandlerMock = vi.fn().mockResolvedValue(); fastify = Fastify({ logger: { level: "silent" } }); + decorateConfig(fastify); const warnSpy = vi.spyOn(fastify.log, "warn"); - await fastify.register( - plugin, - createStripeConfig({ - enablePaymentWebhook: true, - handlers: { webhook: webhookHandlerMock }, - }), - ); + setStripeConfig(fastify, { handlers: { webhook: webhookHandlerMock } }); + await fastify.register(plugin); await fastify.ready(); expect(warnSpy).not.toHaveBeenCalledWith( @@ -180,14 +180,10 @@ describe("webhookController — dispatch", async () => { it("does not call the default handler when handlers.webhook is configured", async () => { const webhookHandlerMock = vi.fn().mockResolvedValue(); fastify = Fastify({ logger: false }); + decorateConfig(fastify); - await fastify.register( - plugin, - createStripeConfig({ - enablePaymentWebhook: true, - handlers: { webhook: webhookHandlerMock }, - }), - ); + setStripeConfig(fastify, { handlers: { webhook: webhookHandlerMock } }); + await fastify.register(plugin); await fastify.ready(); const res = await injectWebhook(fastify, "/payment/webhook"); @@ -228,11 +224,10 @@ describe("webhookController — defensive guards", async () => { ); fastify = Fastify({ logger: false }); + decorateConfig(fastify); - await fastify.register( - plugin, - createStripeConfig({ enablePaymentWebhook: true }), - ); + setStripeConfig(fastify); + await fastify.register(plugin); await fastify.ready(); const res = await injectWebhook(fastify, "/payment/webhook"); diff --git a/packages/stripe/src/index.ts b/packages/stripe/src/index.ts index 1df23cc0e..42d2be1c2 100644 --- a/packages/stripe/src/index.ts +++ b/packages/stripe/src/index.ts @@ -1,7 +1,15 @@ import Stripe from "stripe"; +import type { StripeClient } from "./utils"; + import { StripeConfig } from "./types"; +declare module "fastify" { + interface FastifyInstance { + stripe: StripeClient; + } +} + declare module "@prefabs.tech/fastify-config" { interface ApiConfig { stripe?: StripeConfig; diff --git a/packages/stripe/src/plugin.ts b/packages/stripe/src/plugin.ts index 47ec7610d..102ad1ccc 100644 --- a/packages/stripe/src/plugin.ts +++ b/packages/stripe/src/plugin.ts @@ -4,22 +4,30 @@ import FastifyPlugin from "fastify-plugin"; import type { StripeConfig } from "./types"; +import StripeClient from "./utils/stripeClient"; import webhookController from "./webhook/controller"; const plugin: FastifyPluginAsync = async ( fastify: FastifyInstance, - options, ) => { - fastify.log.info("Registering Stripe plugin"); + const { config, log } = fastify; - if (!options || Object.keys(options).length === 0) { + log.info("Registering Stripe plugin"); + + if (!config.stripe || Object.keys(config.stripe).length === 0) { throw new Error( "Missing stripe configuration. Did you forget to pass it to the stripe plugin?", ); } - if (options.enablePaymentWebhook) { - await fastify.register(webhookController, { stripeConfig: options }); + if (fastify.stripe) { + throw new Error("fastify-stripe has already been registered"); + } + + fastify.decorate("stripe", new StripeClient(config)); + + if (config.stripe?.enablePaymentWebhook) { + await fastify.register(webhookController, { stripeConfig: config.stripe }); } };