import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as bunq from '../ts/index.js'; let testBunqAccount: bunq.BunqAccount; let sandboxApiKey: string; let primaryAccount: bunq.BunqMonetaryAccount; tap.test('should setup error test environment', async () => { // Create sandbox user const tempAccount = new bunq.BunqAccount({ apiKey: '', deviceName: 'bunq-error-test', environment: 'SANDBOX', }); sandboxApiKey = await tempAccount.createSandboxUser(); // Initialize bunq account testBunqAccount = new bunq.BunqAccount({ apiKey: sandboxApiKey, deviceName: 'bunq-error-test', environment: 'SANDBOX', }); await testBunqAccount.init(); // Get primary account const { accounts } = await testBunqAccount.getAccounts(); primaryAccount = accounts[0]; expect(primaryAccount).toBeInstanceOf(bunq.BunqMonetaryAccount); console.log('Error test environment setup complete'); }); tap.test('should handle invalid API key errors', async () => { const invalidAccount = new bunq.BunqAccount({ apiKey: 'invalid_api_key_12345', deviceName: 'bunq-invalid-key', environment: 'SANDBOX', }); try { await invalidAccount.init(); throw new Error('Should have thrown error for invalid API key'); } catch (error) { console.log('Actual error message:', error.message); expect(error).toBeInstanceOf(Error); // The actual error message might vary, just check it's an auth error expect(error.message.toLowerCase()).toMatch(/invalid|incorrect|unauthorized|authentication|credentials/); console.log('Invalid API key error handled correctly'); } }); tap.test('should handle network errors', async () => { // Create account with invalid base URL const networkErrorAccount = new bunq.BunqAccount({ apiKey: sandboxApiKey, deviceName: 'bunq-network-error', environment: 'SANDBOX', }); // Skip this test - can't simulate network error without modifying private properties console.log('Network error test skipped - cannot simulate network error properly'); }); tap.test('should handle rate limiting errors', async () => { // bunq has rate limits: 3 requests per 3 seconds for some endpoints const requests = []; // Try to make many requests quickly for (let i = 0; i < 5; i++) { requests.push(testBunqAccount.getAccounts()); } try { await Promise.all(requests); console.log('Rate limit not reached (sandbox may have different limits)'); } catch (error) { if (error.message.includes('Rate limit')) { console.log('Rate limit error handled correctly'); } else { console.log('Other error occurred:', error.message); } } }); tap.test('should handle insufficient funds errors', async () => { // Try to create a payment larger than account balance try { const payment = await bunq.BunqPayment.builder(testBunqAccount, primaryAccount) .amount('1000000.00', 'EUR') // 1 million EUR .toIban('NL91ABNA0417164300', 'Large Payment Test') .description('This should fail due to insufficient funds') .create(); console.log('Payment created (sandbox may not enforce balance limits)'); } catch (error) { expect(error).toBeInstanceOf(Error); if (error.message.includes('Insufficient balance')) { console.log('Insufficient funds error handled correctly'); } else { console.log('Payment failed with:', error.message); } } }); tap.test('should handle invalid IBAN errors', async () => { try { const payment = await bunq.BunqPayment.builder(testBunqAccount, primaryAccount) .amount('1.00', 'EUR') .toIban('INVALID_IBAN_12345', 'Invalid IBAN Test') .description('This should fail due to invalid IBAN') .create(); throw new Error('Should have thrown error for invalid IBAN'); } catch (error) { expect(error).toBeInstanceOf(Error); console.log('Invalid IBAN error handled correctly:', error.message); } }); tap.test('should handle invalid currency errors', async () => { try { const payment = await bunq.BunqPayment.builder(testBunqAccount, primaryAccount) .amount('10.00', 'XYZ') // Invalid currency .toIban('NL91ABNA0417164300', 'Invalid Currency Test') .description('This should fail due to invalid currency') .create(); throw new Error('Should have thrown error for invalid currency'); } catch (error) { expect(error).toBeInstanceOf(Error); console.log('Invalid currency error handled correctly:', error.message); } }); tap.test('should handle permission errors', async () => { // Try to access another user's resources try { const httpClient = testBunqAccount['apiContext'].getHttpClient(); await httpClient.get('/v1/user/999999/monetary-account'); // Non-existent user throw new Error('Should have thrown permission error'); } catch (error) { expect(error).toBeInstanceOf(Error); console.log('Permission error handled correctly:', error.message); } }); tap.test('should handle malformed request errors', async () => { try { const httpClient = testBunqAccount['apiContext'].getHttpClient(); // Send malformed JSON await httpClient.post('/v1/user/' + testBunqAccount.userId + '/monetary-account', { // Missing required fields invalid_field: 'test' }); throw new Error('Should have thrown error for malformed request'); } catch (error) { expect(error).toBeInstanceOf(Error); console.log('Malformed request error handled correctly:', error.message); } }); tap.test('should handle BunqApiError properly', async () => { // Test custom BunqApiError class try { // Make a request that will return an error const httpClient = testBunqAccount['apiContext'].getHttpClient(); await httpClient.post('/v1/user/' + testBunqAccount.userId + '/card', { // Invalid card creation request type: 'INVALID_TYPE' }); } catch (error) { if (error instanceof bunq.BunqApiError) { expect(error.errors).toBeArray(); expect(error.errors.length).toBeGreaterThan(0); expect(error.errors[0]).toHaveProperty('error_description'); console.log('BunqApiError structure validated:', error.message); } else { console.log('Other error type:', error.message); } } }); tap.test('should handle timeout errors', async () => { // Create HTTP client with very short timeout const shortTimeoutAccount = new bunq.BunqAccount({ apiKey: sandboxApiKey, deviceName: 'bunq-timeout-test', environment: 'SANDBOX', }); // Note: smartrequest doesn't expose timeout configuration directly // In production, you would configure timeouts appropriately console.log('Timeout handling depends on HTTP client configuration'); }); tap.test('should handle concurrent modification errors', async () => { // Test optimistic locking / concurrent modification scenarios // Get account details const account = primaryAccount; // Simulate concurrent updates try { // Two "simultaneous" updates to same resource const update1 = bunq.BunqMonetaryAccount.update(testBunqAccount, account.id, { description: 'Update 1' }); const update2 = bunq.BunqMonetaryAccount.update(testBunqAccount, account.id, { description: 'Update 2' }); await Promise.all([update1, update2]); console.log('Concurrent updates completed (sandbox may not enforce locking)'); } catch (error) { console.log('Concurrent modification error:', error.message); } }); tap.test('should handle signature verification errors', async () => { const crypto = new bunq.BunqCrypto(); await crypto.generateKeyPair(); // Test with invalid signature const invalidSignature = 'invalid_signature_12345'; const data = 'test data'; try { const isValid = crypto.verifyData(data, invalidSignature, crypto.getPublicKey()); expect(isValid).toEqual(false); console.log('Invalid signature correctly rejected'); } catch (error) { console.log('Signature verification error:', error.message); } }); tap.test('should handle environment mismatch errors', async () => { // Try using sandbox API key in production environment const mismatchAccount = new bunq.BunqAccount({ apiKey: sandboxApiKey, // Sandbox key deviceName: 'bunq-env-mismatch', environment: 'PRODUCTION', // Production environment }); try { await mismatchAccount.init(); throw new Error('Should have thrown error for environment mismatch'); } catch (error) { expect(error).toBeInstanceOf(Error); console.log('Environment mismatch error handled correctly'); } }); tap.test('should test error recovery strategies', async () => { // Test that client can recover from errors // 1. Recover from temporary network error let retryCount = 0; const maxRetries = 3; async function retryableOperation() { try { retryCount++; if (retryCount < 2) { throw new Error('Simulated network error'); } return await testBunqAccount.getAccounts(); } catch (error) { if (retryCount < maxRetries) { console.log(`Retry attempt ${retryCount} after error: ${error.message}`); // Add delay to avoid rate limiting await new Promise(resolve => setTimeout(resolve, 3500)); return retryableOperation(); } throw error; } } const accounts = await retryableOperation(); expect(accounts.accounts).toBeArray(); console.log('Error recovery with retry successful'); // 2. Recover from expired session // This is handled automatically by the session manager console.log('Session expiry recovery is handled automatically'); }); tap.test('should cleanup error test resources', async () => { await testBunqAccount.stop(); console.log('Error test cleanup completed'); }); // Export custom error class for testing export class BunqApiError extends Error { public errors: Array<{ error_description: string; error_description_translated: string; }>; constructor(errors: Array) { const message = errors.map(e => e.error_description).join('; '); super(message); this.name = 'BunqApiError'; this.errors = errors; } } export default tap.start();