Skip to content

Commit a63a823

Browse files
feat(whop): add tests
1 parent bfe8a6a commit a63a823

2 files changed

Lines changed: 235 additions & 0 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { whop } from "../whop-provider";
4+
5+
describe("@paykitjs/whop", () => {
6+
it("should return a provider config with createAdapter", () => {
7+
const config = whop({
8+
apiKey: "whop_test_123",
9+
companyId: "biz_123",
10+
});
11+
12+
expect(config.id).toBe("whop");
13+
expect(config.name).toBe("Whop");
14+
expect(config.capabilities).toEqual({ testClocks: false });
15+
expect(typeof config.createAdapter).toBe("function");
16+
});
17+
18+
it("should create a PaymentProvider adapter", () => {
19+
const config = whop({
20+
apiKey: "whop_test_123",
21+
companyId: "biz_123",
22+
});
23+
24+
const adapter = config.createAdapter();
25+
expect(adapter.id).toBe("whop");
26+
expect(adapter.name).toBe("Whop");
27+
expect(typeof adapter.createSubscriptionCheckout).toBe("function");
28+
expect(typeof adapter.cancelSubscription).toBe("function");
29+
expect(typeof adapter.handleWebhook).toBe("function");
30+
expect(typeof adapter.syncProducts).toBe("function");
31+
});
32+
});
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { PAYKIT_ERROR_CODES } from "paykitjs";
2+
import { describe, expect, it, vi } from "vitest";
3+
4+
import { createWhopProvider } from "../whop-provider";
5+
6+
type WhopClientMock = {
7+
checkoutConfigurations: { create: ReturnType<typeof vi.fn> };
8+
memberships: {
9+
cancel: ReturnType<typeof vi.fn>;
10+
list: ReturnType<typeof vi.fn>;
11+
resume: ReturnType<typeof vi.fn>;
12+
};
13+
webhooks: { unwrap: ReturnType<typeof vi.fn> };
14+
payments: { list: ReturnType<typeof vi.fn> };
15+
members: { list: ReturnType<typeof vi.fn> };
16+
};
17+
18+
function createClient(overrides: Partial<WhopClientMock> = {}): WhopClientMock {
19+
const base: WhopClientMock = {
20+
checkoutConfigurations: { create: vi.fn() },
21+
memberships: { cancel: vi.fn(), list: vi.fn(), resume: vi.fn() },
22+
webhooks: { unwrap: vi.fn() },
23+
payments: { list: vi.fn() },
24+
members: { list: vi.fn() },
25+
};
26+
27+
return {
28+
...base,
29+
...overrides,
30+
checkoutConfigurations: {
31+
...base.checkoutConfigurations,
32+
...overrides.checkoutConfigurations,
33+
},
34+
memberships: {
35+
...base.memberships,
36+
...overrides.memberships,
37+
},
38+
webhooks: {
39+
...base.webhooks,
40+
...overrides.webhooks,
41+
},
42+
payments: {
43+
...base.payments,
44+
...overrides.payments,
45+
},
46+
members: {
47+
...base.members,
48+
...overrides.members,
49+
},
50+
};
51+
}
52+
53+
describe("providers/whop", () => {
54+
it("creates a checkout session and returns a payment URL", async () => {
55+
const createCheckout = vi
56+
.fn()
57+
.mockResolvedValue({ id: "checkout_123", purchase_url: "https://whop.com/p/123" });
58+
const client = createClient({ checkoutConfigurations: { create: createCheckout } });
59+
const provider = createWhopProvider(client as never, {
60+
apiKey: "whop_test_123",
61+
companyId: "biz_123",
62+
});
63+
64+
const result = await provider.createSubscriptionCheckout({
65+
cancelUrl: "https://example.com/cancel",
66+
metadata: { ref: "abc" },
67+
providerCustomerId: "cus_123",
68+
providerProduct: { productId: "plan_123" },
69+
successUrl: "https://example.com/success",
70+
});
71+
72+
expect(createCheckout).toHaveBeenCalledWith({
73+
plan_id: "plan_123",
74+
metadata: { ref: "abc" },
75+
redirect_url: "https://example.com/success",
76+
});
77+
expect(result).toEqual({
78+
paymentUrl: "https://whop.com/p/123",
79+
providerCheckoutSessionId: "checkout_123",
80+
});
81+
});
82+
83+
it("throws when a checkout session does not include a purchase URL", async () => {
84+
const createCheckout = vi.fn().mockResolvedValue({ id: "checkout_123" });
85+
const client = createClient({ checkoutConfigurations: { create: createCheckout } });
86+
const provider = createWhopProvider(client as never, {
87+
apiKey: "whop_test_123",
88+
companyId: "biz_123",
89+
});
90+
91+
await expect(
92+
provider.createSubscriptionCheckout({
93+
providerCustomerId: "cus_123",
94+
providerProduct: { productId: "plan_123" },
95+
successUrl: "https://example.com/success",
96+
}),
97+
).rejects.toMatchObject({
98+
code: PAYKIT_ERROR_CODES.PROVIDER_SESSION_INVALID.code,
99+
});
100+
});
101+
102+
it("normalizes subscription events from membership webhooks", async () => {
103+
const unwrap = vi.fn().mockReturnValue({
104+
type: "membership.cancel_at_period_end_changed",
105+
data: {
106+
id: "sub_123",
107+
user: { id: "user_123" },
108+
product: { id: "prod_123" },
109+
cancel_at_period_end: true,
110+
canceled_at: "2024-01-10T00:00:00.000Z",
111+
renewal_period_start: "2024-01-01T00:00:00.000Z",
112+
renewal_period_end: "2024-02-01T00:00:00.000Z",
113+
status: "active",
114+
},
115+
});
116+
const client = createClient({ webhooks: { unwrap } });
117+
const provider = createWhopProvider(client as never, {
118+
apiKey: "whop_test_123",
119+
companyId: "biz_123",
120+
});
121+
122+
const [event] = await provider.handleWebhook({
123+
body: "{}",
124+
headers: { "webhook-id": "evt_123" },
125+
});
126+
127+
expect(event.name).toBe("subscription.updated");
128+
expect(event.payload.providerEventId).toBe("evt_123");
129+
expect(event.payload.providerCustomerId).toBe("user_123");
130+
expect(event.payload.subscription).toMatchObject({
131+
cancelAtPeriodEnd: true,
132+
providerProduct: { productId: "prod_123" },
133+
providerSubscriptionId: "sub_123",
134+
providerSubscriptionScheduleId: null,
135+
status: "active",
136+
});
137+
expect(event.payload.subscription?.canceledAt).toEqual(new Date("2024-01-10T00:00:00.000Z"));
138+
expect(event.payload.subscription?.currentPeriodStartAt).toEqual(
139+
new Date("2024-01-01T00:00:00.000Z"),
140+
);
141+
expect(event.payload.subscription?.currentPeriodEndAt).toEqual(
142+
new Date("2024-02-01T00:00:00.000Z"),
143+
);
144+
expect(event.payload.subscription?.endedAt).toBeNull();
145+
});
146+
147+
it("normalizes checkout events from payment webhooks", async () => {
148+
const unwrap = vi.fn().mockReturnValue({
149+
type: "payment.created",
150+
data: {
151+
id: "pay_123",
152+
status: "paid",
153+
user: { id: "user_123" },
154+
membership: { id: "sub_456" },
155+
metadata: { seats: 2, note: "VIP" },
156+
},
157+
});
158+
const client = createClient({ webhooks: { unwrap } });
159+
const provider = createWhopProvider(client as never, {
160+
apiKey: "whop_test_123",
161+
companyId: "biz_123",
162+
});
163+
164+
const [event] = await provider.handleWebhook({
165+
body: "{}",
166+
headers: { "webhook-id": "evt_456" },
167+
});
168+
169+
expect(event).toEqual({
170+
name: "checkout.completed",
171+
payload: {
172+
checkoutSessionId: "pay_123",
173+
mode: "subscription",
174+
paymentStatus: "paid",
175+
providerCustomerId: "user_123",
176+
providerEventId: "evt_456",
177+
providerSubscriptionId: "sub_456",
178+
status: "paid",
179+
metadata: { seats: "2", note: "VIP" },
180+
},
181+
});
182+
});
183+
184+
it("throws a clear error when webhook signatures are invalid", async () => {
185+
const unwrap = vi.fn().mockImplementation(() => {
186+
throw new Error("invalid");
187+
});
188+
const client = createClient({ webhooks: { unwrap } });
189+
const provider = createWhopProvider(client as never, {
190+
apiKey: "whop_test_123",
191+
companyId: "biz_123",
192+
});
193+
194+
await expect(
195+
provider.handleWebhook({
196+
body: "{}",
197+
headers: { "webhook-id": "evt_789" },
198+
}),
199+
).rejects.toMatchObject({
200+
code: PAYKIT_ERROR_CODES.PROVIDER_SIGNATURE_MISSING.code,
201+
});
202+
});
203+
});

0 commit comments

Comments
 (0)