@framers/agentos-ext-wallet 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,428 +0,0 @@
1
- // @ts-nocheck
2
- /**
3
- * @fileoverview Unit tests for CardManager.
4
- *
5
- * Tests cover: card issuance, duplicate prevention, freeze/unfreeze, close,
6
- * card details retrieval, spending summary, webhook processing, and spending
7
- * policy integration.
8
- */
9
-
10
- import { describe, it, expect, beforeEach, vi } from 'vitest';
11
- import { CardManager, CardSpendingBlockedError } from '../src/cards/CardManager.js';
12
- import { MccCategoryMap } from '../src/cards/MccCategoryMap.js';
13
- import { SpendingPolicyEnforcer } from '../src/SpendingPolicyEnforcer.js';
14
- import { DEFAULT_SPENDING_POLICY } from '../src/types.js';
15
- import type { ICardAdapter, ICardStore, AgentCardRecord, CardTransactionRecord, LithicTransaction, CardConfig, CardState, CardTxStatus } from '../src/cards/types.js';
16
-
17
- /* ------------------------------------------------------------------ */
18
- /* Mocks */
19
- /* ------------------------------------------------------------------ */
20
-
21
- const AGENT = 'card-test-agent';
22
-
23
- function createMockAdapter(): ICardAdapter {
24
- return {
25
- createCard: vi.fn(async (params) => ({
26
- token: 'lithic-card-token-123',
27
- last_four: '4242',
28
- type: params.type,
29
- state: params.state || 'OPEN',
30
- spend_limit: params.spend_limit,
31
- spend_limit_duration: params.spend_limit_duration,
32
- pan: '4242424242424242',
33
- cvv: '123',
34
- exp_month: '12',
35
- exp_year: '2028',
36
- funding: { token: 'fund-1', type: 'DEPOSITORY_CHECKING' },
37
- })),
38
- getCard: vi.fn(async () => ({
39
- token: 'lithic-card-token-123',
40
- last_four: '4242',
41
- type: 'VIRTUAL' as const,
42
- state: 'OPEN' as const,
43
- spend_limit: 50000,
44
- spend_limit_duration: 'MONTHLY' as const,
45
- pan: '4242424242424242',
46
- cvv: '123',
47
- exp_month: '12',
48
- exp_year: '2028',
49
- funding: { token: 'fund-1', type: 'DEPOSITORY_CHECKING' },
50
- })),
51
- updateCard: vi.fn(async () => ({} as any)),
52
- pauseCard: vi.fn(async () => {}),
53
- resumeCard: vi.fn(async () => {}),
54
- closeCard: vi.fn(async () => {}),
55
- listTransactions: vi.fn(async () => []),
56
- createAuthRule: vi.fn(async () => {}),
57
- };
58
- }
59
-
60
- function createMockStore(): ICardStore & { cards: Map<string, AgentCardRecord>; txs: CardTransactionRecord[] } {
61
- const cards = new Map<string, AgentCardRecord>();
62
- const txs: CardTransactionRecord[] = [];
63
- return {
64
- cards,
65
- txs,
66
- async getCard(agentId: string) {
67
- return [...cards.values()].find(c => c.agentId === agentId) || null;
68
- },
69
- async saveCard(record: AgentCardRecord) {
70
- cards.set(record.id, record);
71
- },
72
- async updateCardState(agentId: string, state: CardState) {
73
- for (const card of cards.values()) {
74
- if (card.agentId === agentId) card.state = state;
75
- }
76
- },
77
- async insertTransaction(record: CardTransactionRecord) {
78
- txs.push(record);
79
- },
80
- async updateTransactionStatus(id: string, status: CardTxStatus) {
81
- const tx = txs.find(t => t.id === id);
82
- if (tx) tx.status = status;
83
- },
84
- async getTransactions(cardId: string, limit = 20) {
85
- return txs.filter(t => t.cardId === cardId).slice(-limit).reverse();
86
- },
87
- async getTransactionsByPeriod(cardId: string, since: number) {
88
- return txs.filter(t => t.cardId === cardId && t.createdAt >= since);
89
- },
90
- };
91
- }
92
-
93
- function createTestLedger() {
94
- const entries: any[] = [];
95
- return {
96
- entries,
97
- async sumByPeriod(agentId: string, periodKey: string) {
98
- return entries.filter(e => e.agentId === agentId && e.periodKey === periodKey).reduce((s, e) => s + e.amountUsd, 0);
99
- },
100
- async sumByCategoryAndPeriod(agentId: string, category: string, periodKey: string) {
101
- return entries.filter(e => e.agentId === agentId && e.category === category && e.periodKey === periodKey).reduce((s, e) => s + e.amountUsd, 0);
102
- },
103
- async insert(entry: any) { entries.push(entry); },
104
- };
105
- }
106
-
107
- const CARD_CONFIG: CardConfig = {
108
- enabled: true,
109
- provider: 'lithic',
110
- defaultSpendLimitUsd: 500,
111
- allowPhysical: false,
112
- };
113
-
114
- function createTestManager() {
115
- const adapter = createMockAdapter();
116
- const store = createMockStore();
117
- const ledger = createTestLedger();
118
- const policyEnforcer = new SpendingPolicyEnforcer(DEFAULT_SPENDING_POLICY, ledger);
119
- const mccMap = new MccCategoryMap();
120
-
121
- const manager = new CardManager({
122
- adapter,
123
- store,
124
- policyEnforcer,
125
- mccMap,
126
- config: CARD_CONFIG,
127
- });
128
-
129
- return { manager, adapter, store, ledger, policyEnforcer, mccMap };
130
- }
131
-
132
- /* ------------------------------------------------------------------ */
133
- /* Tests */
134
- /* ------------------------------------------------------------------ */
135
-
136
- describe('CardManager', () => {
137
-
138
- /* ── Card issuance ───────────────────────────────────────────────── */
139
-
140
- describe('issueCard', () => {
141
- it('should issue a virtual card', async () => {
142
- const { manager, adapter, store } = createTestManager();
143
- const card = await manager.issueCard(AGENT);
144
-
145
- expect(card.agentId).toBe(AGENT);
146
- expect(card.last4).toBe('4242');
147
- expect(card.cardType).toBe('VIRTUAL');
148
- expect(card.state).toBe('OPEN');
149
- expect(card.spendLimitUsd).toBe(500);
150
- expect(adapter.createCard).toHaveBeenCalledOnce();
151
- expect(store.cards.size).toBe(1);
152
- });
153
-
154
- it('should reject duplicate card for same agent', async () => {
155
- const { manager } = createTestManager();
156
- await manager.issueCard(AGENT);
157
- await expect(manager.issueCard(AGENT)).rejects.toThrow('already has a card');
158
- });
159
-
160
- it('should use custom spend limit', async () => {
161
- const { manager } = createTestManager();
162
- const card = await manager.issueCard(AGENT, { spendLimitUsd: 1000 });
163
- expect(card.spendLimitUsd).toBe(1000);
164
- });
165
-
166
- it('should set memo on card', async () => {
167
- const { manager } = createTestManager();
168
- const card = await manager.issueCard(AGENT, { memo: 'API payments' });
169
- expect(card.memo).toBe('API payments');
170
- });
171
-
172
- it('should create auth rules for blocked categories', async () => {
173
- const { manager, adapter } = createTestManager();
174
- await manager.issueCard(AGENT);
175
- // Default policy has no blocked categories, so createAuthRule should not be called
176
- // (getBlockedMccs returns [])
177
- // This tests the path exists without error
178
- expect(adapter.createCard).toHaveBeenCalledOnce();
179
- });
180
- });
181
-
182
- /* ── Card retrieval ──────────────────────────────────────────────── */
183
-
184
- describe('getCard', () => {
185
- it('should return null when no card exists', async () => {
186
- const { manager } = createTestManager();
187
- const card = await manager.getCard(AGENT);
188
- expect(card).toBeNull();
189
- });
190
-
191
- it('should return card after issuance', async () => {
192
- const { manager } = createTestManager();
193
- await manager.issueCard(AGENT);
194
- const card = await manager.getCard(AGENT);
195
- expect(card).toBeTruthy();
196
- expect(card!.last4).toBe('4242');
197
- });
198
- });
199
-
200
- /* ── Card details ────────────────────────────────────────────────── */
201
-
202
- describe('getCardDetails', () => {
203
- it('should return PAN, CVV, and expiry', async () => {
204
- const { manager } = createTestManager();
205
- await manager.issueCard(AGENT);
206
- const details = await manager.getCardDetails(AGENT);
207
- expect(details.pan).toBe('4242424242424242');
208
- expect(details.cvv).toBe('123');
209
- expect(details.expMonth).toBe('12');
210
- expect(details.expYear).toBe('2028');
211
- });
212
-
213
- it('should throw when no card exists', async () => {
214
- const { manager } = createTestManager();
215
- await expect(manager.getCardDetails(AGENT)).rejects.toThrow('No card found');
216
- });
217
-
218
- it('should check spending policy when amount provided', async () => {
219
- const { manager } = createTestManager();
220
- await manager.issueCard(AGENT);
221
- // Within limits
222
- const details = await manager.getCardDetails(AGENT, 15, 'shopping');
223
- expect(details.pan).toBeTruthy();
224
- });
225
-
226
- it('should block when spending policy denies', async () => {
227
- const { manager } = createTestManager();
228
- await manager.issueCard(AGENT);
229
- // perTransactionLimitUsd is 20 in DEFAULT_SPENDING_POLICY
230
- await expect(manager.getCardDetails(AGENT, 25, 'shopping')).rejects.toThrow();
231
- });
232
- });
233
-
234
- /* ── Freeze / unfreeze ───────────────────────────────────────────── */
235
-
236
- describe('freezeCard', () => {
237
- it('should freeze an open card', async () => {
238
- const { manager, adapter, store } = createTestManager();
239
- await manager.issueCard(AGENT);
240
- await manager.freezeCard(AGENT);
241
-
242
- expect(adapter.pauseCard).toHaveBeenCalledOnce();
243
- const card = await store.getCard(AGENT);
244
- expect(card!.state).toBe('PAUSED');
245
- });
246
-
247
- it('should be idempotent for already paused card', async () => {
248
- const { manager, adapter, store } = createTestManager();
249
- await manager.issueCard(AGENT);
250
- await manager.freezeCard(AGENT);
251
- await manager.freezeCard(AGENT); // second call — no-op
252
- expect(adapter.pauseCard).toHaveBeenCalledOnce();
253
- });
254
-
255
- it('should throw for closed card', async () => {
256
- const { manager, store } = createTestManager();
257
- await manager.issueCard(AGENT);
258
- const card = await store.getCard(AGENT);
259
- card!.state = 'CLOSED';
260
- await expect(manager.freezeCard(AGENT)).rejects.toThrow('Cannot freeze a closed card');
261
- });
262
- });
263
-
264
- describe('unfreezeCard', () => {
265
- it('should unfreeze a paused card', async () => {
266
- const { manager, adapter, store } = createTestManager();
267
- await manager.issueCard(AGENT);
268
- await manager.freezeCard(AGENT);
269
- await manager.unfreezeCard(AGENT);
270
-
271
- expect(adapter.resumeCard).toHaveBeenCalledOnce();
272
- const card = await store.getCard(AGENT);
273
- expect(card!.state).toBe('OPEN');
274
- });
275
-
276
- it('should be idempotent for already open card', async () => {
277
- const { manager, adapter } = createTestManager();
278
- await manager.issueCard(AGENT);
279
- await manager.unfreezeCard(AGENT);
280
- expect(adapter.resumeCard).not.toHaveBeenCalled();
281
- });
282
- });
283
-
284
- /* ── Close card ──────────────────────────────────────────────────── */
285
-
286
- describe('closeCard', () => {
287
- it('should close an open card', async () => {
288
- const { manager, adapter, store } = createTestManager();
289
- await manager.issueCard(AGENT);
290
- await manager.closeCard(AGENT);
291
-
292
- expect(adapter.closeCard).toHaveBeenCalledOnce();
293
- const card = await store.getCard(AGENT);
294
- expect(card!.state).toBe('CLOSED');
295
- });
296
-
297
- it('should be idempotent for already closed card', async () => {
298
- const { manager, adapter, store } = createTestManager();
299
- await manager.issueCard(AGENT);
300
- const card = await store.getCard(AGENT);
301
- card!.state = 'CLOSED';
302
- await manager.closeCard(AGENT);
303
- expect(adapter.closeCard).not.toHaveBeenCalled();
304
- });
305
- });
306
-
307
- /* ── Spending summary ────────────────────────────────────────────── */
308
-
309
- describe('getSpendingSummary', () => {
310
- it('should return empty summary for new card', async () => {
311
- const { manager } = createTestManager();
312
- await manager.issueCard(AGENT);
313
- const summary = await manager.getSpendingSummary(AGENT);
314
- expect(summary.totalUsd).toBe(0);
315
- expect(summary.transactionCount).toBe(0);
316
- expect(summary.byCategory).toEqual([]);
317
- });
318
-
319
- it('should aggregate transactions by category', async () => {
320
- const { manager, store } = createTestManager();
321
- const card = await manager.issueCard(AGENT);
322
-
323
- // Add mock transactions
324
- store.txs.push(
325
- { id: 'tx1', cardId: card.id, lithicTxToken: 'lt1', merchantMcc: '5812', category: 'dining', amountUsd: 15, status: 'SETTLED', createdAt: Date.now() },
326
- { id: 'tx2', cardId: card.id, lithicTxToken: 'lt2', merchantMcc: '5812', category: 'dining', amountUsd: 25, status: 'SETTLED', createdAt: Date.now() },
327
- { id: 'tx3', cardId: card.id, lithicTxToken: 'lt3', merchantMcc: '5411', category: 'shopping', amountUsd: 50, status: 'PENDING', createdAt: Date.now() },
328
- );
329
-
330
- const summary = await manager.getSpendingSummary(AGENT, 'month');
331
- expect(summary.totalUsd).toBe(90);
332
- expect(summary.transactionCount).toBe(3);
333
- expect(summary.byCategory.find(c => c.category === 'dining')?.amountUsd).toBe(40);
334
- expect(summary.byCategory.find(c => c.category === 'shopping')?.amountUsd).toBe(50);
335
- });
336
-
337
- it('should exclude VOIDED/DECLINED transactions', async () => {
338
- const { manager, store } = createTestManager();
339
- const card = await manager.issueCard(AGENT);
340
-
341
- store.txs.push(
342
- { id: 'tx1', cardId: card.id, lithicTxToken: 'lt1', merchantMcc: '5812', category: 'dining', amountUsd: 15, status: 'SETTLED', createdAt: Date.now() },
343
- { id: 'tx2', cardId: card.id, lithicTxToken: 'lt2', merchantMcc: '5812', category: 'dining', amountUsd: 10, status: 'DECLINED', createdAt: Date.now() },
344
- { id: 'tx3', cardId: card.id, lithicTxToken: 'lt3', merchantMcc: '5812', category: 'dining', amountUsd: 5, status: 'VOIDED', createdAt: Date.now() },
345
- );
346
-
347
- const summary = await manager.getSpendingSummary(AGENT, 'month');
348
- expect(summary.totalUsd).toBe(15);
349
- expect(summary.transactionCount).toBe(1);
350
- });
351
- });
352
-
353
- /* ── Webhook processing ──────────────────────────────────────────── */
354
-
355
- describe('processWebhookTransaction', () => {
356
- it('should record transaction and spending', async () => {
357
- const { manager, store, ledger } = createTestManager();
358
- await manager.issueCard(AGENT);
359
-
360
- const lithicTx: LithicTransaction = {
361
- token: 'ltx-001',
362
- card_token: 'lithic-card-token-123',
363
- amount: -1500, // $15.00 in cents, negative for debits
364
- merchant: { descriptor: 'STARBUCKS', mcc: '5812', city: 'SF', state: 'CA', country: 'US' },
365
- status: 'SETTLED',
366
- created: new Date().toISOString(),
367
- };
368
-
369
- await manager.processWebhookTransaction(AGENT, lithicTx);
370
-
371
- expect(store.txs.length).toBe(1);
372
- expect(store.txs[0].category).toBe('dining');
373
- expect(store.txs[0].amountUsd).toBe(15);
374
- expect(store.txs[0].merchantName).toBe('STARBUCKS');
375
- // Should record in spending ledger
376
- expect(ledger.entries.length).toBe(2); // daily + monthly
377
- });
378
-
379
- it('should resolve MCC to correct category', async () => {
380
- const { manager, store } = createTestManager();
381
- await manager.issueCard(AGENT);
382
-
383
- const lithicTx: LithicTransaction = {
384
- token: 'ltx-002',
385
- card_token: 'lithic-card-token-123',
386
- amount: -5000,
387
- merchant: { descriptor: 'HILTON HOTEL', mcc: '7011' },
388
- status: 'PENDING',
389
- created: new Date().toISOString(),
390
- };
391
-
392
- await manager.processWebhookTransaction(AGENT, lithicTx);
393
- expect(store.txs[0].category).toBe('travel');
394
- });
395
-
396
- it('should skip unknown agents', async () => {
397
- const { manager, store } = createTestManager();
398
- const lithicTx: LithicTransaction = {
399
- token: 'ltx-003',
400
- card_token: 'unknown-token',
401
- amount: -1000,
402
- merchant: { descriptor: 'SHOP', mcc: '5411' },
403
- status: 'SETTLED',
404
- created: new Date().toISOString(),
405
- };
406
-
407
- await manager.processWebhookTransaction('unknown-agent', lithicTx);
408
- expect(store.txs.length).toBe(0);
409
- });
410
-
411
- it('should not record spending for DECLINED transactions', async () => {
412
- const { manager, ledger } = createTestManager();
413
- await manager.issueCard(AGENT);
414
-
415
- const lithicTx: LithicTransaction = {
416
- token: 'ltx-004',
417
- card_token: 'lithic-card-token-123',
418
- amount: -2000,
419
- merchant: { descriptor: 'STORE', mcc: '5411' },
420
- status: 'DECLINED',
421
- created: new Date().toISOString(),
422
- };
423
-
424
- await manager.processWebhookTransaction(AGENT, lithicTx);
425
- expect(ledger.entries.length).toBe(0);
426
- });
427
- });
428
- });
@@ -1,224 +0,0 @@
1
- // @ts-nocheck
2
- /**
3
- * @fileoverview Unit tests for LithicCardAdapter.
4
- *
5
- * Tests cover: all API methods, error handling, sandbox vs production URLs,
6
- * and fetch mock interactions.
7
- */
8
-
9
- import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
10
- import { LithicCardAdapter, LithicApiError } from '../src/cards/LithicCardAdapter.js';
11
-
12
- /* ------------------------------------------------------------------ */
13
- /* Mock fetch */
14
- /* ------------------------------------------------------------------ */
15
-
16
- const mockFetch = vi.fn();
17
-
18
- beforeEach(() => {
19
- vi.stubGlobal('fetch', mockFetch);
20
- mockFetch.mockReset();
21
- });
22
-
23
- afterEach(() => {
24
- vi.unstubAllGlobals();
25
- });
26
-
27
- function mockResponse(data: any, status = 200) {
28
- return {
29
- ok: status >= 200 && status < 300,
30
- status,
31
- json: async () => data,
32
- text: async () => JSON.stringify(data),
33
- };
34
- }
35
-
36
- /* ------------------------------------------------------------------ */
37
- /* Tests */
38
- /* ------------------------------------------------------------------ */
39
-
40
- describe('LithicCardAdapter', () => {
41
- let adapter: LithicCardAdapter;
42
-
43
- beforeEach(() => {
44
- adapter = new LithicCardAdapter({ apiKey: 'test-api-key', sandbox: true });
45
- });
46
-
47
- /* ── URL configuration ────────────────────────────────────────────── */
48
-
49
- describe('URL configuration', () => {
50
- it('should use sandbox URL by default', async () => {
51
- mockFetch.mockResolvedValue(mockResponse({ token: 'card-1', last_four: '1234' }));
52
- await adapter.getCard('card-1');
53
- expect(mockFetch).toHaveBeenCalledWith(
54
- expect.stringContaining('sandbox.lithic.com'),
55
- expect.any(Object),
56
- );
57
- });
58
-
59
- it('should use production URL when sandbox=false', async () => {
60
- const prodAdapter = new LithicCardAdapter({ apiKey: 'key', sandbox: false });
61
- mockFetch.mockResolvedValue(mockResponse({ token: 'card-1' }));
62
- await prodAdapter.getCard('card-1');
63
- expect(mockFetch).toHaveBeenCalledWith(
64
- expect.stringContaining('api.lithic.com'),
65
- expect.any(Object),
66
- );
67
- });
68
-
69
- it('should use custom baseUrl when provided', async () => {
70
- const custom = new LithicCardAdapter({ apiKey: 'key', baseUrl: 'https://custom.api.com/v1' });
71
- mockFetch.mockResolvedValue(mockResponse({ token: 'card-1' }));
72
- await custom.getCard('card-1');
73
- expect(mockFetch).toHaveBeenCalledWith(
74
- 'https://custom.api.com/v1/cards/card-1',
75
- expect.any(Object),
76
- );
77
- });
78
- });
79
-
80
- /* ── Card CRUD ────────────────────────────────────────────────────── */
81
-
82
- describe('createCard', () => {
83
- it('should POST to /cards with correct body', async () => {
84
- mockFetch.mockResolvedValue(mockResponse({
85
- token: 'card-new',
86
- last_four: '5678',
87
- type: 'VIRTUAL',
88
- state: 'OPEN',
89
- spend_limit: 50000,
90
- spend_limit_duration: 'MONTHLY',
91
- }));
92
-
93
- const result = await adapter.createCard({
94
- type: 'VIRTUAL',
95
- spend_limit: 50000,
96
- spend_limit_duration: 'MONTHLY',
97
- memo: 'test card',
98
- });
99
-
100
- expect(result.token).toBe('card-new');
101
- expect(result.last_four).toBe('5678');
102
- expect(mockFetch).toHaveBeenCalledWith(
103
- expect.stringContaining('/cards'),
104
- expect.objectContaining({ method: 'POST' }),
105
- );
106
- });
107
- });
108
-
109
- describe('getCard', () => {
110
- it('should GET /cards/:token', async () => {
111
- mockFetch.mockResolvedValue(mockResponse({ token: 'card-1', state: 'OPEN' }));
112
- const result = await adapter.getCard('card-1');
113
- expect(result.token).toBe('card-1');
114
- });
115
- });
116
-
117
- describe('updateCard', () => {
118
- it('should PATCH /cards/:token', async () => {
119
- mockFetch.mockResolvedValue(mockResponse({ token: 'card-1', spend_limit: 100000 }));
120
- await adapter.updateCard('card-1', { spend_limit: 100000 });
121
- expect(mockFetch).toHaveBeenCalledWith(
122
- expect.stringContaining('/cards/card-1'),
123
- expect.objectContaining({ method: 'PATCH' }),
124
- );
125
- });
126
- });
127
-
128
- describe('pauseCard', () => {
129
- it('should PATCH with state PAUSED', async () => {
130
- mockFetch.mockResolvedValue(mockResponse({ token: 'card-1', state: 'PAUSED' }));
131
- await adapter.pauseCard('card-1');
132
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
133
- expect(body.state).toBe('PAUSED');
134
- });
135
- });
136
-
137
- describe('resumeCard', () => {
138
- it('should PATCH with state OPEN', async () => {
139
- mockFetch.mockResolvedValue(mockResponse({ token: 'card-1', state: 'OPEN' }));
140
- await adapter.resumeCard('card-1');
141
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
142
- expect(body.state).toBe('OPEN');
143
- });
144
- });
145
-
146
- describe('closeCard', () => {
147
- it('should PATCH with state CLOSED', async () => {
148
- mockFetch.mockResolvedValue(mockResponse({ token: 'card-1', state: 'CLOSED' }));
149
- await adapter.closeCard('card-1');
150
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
151
- expect(body.state).toBe('CLOSED');
152
- });
153
- });
154
-
155
- /* ── Transactions ──────────────────────────────────────────────────── */
156
-
157
- describe('listTransactions', () => {
158
- it('should GET /transactions with card_token param', async () => {
159
- mockFetch.mockResolvedValue(mockResponse({ data: [{ token: 'tx-1', amount: 1500 }] }));
160
- const txs = await adapter.listTransactions('card-1');
161
- expect(txs.length).toBe(1);
162
- expect(txs[0].token).toBe('tx-1');
163
- expect(mockFetch).toHaveBeenCalledWith(
164
- expect.stringContaining('card_token=card-1'),
165
- expect.any(Object),
166
- );
167
- });
168
-
169
- it('should pass optional params', async () => {
170
- mockFetch.mockResolvedValue(mockResponse({ data: [] }));
171
- await adapter.listTransactions('card-1', { begin: '2026-01-01', page_size: 5 });
172
- const url = mockFetch.mock.calls[0][0];
173
- expect(url).toContain('begin=2026-01-01');
174
- expect(url).toContain('page_size=5');
175
- });
176
- });
177
-
178
- /* ── Auth rules ────────────────────────────────────────────────────── */
179
-
180
- describe('createAuthRule', () => {
181
- it('should POST to /auth_rules with card token', async () => {
182
- mockFetch.mockResolvedValue(mockResponse({}, 200));
183
- await adapter.createAuthRule('card-1', { blocked_mcc: ['5812', '5813'] });
184
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
185
- expect(body.card_tokens).toEqual(['card-1']);
186
- expect(body.blocked_mcc).toEqual(['5812', '5813']);
187
- });
188
- });
189
-
190
- /* ── Error handling ────────────────────────────────────────────────── */
191
-
192
- describe('error handling', () => {
193
- it('should throw LithicApiError on non-ok response', async () => {
194
- mockFetch.mockResolvedValue({
195
- ok: false,
196
- status: 404,
197
- text: async () => 'Card not found',
198
- });
199
-
200
- await expect(adapter.getCard('bad-token')).rejects.toThrow(LithicApiError);
201
- });
202
-
203
- it('should include status code in error', async () => {
204
- mockFetch.mockResolvedValue({
205
- ok: false,
206
- status: 403,
207
- text: async () => 'Forbidden',
208
- });
209
-
210
- try {
211
- await adapter.getCard('bad-token');
212
- } catch (err: any) {
213
- expect(err.statusCode).toBe(403);
214
- expect(err.name).toBe('LithicApiError');
215
- }
216
- });
217
-
218
- it('should include Authorization header in requests', async () => {
219
- mockFetch.mockResolvedValue(mockResponse({ token: 'card-1' }));
220
- await adapter.getCard('card-1');
221
- expect(mockFetch.mock.calls[0][1].headers['Authorization']).toBe('test-api-key');
222
- });
223
- });
224
- });