Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 10 additions & 28 deletions packages/stripe/src/__test__/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -116,17 +99,16 @@ describe("stripePlugin — fastify-plugin wrapping", async () => {
beforeEach(() => {
vi.clearAllMocks();
fastify = Fastify({ logger: false });
fastify.decorate("config", {} as unknown as FastifyInstance["config"]);
});

afterEach(async () => {
await fastify.close();
});

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(
Expand Down
11 changes: 7 additions & 4 deletions packages/stripe/src/__test__/stripeRawBodyParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() },
Expand Down Expand Up @@ -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) => {
Expand Down
21 changes: 17 additions & 4 deletions packages/stripe/src/__test__/verifyStripeSignature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(), {
Expand All @@ -27,10 +31,11 @@ const registerWithStripe = async (
plugin: typeof import("../plugin").default,
overrides: Partial<StripeConfig> = {},
) => {
await fastify.register(
plugin,
createStripeConfig({ enablePaymentWebhook: true, ...overrides }),
);
fastify.config.stripe = createStripeConfig({
enablePaymentWebhook: true,
...overrides,
});
await fastify.register(plugin);
await fastify.ready();
};

Expand All @@ -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,
Expand All @@ -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, {
Expand Down Expand Up @@ -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({
Expand All @@ -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({
Expand Down Expand Up @@ -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({
Expand All @@ -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({
Expand Down Expand Up @@ -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 },
});
Expand All @@ -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 },
});
Expand Down
91 changes: 43 additions & 48 deletions packages/stripe/src/__test__/webhookController.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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<StripeConfig> = {},
) {
fastify.config.stripe = createStripeConfig({
enablePaymentWebhook: true,
...overrides,
});
}

describe("webhookController — route registration", async () => {
const { default: plugin } = await import("../plugin");

Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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");
Expand All @@ -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");
Expand All @@ -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");
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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");
Expand Down Expand Up @@ -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");
Expand Down
8 changes: 8 additions & 0 deletions packages/stripe/src/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Loading
Loading