Skip to content

Commit abfedd8

Browse files
committed
test(billing): add unit tests for isValidPaymentMethod and filterValidPaymentMethods
- 25 new tests covering card expiration validation, link payment methods, and unsupported types - Tests for filterValidPaymentMethods including edge cases (empty, all valid, all invalid, mixed) - Verifies order preservation and non-mutation of input array
1 parent 8611c2a commit abfedd8

File tree

1 file changed

+274
-0
lines changed

1 file changed

+274
-0
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import { describe, expect, it } from 'bun:test'
2+
3+
import {
4+
filterValidPaymentMethods,
5+
isValidPaymentMethod,
6+
} from '../auto-topup-helpers'
7+
8+
import type Stripe from 'stripe'
9+
10+
/**
11+
* Creates a mock Stripe card payment method for testing.
12+
*/
13+
function createCardPaymentMethod(
14+
id: string,
15+
expYear: number | undefined,
16+
expMonth: number | undefined,
17+
): Stripe.PaymentMethod {
18+
return {
19+
id,
20+
type: 'card',
21+
card:
22+
expYear !== undefined && expMonth !== undefined
23+
? { exp_year: expYear, exp_month: expMonth }
24+
: expYear !== undefined
25+
? { exp_year: expYear }
26+
: expMonth !== undefined
27+
? { exp_month: expMonth }
28+
: undefined,
29+
} as Stripe.PaymentMethod
30+
}
31+
32+
/**
33+
* Creates a mock Stripe link payment method for testing.
34+
*/
35+
function createLinkPaymentMethod(id: string): Stripe.PaymentMethod {
36+
return {
37+
id,
38+
type: 'link',
39+
} as Stripe.PaymentMethod
40+
}
41+
42+
/**
43+
* Creates a mock Stripe payment method with a specified type.
44+
*/
45+
function createPaymentMethodWithType(
46+
id: string,
47+
type: string,
48+
): Stripe.PaymentMethod {
49+
return {
50+
id,
51+
type,
52+
} as Stripe.PaymentMethod
53+
}
54+
55+
describe('auto-topup-helpers', () => {
56+
describe('isValidPaymentMethod', () => {
57+
describe('card payment methods', () => {
58+
it('should return true for card with future expiration date', () => {
59+
// Card expiring in December 2099 - definitely in the future
60+
const card = createCardPaymentMethod('pm_1', 2099, 12)
61+
expect(isValidPaymentMethod(card)).toBe(true)
62+
})
63+
64+
it('should return true for card expiring many years in the future', () => {
65+
const card = createCardPaymentMethod('pm_1', 2050, 6)
66+
expect(isValidPaymentMethod(card)).toBe(true)
67+
})
68+
69+
it('should return false for card that expired in the past', () => {
70+
// Card expired in January 2020 - definitely in the past
71+
const card = createCardPaymentMethod('pm_1', 2020, 1)
72+
expect(isValidPaymentMethod(card)).toBe(false)
73+
})
74+
75+
it('should return false for card that expired years ago', () => {
76+
const card = createCardPaymentMethod('pm_1', 2015, 6)
77+
expect(isValidPaymentMethod(card)).toBe(false)
78+
})
79+
80+
it('should return false for card expiring in current month', () => {
81+
// The logic uses > not >= so cards expiring this month are invalid
82+
// as the check creates a date at the START of the expiration month
83+
const now = new Date()
84+
const card = createCardPaymentMethod(
85+
'pm_1',
86+
now.getFullYear(),
87+
now.getMonth() + 1,
88+
)
89+
expect(isValidPaymentMethod(card)).toBe(false)
90+
})
91+
92+
it('should return true for card expiring next month', () => {
93+
const now = new Date()
94+
// Handle year rollover
95+
const nextMonth = now.getMonth() + 2 // +2 because getMonth is 0-indexed but exp_month is 1-indexed
96+
const year =
97+
nextMonth > 12 ? now.getFullYear() + 1 : now.getFullYear()
98+
const month = nextMonth > 12 ? nextMonth - 12 : nextMonth
99+
const card = createCardPaymentMethod('pm_1', year, month)
100+
expect(isValidPaymentMethod(card)).toBe(true)
101+
})
102+
103+
it('should return false for card with missing exp_year', () => {
104+
const card = createCardPaymentMethod('pm_1', undefined, 12)
105+
expect(isValidPaymentMethod(card)).toBe(false)
106+
})
107+
108+
it('should return false for card with missing exp_month', () => {
109+
const card = createCardPaymentMethod('pm_1', 2099, undefined)
110+
expect(isValidPaymentMethod(card)).toBe(false)
111+
})
112+
113+
it('should return false for card with missing card object', () => {
114+
const card = {
115+
id: 'pm_1',
116+
type: 'card',
117+
card: undefined,
118+
} as Stripe.PaymentMethod
119+
expect(isValidPaymentMethod(card)).toBe(false)
120+
})
121+
122+
it('should return false for card with null card object', () => {
123+
const card = {
124+
id: 'pm_1',
125+
type: 'card',
126+
card: null,
127+
} as unknown as Stripe.PaymentMethod
128+
expect(isValidPaymentMethod(card)).toBe(false)
129+
})
130+
})
131+
132+
describe('link payment methods', () => {
133+
it('should return true for link payment method', () => {
134+
const link = createLinkPaymentMethod('pm_link_1')
135+
expect(isValidPaymentMethod(link)).toBe(true)
136+
})
137+
138+
it('should return true for any link payment method regardless of other properties', () => {
139+
const link = {
140+
id: 'pm_link_2',
141+
type: 'link',
142+
link: { email: 'test@example.com' },
143+
} as Stripe.PaymentMethod
144+
expect(isValidPaymentMethod(link)).toBe(true)
145+
})
146+
})
147+
148+
describe('other payment method types', () => {
149+
it('should return false for sepa_debit payment method', () => {
150+
const sepa = createPaymentMethodWithType('pm_sepa_1', 'sepa_debit')
151+
expect(isValidPaymentMethod(sepa)).toBe(false)
152+
})
153+
154+
it('should return false for us_bank_account payment method', () => {
155+
const bank = createPaymentMethodWithType('pm_bank_1', 'us_bank_account')
156+
expect(isValidPaymentMethod(bank)).toBe(false)
157+
})
158+
159+
it('should return false for acss_debit payment method', () => {
160+
const acss = createPaymentMethodWithType('pm_acss_1', 'acss_debit')
161+
expect(isValidPaymentMethod(acss)).toBe(false)
162+
})
163+
164+
it('should return false for unknown payment method type', () => {
165+
const unknown = createPaymentMethodWithType('pm_unknown', 'unknown_type')
166+
expect(isValidPaymentMethod(unknown)).toBe(false)
167+
})
168+
169+
it('should return false for empty type string', () => {
170+
const empty = createPaymentMethodWithType('pm_empty', '')
171+
expect(isValidPaymentMethod(empty)).toBe(false)
172+
})
173+
})
174+
})
175+
176+
describe('filterValidPaymentMethods', () => {
177+
it('should return empty array for empty input', () => {
178+
const result = filterValidPaymentMethods([])
179+
expect(result).toEqual([])
180+
})
181+
182+
it('should return all payment methods when all are valid', () => {
183+
const validCard = createCardPaymentMethod('pm_card_1', 2099, 12)
184+
const validLink = createLinkPaymentMethod('pm_link_1')
185+
const validCard2 = createCardPaymentMethod('pm_card_2', 2050, 6)
186+
187+
const result = filterValidPaymentMethods([validCard, validLink, validCard2])
188+
189+
expect(result).toHaveLength(3)
190+
expect(result[0].id).toBe('pm_card_1')
191+
expect(result[1].id).toBe('pm_link_1')
192+
expect(result[2].id).toBe('pm_card_2')
193+
})
194+
195+
it('should return empty array when all payment methods are invalid', () => {
196+
const expiredCard1 = createCardPaymentMethod('pm_expired_1', 2020, 1)
197+
const expiredCard2 = createCardPaymentMethod('pm_expired_2', 2015, 6)
198+
const sepa = createPaymentMethodWithType('pm_sepa_1', 'sepa_debit')
199+
200+
const result = filterValidPaymentMethods([expiredCard1, expiredCard2, sepa])
201+
202+
expect(result).toEqual([])
203+
})
204+
205+
it('should filter out invalid payment methods from mixed list', () => {
206+
const validCard = createCardPaymentMethod('pm_valid_card', 2099, 12)
207+
const expiredCard = createCardPaymentMethod('pm_expired', 2020, 1)
208+
const validLink = createLinkPaymentMethod('pm_link')
209+
const sepa = createPaymentMethodWithType('pm_sepa', 'sepa_debit')
210+
const validCard2 = createCardPaymentMethod('pm_valid_card_2', 2050, 6)
211+
212+
const result = filterValidPaymentMethods([
213+
validCard,
214+
expiredCard,
215+
validLink,
216+
sepa,
217+
validCard2,
218+
])
219+
220+
expect(result).toHaveLength(3)
221+
expect(result.map((pm) => pm.id)).toEqual([
222+
'pm_valid_card',
223+
'pm_link',
224+
'pm_valid_card_2',
225+
])
226+
})
227+
228+
it('should preserve the order of valid payment methods', () => {
229+
const link1 = createLinkPaymentMethod('pm_link_1')
230+
const card1 = createCardPaymentMethod('pm_card_1', 2099, 1)
231+
const link2 = createLinkPaymentMethod('pm_link_2')
232+
const card2 = createCardPaymentMethod('pm_card_2', 2099, 6)
233+
234+
const result = filterValidPaymentMethods([link1, card1, link2, card2])
235+
236+
expect(result.map((pm) => pm.id)).toEqual([
237+
'pm_link_1',
238+
'pm_card_1',
239+
'pm_link_2',
240+
'pm_card_2',
241+
])
242+
})
243+
244+
it('should handle single valid payment method', () => {
245+
const validCard = createCardPaymentMethod('pm_single', 2099, 12)
246+
247+
const result = filterValidPaymentMethods([validCard])
248+
249+
expect(result).toHaveLength(1)
250+
expect(result[0].id).toBe('pm_single')
251+
})
252+
253+
it('should handle single invalid payment method', () => {
254+
const expiredCard = createCardPaymentMethod('pm_expired', 2020, 1)
255+
256+
const result = filterValidPaymentMethods([expiredCard])
257+
258+
expect(result).toEqual([])
259+
})
260+
261+
it('should not mutate the original array', () => {
262+
const validCard = createCardPaymentMethod('pm_valid', 2099, 12)
263+
const expiredCard = createCardPaymentMethod('pm_expired', 2020, 1)
264+
const original = [validCard, expiredCard]
265+
const originalLength = original.length
266+
267+
filterValidPaymentMethods(original)
268+
269+
expect(original).toHaveLength(originalLength)
270+
expect(original[0].id).toBe('pm_valid')
271+
expect(original[1].id).toBe('pm_expired')
272+
})
273+
})
274+
})

0 commit comments

Comments
 (0)