Skip to content

Commit 4461825

Browse files
committed
test: add unit tests for org billing portal endpoint using dependency injection
1 parent 810d33f commit 4461825

File tree

3 files changed

+484
-67
lines changed

3 files changed

+484
-67
lines changed
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
import { describe, expect, mock, test } from 'bun:test'
2+
3+
import type { Logger } from '@codebuff/common/types/contracts/logger'
4+
5+
import { postOrgBillingPortal } from '../_post'
6+
7+
import type {
8+
CreateBillingPortalSessionFn,
9+
GetMembershipFn,
10+
GetSessionFn,
11+
OrgMembership,
12+
Session,
13+
} from '../_post'
14+
15+
const createMockLogger = (errorFn = mock(() => {})): Logger => ({
16+
error: errorFn,
17+
warn: mock(() => {}),
18+
info: mock(() => {}),
19+
debug: mock(() => {}),
20+
})
21+
22+
const createMockGetSession = (session: Session): GetSessionFn =>
23+
mock(() => Promise.resolve(session))
24+
25+
const createMockGetMembership = (
26+
result: OrgMembership | null
27+
): GetMembershipFn => mock(() => Promise.resolve(result))
28+
29+
const createMockCreateBillingPortalSession = (
30+
result: { url: string } | Error = { url: 'https://billing.stripe.com/session/test_123' }
31+
): CreateBillingPortalSessionFn => {
32+
if (result instanceof Error) {
33+
return mock(() => Promise.reject(result))
34+
}
35+
return mock(() => Promise.resolve(result))
36+
}
37+
38+
const defaultOrg = {
39+
id: 'org-123',
40+
name: 'Test Org',
41+
slug: 'test-org',
42+
stripe_customer_id: 'cus_org_123',
43+
}
44+
45+
const buildReturnUrl = (orgSlug: string) => `https://codebuff.com/orgs/${orgSlug}/settings`
46+
47+
describe('/api/orgs/[orgId]/billing/portal POST endpoint', () => {
48+
const orgId = 'org-123'
49+
50+
describe('Feature flag', () => {
51+
test('returns 503 when org billing is disabled', async () => {
52+
const response = await postOrgBillingPortal({
53+
orgId,
54+
getSession: createMockGetSession({ user: { id: 'user-123' } }),
55+
getMembership: createMockGetMembership({
56+
role: 'owner',
57+
organization: defaultOrg,
58+
}),
59+
createBillingPortalSession: createMockCreateBillingPortalSession(),
60+
logger: createMockLogger(),
61+
orgBillingEnabled: false,
62+
buildReturnUrl,
63+
})
64+
65+
expect(response.status).toBe(503)
66+
const body = await response.json()
67+
expect(body).toEqual({ error: 'Organization billing is temporarily disabled' })
68+
})
69+
})
70+
71+
describe('Authentication', () => {
72+
test('returns 401 when session is null', async () => {
73+
const response = await postOrgBillingPortal({
74+
orgId,
75+
getSession: createMockGetSession(null),
76+
getMembership: createMockGetMembership(null),
77+
createBillingPortalSession: createMockCreateBillingPortalSession(),
78+
logger: createMockLogger(),
79+
orgBillingEnabled: true,
80+
buildReturnUrl,
81+
})
82+
83+
expect(response.status).toBe(401)
84+
const body = await response.json()
85+
expect(body).toEqual({ error: 'Unauthorized' })
86+
})
87+
88+
test('returns 401 when session.user is null', async () => {
89+
const response = await postOrgBillingPortal({
90+
orgId,
91+
getSession: createMockGetSession({ user: null }),
92+
getMembership: createMockGetMembership(null),
93+
createBillingPortalSession: createMockCreateBillingPortalSession(),
94+
logger: createMockLogger(),
95+
orgBillingEnabled: true,
96+
buildReturnUrl,
97+
})
98+
99+
expect(response.status).toBe(401)
100+
const body = await response.json()
101+
expect(body).toEqual({ error: 'Unauthorized' })
102+
})
103+
104+
test('returns 401 when session.user.id is missing', async () => {
105+
const response = await postOrgBillingPortal({
106+
orgId,
107+
getSession: createMockGetSession({ user: {} as any }),
108+
getMembership: createMockGetMembership(null),
109+
createBillingPortalSession: createMockCreateBillingPortalSession(),
110+
logger: createMockLogger(),
111+
orgBillingEnabled: true,
112+
buildReturnUrl,
113+
})
114+
115+
expect(response.status).toBe(401)
116+
const body = await response.json()
117+
expect(body).toEqual({ error: 'Unauthorized' })
118+
})
119+
})
120+
121+
describe('Organization membership', () => {
122+
test('returns 404 when user is not a member of the organization', async () => {
123+
const response = await postOrgBillingPortal({
124+
orgId,
125+
getSession: createMockGetSession({ user: { id: 'user-123' } }),
126+
getMembership: createMockGetMembership(null),
127+
createBillingPortalSession: createMockCreateBillingPortalSession(),
128+
logger: createMockLogger(),
129+
orgBillingEnabled: true,
130+
buildReturnUrl,
131+
})
132+
133+
expect(response.status).toBe(404)
134+
const body = await response.json()
135+
expect(body).toEqual({ error: 'Organization not found' })
136+
})
137+
138+
test('calls getMembership with correct parameters', async () => {
139+
const mockGetMembership = createMockGetMembership({
140+
role: 'owner',
141+
organization: defaultOrg,
142+
})
143+
144+
await postOrgBillingPortal({
145+
orgId: 'org-456',
146+
getSession: createMockGetSession({ user: { id: 'user-789' } }),
147+
getMembership: mockGetMembership,
148+
createBillingPortalSession: createMockCreateBillingPortalSession(),
149+
logger: createMockLogger(),
150+
orgBillingEnabled: true,
151+
buildReturnUrl,
152+
})
153+
154+
expect(mockGetMembership).toHaveBeenCalledTimes(1)
155+
expect(mockGetMembership).toHaveBeenCalledWith({
156+
orgId: 'org-456',
157+
userId: 'user-789',
158+
})
159+
})
160+
})
161+
162+
describe('Permissions', () => {
163+
test('returns 403 when user is a member (not owner or admin)', async () => {
164+
const response = await postOrgBillingPortal({
165+
orgId,
166+
getSession: createMockGetSession({ user: { id: 'user-123' } }),
167+
getMembership: createMockGetMembership({
168+
role: 'member',
169+
organization: defaultOrg,
170+
}),
171+
createBillingPortalSession: createMockCreateBillingPortalSession(),
172+
logger: createMockLogger(),
173+
orgBillingEnabled: true,
174+
buildReturnUrl,
175+
})
176+
177+
expect(response.status).toBe(403)
178+
const body = await response.json()
179+
expect(body).toEqual({ error: 'Insufficient permissions' })
180+
})
181+
182+
test('allows owner to access billing portal', async () => {
183+
const response = await postOrgBillingPortal({
184+
orgId,
185+
getSession: createMockGetSession({ user: { id: 'user-123' } }),
186+
getMembership: createMockGetMembership({
187+
role: 'owner',
188+
organization: defaultOrg,
189+
}),
190+
createBillingPortalSession: createMockCreateBillingPortalSession(),
191+
logger: createMockLogger(),
192+
orgBillingEnabled: true,
193+
buildReturnUrl,
194+
})
195+
196+
expect(response.status).toBe(200)
197+
})
198+
199+
test('allows admin to access billing portal', async () => {
200+
const response = await postOrgBillingPortal({
201+
orgId,
202+
getSession: createMockGetSession({ user: { id: 'user-123' } }),
203+
getMembership: createMockGetMembership({
204+
role: 'admin',
205+
organization: defaultOrg,
206+
}),
207+
createBillingPortalSession: createMockCreateBillingPortalSession(),
208+
logger: createMockLogger(),
209+
orgBillingEnabled: true,
210+
buildReturnUrl,
211+
})
212+
213+
expect(response.status).toBe(200)
214+
})
215+
})
216+
217+
describe('Stripe customer validation', () => {
218+
test('returns 400 when organization has no stripe_customer_id', async () => {
219+
const response = await postOrgBillingPortal({
220+
orgId,
221+
getSession: createMockGetSession({ user: { id: 'user-123' } }),
222+
getMembership: createMockGetMembership({
223+
role: 'owner',
224+
organization: { ...defaultOrg, stripe_customer_id: null },
225+
}),
226+
createBillingPortalSession: createMockCreateBillingPortalSession(),
227+
logger: createMockLogger(),
228+
orgBillingEnabled: true,
229+
buildReturnUrl,
230+
})
231+
232+
expect(response.status).toBe(400)
233+
const body = await response.json()
234+
expect(body).toEqual({ error: 'No Stripe customer ID found for organization' })
235+
})
236+
})
237+
238+
describe('Successful portal session creation', () => {
239+
test('returns 200 with portal URL on success', async () => {
240+
const expectedUrl = 'https://billing.stripe.com/session/org_abc123'
241+
const response = await postOrgBillingPortal({
242+
orgId,
243+
getSession: createMockGetSession({ user: { id: 'user-123' } }),
244+
getMembership: createMockGetMembership({
245+
role: 'owner',
246+
organization: defaultOrg,
247+
}),
248+
createBillingPortalSession: createMockCreateBillingPortalSession({ url: expectedUrl }),
249+
logger: createMockLogger(),
250+
orgBillingEnabled: true,
251+
buildReturnUrl,
252+
})
253+
254+
expect(response.status).toBe(200)
255+
const body = await response.json()
256+
expect(body).toEqual({ url: expectedUrl })
257+
})
258+
259+
test('calls createBillingPortalSession with correct parameters', async () => {
260+
const mockCreateSession = createMockCreateBillingPortalSession()
261+
262+
await postOrgBillingPortal({
263+
orgId,
264+
getSession: createMockGetSession({ user: { id: 'user-123' } }),
265+
getMembership: createMockGetMembership({
266+
role: 'admin',
267+
organization: {
268+
...defaultOrg,
269+
slug: 'my-org',
270+
stripe_customer_id: 'cus_my_org_456',
271+
},
272+
}),
273+
createBillingPortalSession: mockCreateSession,
274+
logger: createMockLogger(),
275+
orgBillingEnabled: true,
276+
buildReturnUrl: (slug) => `https://example.com/orgs/${slug}/billing`,
277+
})
278+
279+
expect(mockCreateSession).toHaveBeenCalledTimes(1)
280+
expect(mockCreateSession).toHaveBeenCalledWith({
281+
customer: 'cus_my_org_456',
282+
return_url: 'https://example.com/orgs/my-org/billing',
283+
})
284+
})
285+
})
286+
287+
describe('Error handling', () => {
288+
test('returns 500 when Stripe API throws an error', async () => {
289+
const response = await postOrgBillingPortal({
290+
orgId,
291+
getSession: createMockGetSession({ user: { id: 'user-123' } }),
292+
getMembership: createMockGetMembership({
293+
role: 'owner',
294+
organization: defaultOrg,
295+
}),
296+
createBillingPortalSession: createMockCreateBillingPortalSession(
297+
new Error('Stripe API error')
298+
),
299+
logger: createMockLogger(),
300+
orgBillingEnabled: true,
301+
buildReturnUrl,
302+
})
303+
304+
expect(response.status).toBe(500)
305+
const body = await response.json()
306+
expect(body).toEqual({ error: 'Failed to create billing portal session' })
307+
})
308+
309+
test('logs error when Stripe API fails', async () => {
310+
const mockLoggerError = mock(() => {})
311+
const testError = new Error('Stripe connection failed')
312+
313+
await postOrgBillingPortal({
314+
orgId: 'org-error-test',
315+
getSession: createMockGetSession({ user: { id: 'user-error' } }),
316+
getMembership: createMockGetMembership({
317+
role: 'owner',
318+
organization: defaultOrg,
319+
}),
320+
createBillingPortalSession: createMockCreateBillingPortalSession(testError),
321+
logger: createMockLogger(mockLoggerError),
322+
orgBillingEnabled: true,
323+
buildReturnUrl,
324+
})
325+
326+
expect(mockLoggerError).toHaveBeenCalledTimes(1)
327+
expect(mockLoggerError).toHaveBeenCalledWith(
328+
{ userId: 'user-error', orgId: 'org-error-test', error: testError },
329+
'Failed to create org billing portal session'
330+
)
331+
})
332+
})
333+
})

0 commit comments

Comments
 (0)