295 lines
9.9 KiB
JavaScript
295 lines
9.9 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* Webhook Test Script
|
||
*
|
||
* This script creates a local webhook receiver and tests the webhook system.
|
||
*
|
||
* Usage:
|
||
* 1. Start your backend server (docker-compose up or npm run dev)
|
||
* 2. Run this script: node test-webhook.js
|
||
* 3. The script will:
|
||
* - Start a local webhook receiver on port 3001
|
||
* - Create a webhook in the API
|
||
* - Send a test webhook
|
||
* - Wait for incoming webhooks
|
||
*/
|
||
|
||
const express = require('express');
|
||
const crypto = require('crypto');
|
||
const fetch = require('node-fetch');
|
||
|
||
// Configuration
|
||
const API_URL = process.env.API_URL || 'http://localhost:5000';
|
||
const WEBHOOK_PORT = process.env.WEBHOOK_PORT || 3001;
|
||
const WEBHOOK_SECRET = 'test_secret_' + Math.random().toString(36).substring(7);
|
||
|
||
// You need to set this - get it from: curl -X POST http://localhost:5000/api/auth/login -H "Content-Type: application/json" -d '{"key":"YOUR_ADMIN_KEY"}'
|
||
const JWT_TOKEN = process.env.JWT_TOKEN || '';
|
||
|
||
if (!JWT_TOKEN) {
|
||
console.error('\n❌ ERROR: JWT_TOKEN not set!');
|
||
console.error('\nPlease set your JWT token:');
|
||
console.error(' 1. Get token: curl -X POST http://localhost:5000/api/auth/login -H "Content-Type: application/json" -d \'{"key":"YOUR_ADMIN_KEY"}\'');
|
||
console.error(' 2. Run: JWT_TOKEN="your_token_here" node test-webhook.js');
|
||
console.error(' OR: export JWT_TOKEN="your_token_here" && node test-webhook.js\n');
|
||
process.exit(1);
|
||
}
|
||
|
||
let webhookId = null;
|
||
let receivedWebhooks = [];
|
||
|
||
// Create Express app for webhook receiver
|
||
const app = express();
|
||
|
||
// Important: Parse JSON and keep raw body for signature verification
|
||
app.use(express.json({
|
||
verify: (req, res, buf) => {
|
||
req.rawBody = buf.toString('utf8');
|
||
}
|
||
}));
|
||
|
||
// Webhook receiver endpoint
|
||
app.post('/webhook/jackbox', (req, res) => {
|
||
console.log('\n📨 Webhook received!');
|
||
console.log('Headers:', {
|
||
'x-webhook-signature': req.headers['x-webhook-signature'],
|
||
'x-webhook-event': req.headers['x-webhook-event'],
|
||
'user-agent': req.headers['user-agent']
|
||
});
|
||
|
||
const signature = req.headers['x-webhook-signature'];
|
||
|
||
// Verify signature
|
||
if (!signature || !signature.startsWith('sha256=')) {
|
||
console.log('❌ Missing or invalid signature format');
|
||
return res.status(401).send('Missing or invalid signature');
|
||
}
|
||
|
||
const expectedSignature = 'sha256=' + crypto
|
||
.createHmac('sha256', WEBHOOK_SECRET)
|
||
.update(req.rawBody)
|
||
.digest('hex');
|
||
|
||
// Timing-safe comparison
|
||
let isValid = false;
|
||
try {
|
||
isValid = crypto.timingSafeEqual(
|
||
Buffer.from(signature),
|
||
Buffer.from(expectedSignature)
|
||
);
|
||
} catch (err) {
|
||
console.log('❌ Signature verification failed:', err.message);
|
||
return res.status(401).send('Invalid signature');
|
||
}
|
||
|
||
if (!isValid) {
|
||
console.log('❌ Signature mismatch!');
|
||
console.log(' Expected:', expectedSignature);
|
||
console.log(' Received:', signature);
|
||
return res.status(401).send('Invalid signature');
|
||
}
|
||
|
||
console.log('✅ Signature verified!');
|
||
console.log('\nPayload:', JSON.stringify(req.body, null, 2));
|
||
|
||
if (req.body.event === 'game.added') {
|
||
const game = req.body.data.game;
|
||
console.log(`\n🎮 Game Added: ${game.title} from ${game.pack_name}`);
|
||
console.log(` Players: ${game.min_players}-${game.max_players}`);
|
||
console.log(` Session ID: ${req.body.data.session.id}`);
|
||
console.log(` Games Played: ${req.body.data.session.games_played}`);
|
||
}
|
||
|
||
receivedWebhooks.push(req.body);
|
||
res.status(200).send('OK');
|
||
});
|
||
|
||
// Health check
|
||
app.get('/health', (req, res) => {
|
||
res.json({
|
||
status: 'ok',
|
||
webhooksReceived: receivedWebhooks.length
|
||
});
|
||
});
|
||
|
||
// Start webhook receiver
|
||
const server = app.listen(WEBHOOK_PORT, async () => {
|
||
console.log(`\n🚀 Webhook receiver started on http://localhost:${WEBHOOK_PORT}`);
|
||
console.log(`📍 Webhook endpoint: http://localhost:${WEBHOOK_PORT}/webhook/jackbox`);
|
||
console.log(`🔐 Secret: ${WEBHOOK_SECRET}\n`);
|
||
|
||
// Wait a moment for server to be ready
|
||
await new Promise(resolve => setTimeout(resolve, 500));
|
||
|
||
// Run tests
|
||
await runTests();
|
||
});
|
||
|
||
async function runTests() {
|
||
console.log('═══════════════════════════════════════════════════════');
|
||
console.log('Starting Webhook Tests');
|
||
console.log('═══════════════════════════════════════════════════════\n');
|
||
|
||
try {
|
||
// Test 1: Create webhook
|
||
console.log('📝 Test 1: Creating webhook...');
|
||
const createResponse = await fetch(`${API_URL}/api/webhooks`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${JWT_TOKEN}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
name: 'Test Webhook',
|
||
url: `http://localhost:${WEBHOOK_PORT}/webhook/jackbox`,
|
||
secret: WEBHOOK_SECRET,
|
||
events: ['game.added']
|
||
})
|
||
});
|
||
|
||
if (!createResponse.ok) {
|
||
const error = await createResponse.text();
|
||
throw new Error(`Failed to create webhook: ${createResponse.status} ${error}`);
|
||
}
|
||
|
||
const webhook = await createResponse.json();
|
||
webhookId = webhook.id;
|
||
console.log(`✅ Webhook created with ID: ${webhookId}\n`);
|
||
|
||
// Test 2: List webhooks
|
||
console.log('📝 Test 2: Listing webhooks...');
|
||
const listResponse = await fetch(`${API_URL}/api/webhooks`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${JWT_TOKEN}`
|
||
}
|
||
});
|
||
|
||
if (!listResponse.ok) {
|
||
throw new Error(`Failed to list webhooks: ${listResponse.status}`);
|
||
}
|
||
|
||
const webhooks = await listResponse.json();
|
||
console.log(`✅ Found ${webhooks.length} webhook(s)\n`);
|
||
|
||
// Test 3: Send test webhook
|
||
console.log('📝 Test 3: Sending test webhook...');
|
||
const testResponse = await fetch(`${API_URL}/api/webhooks/test/${webhookId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${JWT_TOKEN}`
|
||
}
|
||
});
|
||
|
||
if (!testResponse.ok) {
|
||
throw new Error(`Failed to send test webhook: ${testResponse.status}`);
|
||
}
|
||
|
||
console.log('✅ Test webhook sent\n');
|
||
|
||
// Wait for webhook to be received
|
||
console.log('⏳ Waiting for webhook delivery (5 seconds)...');
|
||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||
|
||
if (receivedWebhooks.length > 0) {
|
||
console.log(`✅ Received ${receivedWebhooks.length} webhook(s)!\n`);
|
||
} else {
|
||
console.log('⚠️ No webhooks received yet. Check webhook logs.\n');
|
||
}
|
||
|
||
// Test 4: Check webhook logs
|
||
console.log('📝 Test 4: Checking webhook logs...');
|
||
const logsResponse = await fetch(`${API_URL}/api/webhooks/${webhookId}/logs?limit=10`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${JWT_TOKEN}`
|
||
}
|
||
});
|
||
|
||
if (!logsResponse.ok) {
|
||
throw new Error(`Failed to get webhook logs: ${logsResponse.status}`);
|
||
}
|
||
|
||
const logs = await logsResponse.json();
|
||
console.log(`✅ Found ${logs.length} log entries\n`);
|
||
|
||
if (logs.length > 0) {
|
||
console.log('Recent webhook deliveries:');
|
||
logs.forEach((log, i) => {
|
||
console.log(` ${i + 1}. Event: ${log.event_type}, Status: ${log.response_status || 'pending'}, Time: ${log.created_at}`);
|
||
if (log.error_message) {
|
||
console.log(` Error: ${log.error_message}`);
|
||
}
|
||
});
|
||
console.log('');
|
||
}
|
||
|
||
// Summary
|
||
console.log('═══════════════════════════════════════════════════════');
|
||
console.log('Test Summary');
|
||
console.log('═══════════════════════════════════════════════════════');
|
||
console.log(`✅ Webhook created: ID ${webhookId}`);
|
||
console.log(`✅ Webhooks received: ${receivedWebhooks.length}`);
|
||
console.log(`✅ Webhook logs: ${logs.length} entries`);
|
||
console.log('═══════════════════════════════════════════════════════\n');
|
||
|
||
console.log('🎉 All tests completed!');
|
||
console.log('\n💡 Next steps:');
|
||
console.log(' 1. Add a game to an active session in the Picker page');
|
||
console.log(' 2. Watch for the webhook to be received here');
|
||
console.log(' 3. Press Ctrl+C to cleanup and exit\n');
|
||
|
||
} catch (error) {
|
||
console.error('\n❌ Test failed:', error.message);
|
||
console.error('\nMake sure:');
|
||
console.error(' - Backend server is running (http://localhost:5000)');
|
||
console.error(' - JWT_TOKEN is valid');
|
||
console.error(' - Port 3001 is available\n');
|
||
await cleanup();
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
async function cleanup() {
|
||
console.log('\n🧹 Cleaning up...');
|
||
|
||
if (webhookId) {
|
||
try {
|
||
console.log(` Deleting webhook ${webhookId}...`);
|
||
const deleteResponse = await fetch(`${API_URL}/api/webhooks/${webhookId}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Authorization': `Bearer ${JWT_TOKEN}`
|
||
}
|
||
});
|
||
|
||
if (deleteResponse.ok) {
|
||
console.log(' ✅ Webhook deleted');
|
||
} else {
|
||
console.log(' ⚠️ Could not delete webhook (you may need to delete it manually)');
|
||
}
|
||
} catch (err) {
|
||
console.log(' ⚠️ Error during cleanup:', err.message);
|
||
}
|
||
}
|
||
|
||
console.log(' Stopping webhook receiver...');
|
||
server.close(() => {
|
||
console.log(' ✅ Server stopped');
|
||
console.log('\n👋 Goodbye!\n');
|
||
process.exit(0);
|
||
});
|
||
}
|
||
|
||
// Handle Ctrl+C
|
||
process.on('SIGINT', async () => {
|
||
console.log('\n\n⚠️ Received interrupt signal');
|
||
await cleanup();
|
||
});
|
||
|
||
// Handle errors
|
||
process.on('uncaughtException', async (err) => {
|
||
console.error('\n❌ Uncaught exception:', err);
|
||
await cleanup();
|
||
});
|
||
|