update
This commit is contained in:
328
test/test.webhooks.ts
Normal file
328
test/test.webhooks.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as bunq from '../ts/index.js';
|
||||
import * as plugins from '../ts/bunq.plugins.js';
|
||||
|
||||
let testBunqAccount: bunq.BunqAccount;
|
||||
let sandboxApiKey: string;
|
||||
let primaryAccount: bunq.BunqMonetaryAccount;
|
||||
|
||||
tap.test('should setup webhook test environment', async () => {
|
||||
// Create sandbox user
|
||||
const tempAccount = new bunq.BunqAccount({
|
||||
apiKey: '',
|
||||
deviceName: 'bunq-webhook-test',
|
||||
environment: 'SANDBOX',
|
||||
});
|
||||
|
||||
sandboxApiKey = await tempAccount.createSandboxUser();
|
||||
console.log('Generated sandbox API key for webhook tests');
|
||||
|
||||
// Initialize bunq account
|
||||
testBunqAccount = new bunq.BunqAccount({
|
||||
apiKey: sandboxApiKey,
|
||||
deviceName: 'bunq-webhook-test',
|
||||
environment: 'SANDBOX',
|
||||
});
|
||||
|
||||
await testBunqAccount.init();
|
||||
|
||||
// Get primary account
|
||||
const accounts = await testBunqAccount.getAccounts();
|
||||
primaryAccount = accounts[0];
|
||||
|
||||
expect(primaryAccount).toBeInstanceOf(bunq.BunqMonetaryAccount);
|
||||
});
|
||||
|
||||
tap.test('should create and manage webhooks', async () => {
|
||||
const webhook = new bunq.BunqWebhook(testBunqAccount);
|
||||
|
||||
// Create a webhook
|
||||
const webhookUrl = 'https://example.com/webhook/bunq';
|
||||
const webhookId = await webhook.create(primaryAccount, webhookUrl);
|
||||
|
||||
expect(webhookId).toBeTypeofNumber();
|
||||
console.log(`Created webhook with ID: ${webhookId}`);
|
||||
|
||||
// List webhooks
|
||||
const webhooks = await webhook.list(primaryAccount);
|
||||
expect(webhooks).toBeArray();
|
||||
expect(webhooks.length).toBeGreaterThan(0);
|
||||
|
||||
const createdWebhook = webhooks.find(w => w.id === webhookId);
|
||||
expect(createdWebhook).toBeDefined();
|
||||
expect(createdWebhook?.url).toBe(webhookUrl);
|
||||
|
||||
console.log(`Found ${webhooks.length} webhooks`);
|
||||
|
||||
// Update webhook
|
||||
const updatedUrl = 'https://example.com/webhook/bunq-updated';
|
||||
await webhook.update(primaryAccount, webhookId, updatedUrl);
|
||||
|
||||
// Get updated webhook
|
||||
const updatedWebhook = await webhook.get(primaryAccount, webhookId);
|
||||
expect(updatedWebhook.url).toBe(updatedUrl);
|
||||
|
||||
// Delete webhook
|
||||
await webhook.delete(primaryAccount, webhookId);
|
||||
console.log('Webhook deleted successfully');
|
||||
|
||||
// Verify deletion
|
||||
const remainingWebhooks = await webhook.list(primaryAccount);
|
||||
const deletedWebhook = remainingWebhooks.find(w => w.id === webhookId);
|
||||
expect(deletedWebhook).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should test webhook signature verification', async () => {
|
||||
const webhook = new bunq.BunqWebhook(testBunqAccount);
|
||||
|
||||
// Create test webhook data
|
||||
const webhookBody = JSON.stringify({
|
||||
NotificationUrl: {
|
||||
target_url: 'https://example.com/webhook/bunq',
|
||||
category: 'PAYMENT',
|
||||
event_type: 'PAYMENT_CREATED',
|
||||
object: {
|
||||
Payment: {
|
||||
id: 12345,
|
||||
created: '2025-07-18 12:00:00.000000',
|
||||
updated: '2025-07-18 12:00:00.000000',
|
||||
monetary_account_id: primaryAccount.id,
|
||||
amount: {
|
||||
currency: 'EUR',
|
||||
value: '10.00'
|
||||
},
|
||||
description: 'Test webhook payment',
|
||||
type: 'BUNQ',
|
||||
sub_type: 'PAYMENT'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create a fake signature (in real scenario, this would come from bunq)
|
||||
const crypto = new bunq.BunqCrypto();
|
||||
await crypto.generateKeyPair();
|
||||
const signature = crypto.signData(webhookBody);
|
||||
|
||||
// Test signature verification (would normally use bunq's public key)
|
||||
const isValid = crypto.verifyData(webhookBody, signature, crypto.getPublicKey());
|
||||
expect(isValid).toBe(true);
|
||||
|
||||
console.log('Webhook signature verification tested');
|
||||
});
|
||||
|
||||
tap.test('should test webhook event parsing', async () => {
|
||||
// Test different webhook event types
|
||||
|
||||
// 1. Payment created event
|
||||
const paymentEvent = {
|
||||
NotificationUrl: {
|
||||
target_url: 'https://example.com/webhook/bunq',
|
||||
category: 'PAYMENT',
|
||||
event_type: 'PAYMENT_CREATED',
|
||||
object: {
|
||||
Payment: {
|
||||
id: 12345,
|
||||
amount: { currency: 'EUR', value: '10.00' },
|
||||
description: 'Payment webhook test'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expect(paymentEvent.NotificationUrl.category).toBe('PAYMENT');
|
||||
expect(paymentEvent.NotificationUrl.event_type).toBe('PAYMENT_CREATED');
|
||||
expect(paymentEvent.NotificationUrl.object.Payment).toBeDefined();
|
||||
|
||||
// 2. Request created event
|
||||
const requestEvent = {
|
||||
NotificationUrl: {
|
||||
target_url: 'https://example.com/webhook/bunq',
|
||||
category: 'REQUEST',
|
||||
event_type: 'REQUEST_INQUIRY_CREATED',
|
||||
object: {
|
||||
RequestInquiry: {
|
||||
id: 67890,
|
||||
amount_inquired: { currency: 'EUR', value: '25.00' },
|
||||
description: 'Request webhook test'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expect(requestEvent.NotificationUrl.category).toBe('REQUEST');
|
||||
expect(requestEvent.NotificationUrl.event_type).toBe('REQUEST_INQUIRY_CREATED');
|
||||
expect(requestEvent.NotificationUrl.object.RequestInquiry).toBeDefined();
|
||||
|
||||
// 3. Card transaction event
|
||||
const cardEvent = {
|
||||
NotificationUrl: {
|
||||
target_url: 'https://example.com/webhook/bunq',
|
||||
category: 'CARD_TRANSACTION',
|
||||
event_type: 'CARD_TRANSACTION_SUCCESSFUL',
|
||||
object: {
|
||||
CardTransaction: {
|
||||
id: 11111,
|
||||
amount: { currency: 'EUR', value: '50.00' },
|
||||
description: 'Card transaction webhook test',
|
||||
merchant_name: 'Test Merchant'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expect(cardEvent.NotificationUrl.category).toBe('CARD_TRANSACTION');
|
||||
expect(cardEvent.NotificationUrl.event_type).toBe('CARD_TRANSACTION_SUCCESSFUL');
|
||||
expect(cardEvent.NotificationUrl.object.CardTransaction).toBeDefined();
|
||||
|
||||
console.log('Webhook event parsing tested for multiple event types');
|
||||
});
|
||||
|
||||
tap.test('should test webhook retry mechanism', async () => {
|
||||
const webhook = new bunq.BunqWebhook(testBunqAccount);
|
||||
|
||||
// Create a webhook that will fail (invalid URL for testing)
|
||||
const failingWebhookUrl = 'https://this-will-fail-12345.example.com/webhook';
|
||||
|
||||
try {
|
||||
const webhookId = await webhook.create(primaryAccount, failingWebhookUrl);
|
||||
console.log(`Created webhook with failing URL: ${webhookId}`);
|
||||
|
||||
// In production, bunq would retry failed webhook deliveries
|
||||
// with exponential backoff: 1s, 2s, 4s, 8s, etc.
|
||||
|
||||
// Clean up
|
||||
await webhook.delete(primaryAccount, webhookId);
|
||||
} catch (error) {
|
||||
console.log('Webhook creation with invalid URL handled:', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should test webhook filtering by event type', async () => {
|
||||
const notification = new bunq.BunqNotification(testBunqAccount);
|
||||
|
||||
// Get current notification filters
|
||||
const urlFilters = await notification.listUrlFilters();
|
||||
console.log(`Current URL notification filters: ${urlFilters.length}`);
|
||||
|
||||
// Create notification filter for specific events
|
||||
try {
|
||||
const filterId = await notification.createUrlFilter({
|
||||
notification_target: 'https://example.com/webhook/filtered',
|
||||
category: ['PAYMENT', 'REQUEST']
|
||||
});
|
||||
|
||||
expect(filterId).toBeTypeofNumber();
|
||||
console.log(`Created notification filter with ID: ${filterId}`);
|
||||
|
||||
// List filters again
|
||||
const updatedFilters = await notification.listUrlFilters();
|
||||
expect(updatedFilters.length).toBeGreaterThan(urlFilters.length);
|
||||
|
||||
// Delete the filter
|
||||
await notification.deleteUrlFilter(filterId);
|
||||
console.log('Notification filter deleted successfully');
|
||||
} catch (error) {
|
||||
console.log('Notification filter creation failed:', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should test webhook security best practices', async () => {
|
||||
// Test webhook security measures
|
||||
|
||||
// 1. IP whitelisting (bunq's IPs should be whitelisted on your server)
|
||||
const bunqWebhookIPs = [
|
||||
'185.40.108.0/24', // Example bunq IP range
|
||||
'185.40.109.0/24' // Example bunq IP range
|
||||
];
|
||||
|
||||
expect(bunqWebhookIPs).toBeArray();
|
||||
expect(bunqWebhookIPs.length).toBeGreaterThan(0);
|
||||
|
||||
// 2. Signature verification is mandatory
|
||||
const webhookData = {
|
||||
body: '{"test": "data"}',
|
||||
signature: 'invalid-signature'
|
||||
};
|
||||
|
||||
// This should fail with invalid signature
|
||||
const crypto = new bunq.BunqCrypto();
|
||||
await crypto.generateKeyPair();
|
||||
|
||||
const isValidSignature = crypto.verifyData(
|
||||
webhookData.body,
|
||||
webhookData.signature,
|
||||
crypto.getPublicKey()
|
||||
);
|
||||
|
||||
expect(isValidSignature).toBe(false);
|
||||
console.log('Invalid signature correctly rejected');
|
||||
|
||||
// 3. Webhook URL should use HTTPS
|
||||
const webhookUrl = 'https://example.com/webhook/bunq';
|
||||
expect(webhookUrl).toStartWith('https://');
|
||||
|
||||
// 4. Webhook should have authentication token in URL
|
||||
const secureWebhookUrl = 'https://example.com/webhook/bunq?token=secret123';
|
||||
expect(secureWebhookUrl).toInclude('token=');
|
||||
|
||||
console.log('Webhook security best practices validated');
|
||||
});
|
||||
|
||||
tap.test('should test webhook event deduplication', async () => {
|
||||
// Test handling duplicate webhook events
|
||||
|
||||
const processedEvents = new Set<string>();
|
||||
|
||||
// Simulate receiving the same event multiple times
|
||||
const event = {
|
||||
NotificationUrl: {
|
||||
id: 'event-12345',
|
||||
target_url: 'https://example.com/webhook/bunq',
|
||||
category: 'PAYMENT',
|
||||
event_type: 'PAYMENT_CREATED',
|
||||
object: {
|
||||
Payment: {
|
||||
id: 12345
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Process event first time
|
||||
const eventId = `${event.NotificationUrl.category}-${event.NotificationUrl.object.Payment.id}`;
|
||||
|
||||
if (!processedEvents.has(eventId)) {
|
||||
processedEvents.add(eventId);
|
||||
console.log('Event processed successfully');
|
||||
}
|
||||
|
||||
// Try to process same event again
|
||||
if (!processedEvents.has(eventId)) {
|
||||
throw new Error('Duplicate event should have been caught');
|
||||
} else {
|
||||
console.log('Duplicate event correctly ignored');
|
||||
}
|
||||
|
||||
expect(processedEvents.size).toBe(1);
|
||||
});
|
||||
|
||||
tap.test('should cleanup webhook test resources', async () => {
|
||||
// Clean up any remaining webhooks
|
||||
const webhook = new bunq.BunqWebhook(testBunqAccount);
|
||||
const remainingWebhooks = await webhook.list(primaryAccount);
|
||||
|
||||
for (const wh of remainingWebhooks) {
|
||||
try {
|
||||
await webhook.delete(primaryAccount, wh.id);
|
||||
console.log(`Cleaned up webhook ${wh.id}`);
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
await testBunqAccount.stop();
|
||||
console.log('Webhook test cleanup completed');
|
||||
});
|
||||
|
||||
export default tap.start();
|
Reference in New Issue
Block a user