Skip to content

Commit d9a9ff1

Browse files
furiosaRyan R. Fox
authored andcommitted
Add edge case tests (Group E) for balance-aware selector
Implement tests E1-E2 validating edge cases: - E1: Server offers only Base USDC, client has only Solana -> null - E2: Server offers EIP-3009 and Permit2, client has USDC -> EIP-3009 Both tests verify graceful handling when there's no overlap and proper protocol prioritization when multiple protocols are available.
1 parent 341813c commit d9a9ff1

2 files changed

Lines changed: 217 additions & 0 deletions

File tree

e2e/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"scripts": {
77
"test": "tsx test.ts",
88
"test:unit": "tsx src/balance-aware-selector.test.ts",
9+
"test:balance-e": "tsx scripts/balance-aware-tests-e.ts",
910
"test:watch": "tsc --watch",
1011
"setup": "./setup.sh",
1112
"setup:legacy": "./setup.sh --legacy",
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/**
2+
* Balance-Aware Selection Tests (Group E: Edge Cases)
3+
*
4+
* Tests E1-E2 validating edge case scenarios.
5+
*
6+
* | # | Scenario | Expected |
7+
* |----|----------------------------------------------------|-----------------------------|
8+
* | E1 | Server offers only Base USDC, client has Solana | null (no compatible method) |
9+
* | E2 | Server offers EIP-3009 and Permit2, client has USDC| EIP-3009 (server first) |
10+
*
11+
* Usage:
12+
* pnpm tsx scripts/balance-aware-tests-e.ts
13+
*/
14+
15+
import {
16+
selectPaymentMethod,
17+
PaymentMethod,
18+
BalanceChecker,
19+
} from '../src/balance-aware-selector';
20+
21+
// Payment methods for edge case testing
22+
const BASE_USDC_EIP3009: PaymentMethod = {
23+
asset: 'usdc',
24+
network: 'eip155:84532', // Base Sepolia
25+
protocol: 'eip3009',
26+
};
27+
28+
const BASE_USDC_PERMIT2: PaymentMethod = {
29+
asset: 'usdc',
30+
network: 'eip155:84532', // Base Sepolia
31+
protocol: 'permit2',
32+
};
33+
34+
const SOLANA_USDC: PaymentMethod = {
35+
asset: 'usdc',
36+
network: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', // Solana Devnet
37+
protocol: 'exact',
38+
};
39+
40+
interface TestResult {
41+
name: string;
42+
passed: boolean;
43+
expected: string;
44+
actual: string;
45+
error?: string;
46+
}
47+
48+
const results: TestResult[] = [];
49+
50+
function log(msg: string): void {
51+
console.log(msg);
52+
}
53+
54+
function formatMethod(method: PaymentMethod | null): string {
55+
if (!method) return 'null (no compatible method)';
56+
return `${method.asset.toUpperCase()} via ${method.protocol} on ${method.network}`;
57+
}
58+
59+
/**
60+
* Test E1: No overlap between server offers and client capabilities
61+
* Server offers: Base USDC only
62+
* Client has: Solana USDC only
63+
* Expected: null (no compatible method) - graceful error
64+
*/
65+
async function testE1(): Promise<TestResult> {
66+
const testName = 'E1: Server Base only, Client Solana only → Error';
67+
log(`\n🧪 ${testName}`);
68+
69+
const serverAccepts = [BASE_USDC_EIP3009]; // Server only accepts Base USDC
70+
71+
const balanceChecker: BalanceChecker = async (method: PaymentMethod) => {
72+
// Client only has Solana USDC - no balance for Base
73+
if (method.network.startsWith('solana:') && method.asset === 'usdc') {
74+
return 1000000n; // 1 USDC (6 decimals)
75+
}
76+
return 0n; // No balance for Base USDC
77+
};
78+
79+
try {
80+
const selected = await selectPaymentMethod(serverAccepts, balanceChecker);
81+
82+
// Expected: null because client has no balance for any server-accepted method
83+
const passed = selected === null;
84+
85+
const result: TestResult = {
86+
name: testName,
87+
passed,
88+
expected: 'null (no compatible method)',
89+
actual: formatMethod(selected),
90+
};
91+
92+
if (passed) {
93+
log(` ✅ Passed: Gracefully returned null (no overlap)`);
94+
} else {
95+
log(` ❌ Failed: Expected null, got ${formatMethod(selected)}`);
96+
}
97+
98+
return result;
99+
} catch (error) {
100+
// Should not throw - should return null gracefully
101+
const result: TestResult = {
102+
name: testName,
103+
passed: false,
104+
expected: 'null (no compatible method)',
105+
actual: 'exception (should not throw)',
106+
error: error instanceof Error ? error.message : String(error),
107+
};
108+
log(` ❌ Failed: Should return null, not throw: ${result.error}`);
109+
return result;
110+
}
111+
}
112+
113+
/**
114+
* Test E2: Server offers multiple protocols for same asset
115+
* Server offers: Base USDC EIP-3009, Base USDC Permit2 (in that order)
116+
* Client has: USDC balance
117+
* Expected: EIP-3009 (first in server preference)
118+
*/
119+
async function testE2(): Promise<TestResult> {
120+
const testName = 'E2: Server offers EIP-3009 + Permit2, Client has USDC → EIP-3009';
121+
log(`\n🧪 ${testName}`);
122+
123+
// Server lists EIP-3009 first, then Permit2
124+
const serverAccepts = [BASE_USDC_EIP3009, BASE_USDC_PERMIT2];
125+
126+
const balanceChecker: BalanceChecker = async (method: PaymentMethod) => {
127+
// Client has USDC on Base (works with both protocols)
128+
if (method.network === 'eip155:84532' && method.asset === 'usdc') {
129+
return 1000000n; // 1 USDC (6 decimals)
130+
}
131+
return 0n;
132+
};
133+
134+
try {
135+
const selected = await selectPaymentMethod(serverAccepts, balanceChecker);
136+
const expected = BASE_USDC_EIP3009; // First method wins
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 EIP-3009 (server's first choice)`);
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(BASE_USDC_EIP3009),
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+
async function main(): Promise<void> {
172+
log('🚀 Balance-Aware Selection Tests (Group E: Edge Cases)');
173+
log('=======================================================');
174+
log('');
175+
log('Testing edge case scenarios for selectPaymentMethod');
176+
177+
// Run Group E tests
178+
results.push(await testE1());
179+
results.push(await testE2());
180+
181+
// Summary
182+
log('\n📊 Test Summary - Group E: Edge Cases');
183+
log('======================================');
184+
185+
const passed = results.filter((r) => r.passed).length;
186+
const failed = results.filter((r) => !r.passed).length;
187+
188+
log(`✅ Passed: ${passed}`);
189+
log(`❌ Failed: ${failed}`);
190+
log(`📈 Total: ${results.length}`);
191+
log('');
192+
193+
if (failed > 0) {
194+
log('❌ FAILED TESTS:');
195+
results
196+
.filter((r) => !r.passed)
197+
.forEach((r) => {
198+
log(` • ${r.name}`);
199+
log(` Expected: ${r.expected}`);
200+
log(` Actual: ${r.actual}`);
201+
if (r.error) {
202+
log(` Error: ${r.error}`);
203+
}
204+
});
205+
log('');
206+
process.exit(1);
207+
}
208+
209+
log('✅ All Group E edge case tests passed!');
210+
process.exit(0);
211+
}
212+
213+
main().catch((error) => {
214+
console.error('❌ Test runner failed:', error.message);
215+
process.exit(1);
216+
});

0 commit comments

Comments
 (0)