#!/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(); });