From 7a7955677f8db355c35a9ab7f148a1c392678244 Mon Sep 17 00:00:00 2001 From: kabin thakuri Date: Fri, 19 Jun 2026 17:48:28 +0545 Subject: [PATCH] feat(stripe): add decorator for stripe package services --- packages/stripe/src/__test__/plugin.test.ts | 2 ++ .../stripe/src/__test__/stripeRawBodyParser.test.ts | 5 +++++ .../src/__test__/verifyStripeSignature.test.ts | 12 ++++++++++++ .../stripe/src/__test__/webhookController.test.ts | 13 +++++++++++++ packages/stripe/src/index.ts | 8 ++++++++ packages/stripe/src/plugin.ts | 9 +++++++++ 6 files changed, 49 insertions(+) diff --git a/packages/stripe/src/__test__/plugin.test.ts b/packages/stripe/src/__test__/plugin.test.ts index f143b7079..4e23e197b 100644 --- a/packages/stripe/src/__test__/plugin.test.ts +++ b/packages/stripe/src/__test__/plugin.test.ts @@ -99,6 +99,7 @@ describe("stripePlugin — configuration present", async () => { beforeEach(() => { vi.clearAllMocks(); fastify = Fastify({ logger: { level: "silent" } }); + fastify.decorate("config", {} as unknown as FastifyInstance["config"]); }); afterEach(async () => { @@ -150,6 +151,7 @@ describe("stripePlugin — fastify-plugin wrapping", async () => { beforeEach(() => { vi.clearAllMocks(); fastify = Fastify({ logger: false }); + fastify.decorate("config", {} as unknown as FastifyInstance["config"]); }); afterEach(async () => { diff --git a/packages/stripe/src/__test__/stripeRawBodyParser.test.ts b/packages/stripe/src/__test__/stripeRawBodyParser.test.ts index f7ac1a634..64427e00c 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,6 +101,7 @@ 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 }); + decorateConfig(fastify); await fastify.register( plugin, createStripeConfig({ enablePaymentWebhook: true }), diff --git a/packages/stripe/src/__test__/verifyStripeSignature.test.ts b/packages/stripe/src/__test__/verifyStripeSignature.test.ts index 840289661..04abc277e 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(), { @@ -50,6 +54,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 +76,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 +115,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 +130,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 +163,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 +189,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 +231,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 +251,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..d854e1459 100644 --- a/packages/stripe/src/__test__/webhookController.test.ts +++ b/packages/stripe/src/__test__/webhookController.test.ts @@ -36,6 +36,10 @@ const injectWebhook = ( url, }); +function decorateConfig(fastify: FastifyInstance) { + fastify.decorate("config", {} as unknown as FastifyInstance["config"]); +} + describe("webhookController — route registration", async () => { const { default: plugin } = await import("../plugin"); @@ -52,6 +56,7 @@ 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, @@ -66,6 +71,7 @@ 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, @@ -83,6 +89,7 @@ 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( @@ -112,6 +119,7 @@ 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, @@ -131,6 +139,7 @@ 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, @@ -145,6 +154,7 @@ 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( @@ -161,6 +171,7 @@ 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( @@ -180,6 +191,7 @@ 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, @@ -228,6 +240,7 @@ describe("webhookController — defensive guards", async () => { ); fastify = Fastify({ logger: false }); + decorateConfig(fastify); await fastify.register( plugin, 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 33eb91e26..7c559669c 100644 --- a/packages/stripe/src/plugin.ts +++ b/packages/stripe/src/plugin.ts @@ -4,6 +4,7 @@ import FastifyPlugin from "fastify-plugin"; import type { StripeConfig } from "./types"; +import StripeClient from "./utils/stripeClient"; import webhookController from "./webhook/controller"; const plugin: FastifyPluginAsync = async ( @@ -24,8 +25,16 @@ const plugin: FastifyPluginAsync = async ( } options = fastify.config.stripe; + } else { + fastify.config.stripe = options; } + if (fastify.stripe) { + throw new Error("fastify-stripe has already been registered"); + } + + fastify.decorate("stripe", new StripeClient(fastify.config)); + if (options.enablePaymentWebhook) { await fastify.register(webhookController, { stripeConfig: options }); }