update
This commit is contained in:
319
test/test.errors.ts
Normal file
319
test/test.errors.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
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) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error.message).toInclude('User credentials are incorrect');
|
||||
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',
|
||||
});
|
||||
|
||||
// Override base URL to simulate network error
|
||||
const apiContext = networkErrorAccount['apiContext'];
|
||||
apiContext['context'].baseUrl = 'https://invalid-url-12345.bunq.com';
|
||||
|
||||
try {
|
||||
await networkErrorAccount.init();
|
||||
throw new Error('Should have thrown network error');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
console.log('Network error handled correctly:', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
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).toBe(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}`);
|
||||
return retryableOperation();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = await retryableOperation();
|
||||
expect(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<any>) {
|
||||
const message = errors.map(e => e.error_description).join('; ');
|
||||
super(message);
|
||||
this.name = 'BunqApiError';
|
||||
this.errors = errors;
|
||||
}
|
||||
}
|
||||
|
||||
export default tap.start();
|
Reference in New Issue
Block a user