Skip to content

Commit 341813c

Browse files
mayorRyan R. Fox
authored andcommitted
WIP: Group C balance-aware tests (320 lines)
1 parent 2b7c4a9 commit 341813c

1 file changed

Lines changed: 320 additions & 0 deletions

File tree

e2e/scripts/balance-aware-tests.ts

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
/**
2+
* Balance-Aware Selection Tests (Group C)
3+
*
4+
* Tests C1-C3 validating balance-aware fallback behavior.
5+
* The selectPaymentMethod function should iterate through payment methods
6+
* in server preference order and select the first one with sufficient balance.
7+
*
8+
* | # | Server Order | Client Has | Expected |
9+
* |----|-----------------------------| --------------|-------------------------------|
10+
* | C1 | Base USDC, WETH, Solana | WETH only | WETH Permit2 |
11+
* | C2 | Base USDC, WETH, Solana | Solana only | Solana USDC |
12+
* | C3 | Base USDC, WETH, Solana | None | Error (no compatible method) |
13+
*
14+
* Usage:
15+
* pnpm tsx scripts/balance-aware-tests.ts
16+
*/
17+
18+
import {
19+
selectPaymentMethod,
20+
PaymentMethod,
21+
BalanceChecker,
22+
} from '../src/balance-aware-selector';
23+
24+
// Payment methods matching the multi-method /protected endpoint
25+
// Order matches server preference: Base USDC EIP-3009, WETH Permit2, Solana USDC
26+
const PAYMENT_METHODS: PaymentMethod[] = [
27+
{
28+
asset: 'usdc',
29+
network: 'eip155:84532', // Base Sepolia
30+
protocol: 'eip3009',
31+
},
32+
{
33+
asset: 'weth',
34+
network: 'eip155:84532', // Base Sepolia
35+
protocol: 'permit2',
36+
},
37+
{
38+
asset: 'usdc',
39+
network: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', // Solana Devnet
40+
protocol: 'exact',
41+
},
42+
];
43+
44+
interface TestResult {
45+
name: string;
46+
passed: boolean;
47+
expected: string;
48+
actual: string;
49+
error?: string;
50+
}
51+
52+
const results: TestResult[] = [];
53+
54+
function log(msg: string): void {
55+
console.log(msg);
56+
}
57+
58+
function formatMethod(method: PaymentMethod | null): string {
59+
if (!method) return 'null (no compatible method)';
60+
return `${method.asset.toUpperCase()} via ${method.protocol} on ${method.network}`;
61+
}
62+
63+
/**
64+
* Test C1: Client has WETH only
65+
* Server offers: Base USDC, WETH, Solana
66+
* Expected: WETH Permit2 (first method with balance)
67+
*/
68+
async function testC1(): Promise<TestResult> {
69+
const testName = 'C1: WETH only → WETH Permit2';
70+
log(`\n🧪 ${testName}`);
71+
72+
const balanceChecker: BalanceChecker = async (method: PaymentMethod) => {
73+
// Client only has WETH
74+
if (method.asset === 'weth' && method.network === 'eip155:84532') {
75+
return 1000000000000000000n; // 1 WETH
76+
}
77+
return 0n; // No balance for other assets
78+
};
79+
80+
try {
81+
const selected = await selectPaymentMethod(PAYMENT_METHODS, balanceChecker);
82+
const expected = PAYMENT_METHODS[1]; // WETH Permit2
83+
84+
const passed =
85+
selected !== null &&
86+
selected.asset === expected.asset &&
87+
selected.network === expected.network &&
88+
selected.protocol === expected.protocol;
89+
90+
const result: TestResult = {
91+
name: testName,
92+
passed,
93+
expected: formatMethod(expected),
94+
actual: formatMethod(selected),
95+
};
96+
97+
if (passed) {
98+
log(` ✅ Passed: Selected ${formatMethod(selected)}`);
99+
} else {
100+
log(` ❌ Failed: Expected ${formatMethod(expected)}, got ${formatMethod(selected)}`);
101+
}
102+
103+
return result;
104+
} catch (error) {
105+
const result: TestResult = {
106+
name: testName,
107+
passed: false,
108+
expected: formatMethod(PAYMENT_METHODS[1]),
109+
actual: 'exception',
110+
error: error instanceof Error ? error.message : String(error),
111+
};
112+
log(` ❌ Failed with exception: ${result.error}`);
113+
return result;
114+
}
115+
}
116+
117+
/**
118+
* Test C2: Client has Solana USDC only
119+
* Server offers: Base USDC, WETH, Solana
120+
* Expected: Solana USDC (third method, first with balance)
121+
*/
122+
async function testC2(): Promise<TestResult> {
123+
const testName = 'C2: Solana only → Solana USDC';
124+
log(`\n🧪 ${testName}`);
125+
126+
const balanceChecker: BalanceChecker = async (method: PaymentMethod) => {
127+
// Client only has Solana USDC
128+
if (method.asset === 'usdc' && method.network.startsWith('solana:')) {
129+
return 1000000n; // 1 USDC (6 decimals)
130+
}
131+
return 0n; // No balance for other assets
132+
};
133+
134+
try {
135+
const selected = await selectPaymentMethod(PAYMENT_METHODS, balanceChecker);
136+
const expected = PAYMENT_METHODS[2]; // Solana USDC
137+
138+
const passed =
139+
selected !== null &&
140+
selected.asset === expected.asset &&
141+
selected.network === expected.network &&
142+
selected.protocol === expected.protocol;
143+
144+
const result: TestResult = {
145+
name: testName,
146+
passed,
147+
expected: formatMethod(expected),
148+
actual: formatMethod(selected),
149+
};
150+
151+
if (passed) {
152+
log(` ✅ Passed: Selected ${formatMethod(selected)}`);
153+
} else {
154+
log(` ❌ Failed: Expected ${formatMethod(expected)}, got ${formatMethod(selected)}`);
155+
}
156+
157+
return result;
158+
} catch (error) {
159+
const result: TestResult = {
160+
name: testName,
161+
passed: false,
162+
expected: formatMethod(PAYMENT_METHODS[2]),
163+
actual: 'exception',
164+
error: error instanceof Error ? error.message : String(error),
165+
};
166+
log(` ❌ Failed with exception: ${result.error}`);
167+
return result;
168+
}
169+
}
170+
171+
/**
172+
* Test C3: Client has no compatible balances
173+
* Server offers: Base USDC, WETH, Solana
174+
* Expected: null (no compatible method) - should NOT crash
175+
*/
176+
async function testC3(): Promise<TestResult> {
177+
const testName = 'C3: No balance → Error (graceful null)';
178+
log(`\n🧪 ${testName}`);
179+
180+
const balanceChecker: BalanceChecker = async (_method: PaymentMethod) => {
181+
// Client has no balances
182+
return 0n;
183+
};
184+
185+
try {
186+
const selected = await selectPaymentMethod(PAYMENT_METHODS, balanceChecker);
187+
188+
// Expected behavior: return null, not throw
189+
const passed = selected === null;
190+
191+
const result: TestResult = {
192+
name: testName,
193+
passed,
194+
expected: 'null (no compatible method)',
195+
actual: formatMethod(selected),
196+
};
197+
198+
if (passed) {
199+
log(` ✅ Passed: Gracefully returned null (no compatible method)`);
200+
} else {
201+
log(` ❌ Failed: Expected null, got ${formatMethod(selected)}`);
202+
}
203+
204+
return result;
205+
} catch (error) {
206+
// If it throws, that's a failure - C3 should gracefully return null
207+
const result: TestResult = {
208+
name: testName,
209+
passed: false,
210+
expected: 'null (no compatible method)',
211+
actual: 'exception (should not throw)',
212+
error: error instanceof Error ? error.message : String(error),
213+
};
214+
log(` ❌ Failed: Should not throw, got exception: ${result.error}`);
215+
return result;
216+
}
217+
}
218+
219+
/**
220+
* Additional test: Verify first match wins
221+
* When client has multiple balances, should return first in server order
222+
*/
223+
async function testFirstMatchWins(): Promise<TestResult> {
224+
const testName = 'Bonus: First match wins (has all balances)';
225+
log(`\n🧪 ${testName}`);
226+
227+
const balanceChecker: BalanceChecker = async (_method: PaymentMethod) => {
228+
// Client has all balances
229+
return 1000000n;
230+
};
231+
232+
try {
233+
const selected = await selectPaymentMethod(PAYMENT_METHODS, balanceChecker);
234+
const expected = PAYMENT_METHODS[0]; // First method (Base USDC EIP-3009)
235+
236+
const passed =
237+
selected !== null &&
238+
selected.asset === expected.asset &&
239+
selected.network === expected.network &&
240+
selected.protocol === expected.protocol;
241+
242+
const result: TestResult = {
243+
name: testName,
244+
passed,
245+
expected: formatMethod(expected),
246+
actual: formatMethod(selected),
247+
};
248+
249+
if (passed) {
250+
log(` ✅ Passed: Selected first method ${formatMethod(selected)}`);
251+
} else {
252+
log(` ❌ Failed: Expected first method ${formatMethod(expected)}, got ${formatMethod(selected)}`);
253+
}
254+
255+
return result;
256+
} catch (error) {
257+
const result: TestResult = {
258+
name: testName,
259+
passed: false,
260+
expected: formatMethod(PAYMENT_METHODS[0]),
261+
actual: 'exception',
262+
error: error instanceof Error ? error.message : String(error),
263+
};
264+
log(` ❌ Failed with exception: ${result.error}`);
265+
return result;
266+
}
267+
}
268+
269+
async function main(): Promise<void> {
270+
log('🚀 Balance-Aware Selection Tests (Group C)');
271+
log('==========================================');
272+
log('');
273+
log('Testing selectPaymentMethod with different balance scenarios');
274+
log('Payment methods in server preference order:');
275+
PAYMENT_METHODS.forEach((m, i) => {
276+
log(` ${i + 1}. ${formatMethod(m)}`);
277+
});
278+
279+
// Run Group C tests
280+
results.push(await testC1());
281+
results.push(await testC2());
282+
results.push(await testC3());
283+
results.push(await testFirstMatchWins());
284+
285+
// Summary
286+
log('\n📊 Test Summary');
287+
log('===============');
288+
289+
const passed = results.filter((r) => r.passed).length;
290+
const failed = results.filter((r) => !r.passed).length;
291+
292+
log(`✅ Passed: ${passed}`);
293+
log(`❌ Failed: ${failed}`);
294+
log(`📈 Total: ${results.length}`);
295+
log('');
296+
297+
if (failed > 0) {
298+
log('❌ FAILED TESTS:');
299+
results
300+
.filter((r) => !r.passed)
301+
.forEach((r) => {
302+
log(` • ${r.name}`);
303+
log(` Expected: ${r.expected}`);
304+
log(` Actual: ${r.actual}`);
305+
if (r.error) {
306+
log(` Error: ${r.error}`);
307+
}
308+
});
309+
log('');
310+
process.exit(1);
311+
}
312+
313+
log('✅ All balance-aware selection tests passed!');
314+
process.exit(0);
315+
}
316+
317+
main().catch((error) => {
318+
console.error('❌ Test runner failed:', error.message);
319+
process.exit(1);
320+
});

0 commit comments

Comments
 (0)