Refactor test files and standardize crypto signature patterns

- Create unified SignatureHelper utility for consistent crypto operations
- Create WebhookTestHelper for streamlined webhook testing
- Remove duplicate test files and consolidate functionality
- Update generate-signature.js to use new utilities and remove hardcoded secrets
- Fix webhook signature verification to handle different buffer lengths
- Standardize test patterns across webhook and unit tests

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jonathan Flatt
2025-05-25 12:27:17 -05:00
parent 1ee760d2fe
commit 39a3ec960d
10 changed files with 333 additions and 495 deletions

View File

@@ -63,7 +63,11 @@ function verifyWebhookSignature(req) {
}
// Properly verify the signature using timing-safe comparison
if (crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(calculatedSignature))) {
// Check lengths first to avoid timingSafeEqual error with different-length buffers
if (
signature.length === calculatedSignature.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(calculatedSignature))
) {
logger.debug('Webhook signature verification succeeded');
return true;
}
@@ -425,21 +429,25 @@ Please check with an administrator to review the logs for more details.`
);
// Only proceed if the check suite is for a pull request and conclusion is success
if (checkSuite.conclusion === 'success' && checkSuite.pull_requests && checkSuite.pull_requests.length > 0) {
if (
checkSuite.conclusion === 'success' &&
checkSuite.pull_requests &&
checkSuite.pull_requests.length > 0
) {
try {
// Process PRs in parallel for better performance
const prPromises = checkSuite.pull_requests.map(async (pr) => {
const prPromises = checkSuite.pull_requests.map(async pr => {
const prResult = {
prNumber: pr.number,
success: false,
error: null,
skippedReason: null
};
try {
// Extract SHA from PR data first, only fall back to check suite SHA if absolutely necessary
const commitSha = pr.head?.sha;
if (!commitSha) {
logger.error(
{
@@ -458,10 +466,10 @@ Please check with an administrator to review the logs for more details.`
prResult.error = 'Missing PR head SHA';
return prResult;
}
// Note: We rely on the check_suite conclusion being 'success'
// Note: We rely on the check_suite conclusion being 'success'
// which already indicates all checks have passed.
// The Combined Status API (legacy) won't show results for
// The Combined Status API (legacy) won't show results for
// modern GitHub Actions check runs.
// Check if we've already reviewed this PR at this commit
@@ -722,7 +730,7 @@ Please perform a comprehensive review of PR #${pr.number} in repository ${repo.f
},
'Automated PR review completed successfully'
);
// Update label to show review is complete
try {
await githubService.managePRLabels({
@@ -743,7 +751,7 @@ Please perform a comprehensive review of PR #${pr.number} in repository ${repo.f
);
// Don't fail the review if label update fails
}
prResult.success = true;
return prResult;
} catch (reviewError) {
@@ -757,7 +765,7 @@ Please perform a comprehensive review of PR #${pr.number} in repository ${repo.f
},
'Error processing automated PR review'
);
// Remove in-progress label on error
try {
await githubService.managePRLabels({
@@ -776,23 +784,25 @@ Please perform a comprehensive review of PR #${pr.number} in repository ${repo.f
'Failed to remove review-in-progress label after error'
);
}
prResult.error = reviewError.message || 'Unknown error during review';
return prResult;
}
});
// Wait for all PR reviews to complete
const results = await Promise.allSettled(prPromises);
const prResults = results.map(result =>
const prResults = results.map(result =>
result.status === 'fulfilled' ? result.value : { success: false, error: result.reason }
);
// Count successes and failures (mutually exclusive)
const successCount = prResults.filter(r => r.success).length;
const failureCount = prResults.filter(r => !r.success && r.error && !r.skippedReason).length;
const failureCount = prResults.filter(
r => !r.success && r.error && !r.skippedReason
).length;
const skippedCount = prResults.filter(r => !r.success && r.skippedReason).length;
logger.info(
{
repo: repo.full_name,
@@ -805,7 +815,7 @@ Please perform a comprehensive review of PR #${pr.number} in repository ${repo.f
},
'Check suite PR review processing completed'
);
// Return detailed status
return res.status(200).json({
success: true,
@@ -826,7 +836,7 @@ Please perform a comprehensive review of PR #${pr.number} in repository ${repo.f
},
'Error processing check suite for PR reviews'
);
return res.status(500).json({
success: false,
error: 'Failed to process check suite',
@@ -850,7 +860,7 @@ Please perform a comprehensive review of PR #${pr.number} in repository ${repo.f
},
'Check suite succeeded but no pull requests found in payload - possible fork PR'
);
// TODO: Could query GitHub API to find PRs for this branch/SHA
// For now, just acknowledge the webhook
return res.status(200).json({

View File

@@ -1,21 +1,21 @@
const crypto = require('crypto');
const fs = require('fs');
const SignatureHelper = require('./utils/signatureHelper');
const webhookSecret = '17DEE6196F8C9804EB536315536F5A44600078FDEEEA646EF2AFBFB1876F3E0F'; // Same as in .env file
const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET || 'test_secret';
const payloadPath = process.argv[2] || './test-payload.json';
const webhookUrl = process.argv[3] || 'http://localhost:3001/api/webhooks/github';
// Read the payload file
const payload = fs.readFileSync(payloadPath, 'utf8');
// Calculate the signature
const hmac = crypto.createHmac('sha256', webhookSecret);
const signature = 'sha256=' + hmac.update(payload).digest('hex');
// Calculate the signature using the utility
const signature = SignatureHelper.createGitHubSignature(payload, webhookSecret);
console.log('X-Hub-Signature-256:', signature);
console.log('\nCommand to test the webhook:');
console.log(`curl -X POST \\
http://localhost:3001/api/webhooks/github \\
${webhookUrl} \\
-H "Content-Type: application/json" \\
-H "X-GitHub-Event: issue_comment" \\
-H "X-Hub-Signature-256: ${signature}" \\
-d @${payloadPath}`);
-d @${payloadPath}`);

View File

@@ -1,66 +1,33 @@
#!/usr/bin/env node
const axios = require('axios');
const crypto = require('crypto');
// Mock GitHub issue opened event payload
const mockPayload = {
action: 'opened',
issue: {
number: 123,
title: 'Application crashes when loading user data',
body: 'The app consistently crashes when trying to load user profiles. This appears to be a critical bug affecting all users. Error occurs in the API endpoint.',
user: {
login: 'testuser'
}
},
repository: {
name: 'test-repo',
full_name: 'testowner/test-repo',
owner: {
login: 'testowner'
}
}
};
// Function to create GitHub webhook signature
function createSignature(payload, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(payload));
return `sha256=${hmac.digest('hex')}`;
}
const WebhookTestHelper = require('./utils/webhookTestHelper');
async function testIssueWebhook() {
const webhookUrl = 'http://localhost:8082/api/webhooks/github';
const webhookUrl = process.env.WEBHOOK_URL || 'http://localhost:8082/api/webhooks/github';
const secret = process.env.GITHUB_WEBHOOK_SECRET || 'test-secret';
const webhookHelper = new WebhookTestHelper(webhookUrl, secret);
try {
console.log('Testing issue webhook with mock payload...');
console.log('Issue:', mockPayload.issue.title);
console.log('Testing issue opened webhook for auto-tagging...');
const signature = createSignature(mockPayload, secret);
const response = await axios.post(webhookUrl, mockPayload, {
headers: {
'Content-Type': 'application/json',
'X-GitHub-Event': 'issues',
'X-GitHub-Delivery': 'test-delivery-id',
'X-Hub-Signature-256': signature,
'User-Agent': 'GitHub-Hookshot/test'
},
timeout: 30000
const result = await webhookHelper.testIssueOpened({
title: 'Application crashes when loading user data',
body: 'The app consistently crashes when trying to load user profiles. This appears to be a critical bug affecting all users. Error occurs in the API endpoint.'
});
console.log('✓ Webhook response status:', response.status);
console.log('✓ Response data:', response.data);
} catch (error) {
console.error('✗ Webhook test failed:');
if (error.response) {
console.error('Status:', error.response.status);
console.error('Data:', error.response.data);
if (result.success) {
console.log('✓ Webhook response status:', result.status);
console.log('✓ Response data:', result.data);
console.log('Issue:', result.payload.issue.title);
} else {
console.error('Error:', error.message);
console.error('✗ Webhook test failed:');
console.error('Status:', result.status);
console.error('Data:', result.data);
console.error('Error:', result.error);
}
} catch (error) {
console.error('✗ Unexpected error:', error.message);
}
}
@@ -68,4 +35,4 @@ if (require.main === module) {
testIssueWebhook();
}
module.exports = { testIssueWebhook, mockPayload };
module.exports = { testIssueWebhook };

View File

@@ -1,121 +0,0 @@
const http = require('http');
// const { promisify } = require('util');
const crypto = require('crypto');
const fs = require('fs');
const axios = require('axios');
// Configuration
const port = 3002; // Different from the main server
const webhookSecret = 'testing_webhook_secret';
const testPayloadPath = './test-payload.json';
const mainServerUrl = 'http://localhost:3001/api/webhooks/github';
// Create a simple webhook receiving server
const server = http.createServer(async (req, res) => {
if (req.method === 'POST') {
console.log('Received webhook request');
console.log('Headers:', req.headers);
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
console.log('Webhook Payload:', body);
// Verify signature if sent
if (req.headers['x-hub-signature-256']) {
const hmac = crypto.createHmac('sha256', webhookSecret);
const signature = 'sha256=' + hmac.update(body).digest('hex');
console.log('Expected signature:', signature);
console.log('Received signature:', req.headers['x-hub-signature-256']);
if (signature === req.headers['x-hub-signature-256']) {
console.log('✅ Signature verification passed');
} else {
console.log('❌ Signature verification failed');
}
}
// Send response
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
status: 'success',
message: 'Webhook received successfully',
timestamp: new Date().toISOString()
})
);
});
} else {
res.writeHead(404);
res.end();
}
});
// Start the server
server.listen(port, async () => {
console.log(`Test webhook receiver listening on port ${port}`);
try {
// Read the test payload
const payload = fs.readFileSync(testPayloadPath, 'utf8');
// Calculate the signature for GitHub webhook
const hmac = crypto.createHmac('sha256', webhookSecret);
const signature = 'sha256=' + hmac.update(payload).digest('hex');
console.log('Test setup:');
console.log('- Webhook receiver is running on port', port);
console.log('- Will send test payload to main server at', mainServerUrl);
console.log('- Signature calculated for GitHub webhook:', signature);
console.log('\nMake sure your .env file contains:');
console.log('GITHUB_WEBHOOK_SECRET=testing_webhook_secret');
console.log('OUTGOING_WEBHOOK_SECRET=testing_webhook_secret');
console.log(
`OUTGOING_WEBHOOK_URLS=http://localhost:${port},https://claude.jonathanflatt.org/webhook`
);
console.log('COMMENT_WEBHOOK_URLS=https://claude.jonathanflatt.org/comment-webhook');
console.log('\nYou can now manually test the webhook by running:');
console.log(`curl -X POST \\
${mainServerUrl} \\
-H "Content-Type: application/json" \\
-H "X-GitHub-Event: issue_comment" \\
-H "X-Hub-Signature-256: ${signature}" \\
-d @${testPayloadPath}`);
console.log('\nOr press Enter to send the test webhook automatically...');
// Wait for user input
process.stdin.once('data', async () => {
try {
console.log('\nSending test webhook to main server...');
// Send the webhook
const response = await axios.post(mainServerUrl, JSON.parse(payload), {
headers: {
'Content-Type': 'application/json',
'X-GitHub-Event': 'issue_comment',
'X-Hub-Signature-256': signature
}
});
console.log(`Main server response (${response.status}):`, response.data);
console.log(
'\nIf everything is set up correctly, you should see a webhook received above ☝️'
);
console.log('\nPress Ctrl+C to exit');
} catch (error) {
console.error('Error sending test webhook:', error.message);
if (error.response) {
console.error('Response data:', error.response.data);
}
}
});
} catch (error) {
console.error('Error in test setup:', error.message);
}
});

View File

@@ -1,57 +0,0 @@
const https = require('https');
const crypto = require('crypto');
const fs = require('fs');
// Configuration
const webhookSecret = '17DEE6196F8C9804EB536315536F5A44600078FDEEEA646EF2AFBFB1876F3E0F';
const payload = fs.readFileSync('./test-payload.json', 'utf8');
const url = 'https://claude.jonathanflatt.org/api/webhooks/github';
// Generate signature
const hmac = crypto.createHmac('sha256', webhookSecret);
const signature = 'sha256=' + hmac.update(payload).digest('hex');
console.log('Webhook URL:', url);
console.log('Payload:', JSON.parse(payload));
console.log('Generated signature:', signature);
// Parse URL
const urlParts = new URL(url);
// Prepare request
const options = {
hostname: urlParts.hostname,
port: urlParts.port || 443,
path: urlParts.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-GitHub-Event': 'issue_comment',
'X-Hub-Signature-256': signature,
'Content-Length': Buffer.byteLength(payload)
}
};
// Make request
const req = https.request(options, res => {
console.log(`\nResponse status: ${res.statusCode}`);
console.log('Response headers:', res.headers);
let data = '';
res.on('data', chunk => {
data += chunk;
});
res.on('end', () => {
console.log('Response body:', data);
});
});
req.on('error', e => {
console.error('Request error:', e.message);
});
// Send the request
req.write(payload);
req.end();
console.log('\nSending webhook to:', url);

View File

@@ -5,65 +5,43 @@
* instead of posting to GitHub
*/
const axios = require('axios');
const WebhookTestHelper = require('./utils/webhookTestHelper');
const API_URL = process.env.API_URL || 'http://localhost:3003';
// Sample webhook payload
const payload = {
action: 'created',
issue: {
number: 1,
title: 'Test Issue',
body: 'Test issue body'
},
comment: {
id: 123,
body: `${process.env.BOT_USERNAME || '@ClaudeBot'} Test command for webhook response`,
user: {
login: 'testuser'
}
},
repository: {
full_name: 'test/repo',
name: 'repo',
owner: {
login: 'test'
}
},
sender: {
login: 'testuser'
}
};
const secret = process.env.GITHUB_WEBHOOK_SECRET || 'test_secret';
async function testWebhookResponse() {
try {
console.log('Sending webhook request to:', `${API_URL}/api/webhooks/github`);
console.log('Payload:', JSON.stringify(payload, null, 2));
const webhookHelper = new WebhookTestHelper(`${API_URL}/api/webhooks/github`, secret);
const response = await axios.post(`${API_URL}/api/webhooks/github`, payload, {
headers: {
'Content-Type': 'application/json',
'X-GitHub-Event': 'issue_comment',
'X-Hub-Signature-256': 'sha256=dummy-signature',
'X-GitHub-Delivery': 'test-delivery-' + Date.now()
}
try {
console.log('Testing webhook response with Claude response...\n');
console.log('Sending webhook request to:', `${API_URL}/api/webhooks/github`);
const result = await webhookHelper.testIssueComment({
commentBody: `${process.env.BOT_USERNAME || '@ClaudeBot'} Test command for webhook response`,
repoOwner: 'test',
repoName: 'repo',
issueNumber: 1,
userLogin: 'testuser'
});
console.log('\nResponse Status:', response.status);
console.log('Response Data:', JSON.stringify(response.data, null, 2));
console.log('Payload:', JSON.stringify(result.payload, null, 2));
console.log('\nResponse Status:', result.status);
console.log('Response Data:', JSON.stringify(result.data, null, 2));
if (response.data.claudeResponse) {
if (result.success && result.data.claudeResponse) {
console.log('\n✅ Success! Claude response received in webhook response:');
console.log(response.data.claudeResponse);
} else {
console.log(result.data.claudeResponse);
} else if (result.success) {
console.log('\n❌ No Claude response found in webhook response');
} else {
console.log('\n❌ Webhook request failed:');
console.log('Error:', result.error);
}
} catch (error) {
console.error('\nError:', error.response ? error.response.data : error.message);
console.error('\nUnexpected error:', error.message);
throw error;
}
}
console.log('Testing webhook response with Claude response...\n');
testWebhookResponse();

View File

@@ -1,184 +0,0 @@
const crypto = require('crypto');
// Set required environment variables before requiring modules
process.env.BOT_USERNAME = '@TestBot';
process.env.NODE_ENV = 'test';
process.env.GITHUB_TOKEN = 'test_token';
process.env.AUTHORIZED_USERS = 'testuser,admin';
// Mock services before requiring actual modules
jest.mock('../../src/services/claudeService', () => ({
processCommand: jest.fn().mockResolvedValue('Claude response')
}));
jest.mock('../../src/services/githubService', () => ({
postComment: jest.fn().mockResolvedValue({ id: 456 })
}));
// Now require modules after environment and mocks are set up
const githubController = require('../../src/controllers/githubController');
const claudeService = require('../../src/services/claudeService');
const githubService = require('../../src/services/githubService');
describe('GitHub Controller', () => {
let req, res;
beforeEach(() => {
// Reset mocks
jest.clearAllMocks();
// Create request and response mocks
req = {
headers: {
'x-github-event': 'issue_comment',
'x-hub-signature-256': '',
'x-github-delivery': 'test-delivery-id'
},
body: {
action: 'created',
comment: {
body: '@TestBot Tell me about this repository',
id: 123456,
user: {
login: 'testuser'
}
},
issue: {
number: 123
},
repository: {
full_name: 'owner/repo',
name: 'repo',
owner: {
login: 'owner'
}
},
sender: {
login: 'testuser'
}
}
};
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
// Mock the environment variables
process.env.GITHUB_WEBHOOK_SECRET = 'test_secret';
// Set up the signature
const payload = JSON.stringify(req.body);
const hmac = crypto.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET);
req.headers['x-hub-signature-256'] = 'sha256=' + hmac.update(payload).digest('hex');
// Mock successful responses from services
claudeService.processCommand.mockResolvedValue('Claude response');
githubService.postComment.mockResolvedValue({ id: 456 });
});
test('should process a valid webhook with @TestBot mention', async () => {
await githubController.handleWebhook(req, res);
// Verify that Claude service was called with correct parameters
expect(claudeService.processCommand).toHaveBeenCalledWith({
repoFullName: 'owner/repo',
issueNumber: 123,
command: 'Tell me about this repository',
isPullRequest: false,
branchName: null
});
// Verify that GitHub service was called to post a comment
expect(githubService.postComment).toHaveBeenCalledWith({
repoOwner: 'owner',
repoName: 'repo',
issueNumber: 123,
body: 'Claude response'
});
// Verify response
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: true,
message: 'Command processed and response posted'
})
);
});
test('should reject a webhook with invalid signature', async () => {
// Tamper with the signature
req.headers['x-hub-signature-256'] = 'sha256=invalid_signature';
// Reset mocks before test
jest.clearAllMocks();
// Set NODE_ENV to production for this test to enable signature verification
const originalNodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
await githubController.handleWebhook(req, res);
// Restore NODE_ENV
process.env.NODE_ENV = originalNodeEnv;
// Verify that services were not called
expect(claudeService.processCommand).not.toHaveBeenCalled();
expect(githubService.postComment).not.toHaveBeenCalled();
// Verify response
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
error: 'Invalid webhook signature'
})
);
});
test('should ignore comments without @TestBot mention', async () => {
// Remove the @TestBot mention
req.body.comment.body = 'This is a regular comment';
// Update the signature
const payload = JSON.stringify(req.body);
const hmac = crypto.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET);
req.headers['x-hub-signature-256'] = 'sha256=' + hmac.update(payload).digest('hex');
await githubController.handleWebhook(req, res);
// Verify that services were not called
expect(claudeService.processCommand).not.toHaveBeenCalled();
expect(githubService.postComment).not.toHaveBeenCalled();
// Verify response
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({ message: 'Webhook processed successfully' });
});
test('should handle errors from Claude service', async () => {
// Make Claude service throw an error
claudeService.processCommand.mockRejectedValue(new Error('Claude error'));
await githubController.handleWebhook(req, res);
// Verify that GitHub service was called to post an error comment
expect(githubService.postComment).toHaveBeenCalledWith(
expect.objectContaining({
repoOwner: 'owner',
repoName: 'repo',
issueNumber: 123,
body: expect.stringContaining('An error occurred while processing your command')
})
);
// Verify response
expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: 'Failed to process command'
})
);
});
});

View File

@@ -1,4 +1,4 @@
const crypto = require('crypto');
const SignatureHelper = require('../../utils/signatureHelper');
// Set required environment variables before requiring modules
process.env.BOT_USERNAME = '@TestBot';
@@ -87,10 +87,11 @@ describe('GitHub Controller', () => {
// Mock the environment variables
process.env.GITHUB_WEBHOOK_SECRET = 'test_secret';
// Set up the signature
const payload = JSON.stringify(req.body);
const hmac = crypto.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET);
req.headers['x-hub-signature-256'] = 'sha256=' + hmac.update(payload).digest('hex');
// Set up the signature using the helper
req.headers['x-hub-signature-256'] = SignatureHelper.createGitHubSignature(
req.body,
process.env.GITHUB_WEBHOOK_SECRET
);
// Mock successful responses from services
claudeService.processCommand.mockResolvedValue('Claude response');
@@ -160,10 +161,11 @@ describe('GitHub Controller', () => {
// Remove the @TestBot mention
req.body.comment.body = 'This is a regular comment';
// Update the signature
const payload = JSON.stringify(req.body);
const hmac = crypto.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET);
req.headers['x-hub-signature-256'] = 'sha256=' + hmac.update(payload).digest('hex');
// Update the signature using the helper
req.headers['x-hub-signature-256'] = SignatureHelper.createGitHubSignature(
req.body,
process.env.GITHUB_WEBHOOK_SECRET
);
await githubController.handleWebhook(req, res);

View File

@@ -0,0 +1,53 @@
const crypto = require('crypto');
/**
* Utility for generating GitHub webhook signatures for testing
*/
class SignatureHelper {
/**
* Create a GitHub webhook signature
* @param {string|object} payload - The payload data (string or object to be stringified)
* @param {string} secret - The webhook secret
* @returns {string} - The signature in sha256=<hash> format
*/
static createGitHubSignature(payload, secret) {
const payloadString = typeof payload === 'string' ? payload : JSON.stringify(payload);
const hmac = crypto.createHmac('sha256', secret);
return `sha256=${hmac.update(payloadString).digest('hex')}`;
}
/**
* Verify a GitHub webhook signature
* @param {string|object} payload - The payload data
* @param {string} signature - The signature to verify
* @param {string} secret - The webhook secret
* @returns {boolean} - True if signature is valid
*/
static verifyGitHubSignature(payload, signature, secret) {
const expectedSignature = this.createGitHubSignature(payload, secret);
// Check lengths first to avoid timingSafeEqual error with different-length buffers
return (
signature.length === expectedSignature.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))
);
}
/**
* Create mock GitHub webhook headers with signature
* @param {string|object} payload - The payload data
* @param {string} secret - The webhook secret
* @param {string} eventType - The GitHub event type (default: 'issue_comment')
* @returns {object} - Headers object with signature and other webhook headers
*/
static createWebhookHeaders(payload, secret, eventType = 'issue_comment') {
return {
'Content-Type': 'application/json',
'X-GitHub-Event': eventType,
'X-GitHub-Delivery': `test-delivery-${Date.now()}`,
'X-Hub-Signature-256': this.createGitHubSignature(payload, secret),
'User-Agent': 'GitHub-Hookshot/test'
};
}
}
module.exports = SignatureHelper;

View File

@@ -0,0 +1,190 @@
const axios = require('axios');
const SignatureHelper = require('./signatureHelper');
/**
* Utility for testing webhook functionality
*/
class WebhookTestHelper {
constructor(webhookUrl = 'http://localhost:3001/api/webhooks/github', secret = 'test_secret') {
this.webhookUrl = webhookUrl;
this.secret = secret;
}
/**
* Create a mock issue comment payload
* @param {object} options - Configuration options
* @returns {object} - Mock GitHub issue comment payload
*/
createIssueCommentPayload(options = {}) {
const defaults = {
action: 'created',
commentBody: '@TestBot Tell me about this repository',
issueNumber: 123,
repoOwner: 'testowner',
repoName: 'test-repo',
userLogin: 'testuser'
};
const config = { ...defaults, ...options };
return {
action: config.action,
comment: {
id: Date.now(),
body: config.commentBody,
user: {
login: config.userLogin
}
},
issue: {
number: config.issueNumber
},
repository: {
name: config.repoName,
full_name: `${config.repoOwner}/${config.repoName}`,
owner: {
login: config.repoOwner
}
},
sender: {
login: config.userLogin
}
};
}
/**
* Create a mock issue opened payload for auto-tagging
* @param {object} options - Configuration options
* @returns {object} - Mock GitHub issue opened payload
*/
createIssueOpenedPayload(options = {}) {
const defaults = {
title: 'Application crashes when loading user data',
body: 'The app consistently crashes when trying to load user profiles. This appears to be a critical bug affecting all users. Error occurs in the API endpoint.',
issueNumber: 123,
repoOwner: 'testowner',
repoName: 'test-repo',
userLogin: 'testuser'
};
const config = { ...defaults, ...options };
return {
action: 'opened',
issue: {
number: config.issueNumber,
title: config.title,
body: config.body,
user: {
login: config.userLogin
}
},
repository: {
name: config.repoName,
full_name: `${config.repoOwner}/${config.repoName}`,
owner: {
login: config.repoOwner
}
}
};
}
/**
* Send a webhook request
* @param {object} payload - The webhook payload
* @param {string} eventType - The GitHub event type
* @param {object} options - Additional options
* @returns {Promise<object>} - Axios response
*/
async sendWebhook(payload, eventType = 'issue_comment', options = {}) {
const headers = SignatureHelper.createWebhookHeaders(payload, this.secret, eventType);
const config = {
headers,
timeout: options.timeout || 30000,
...options
};
return axios.post(this.webhookUrl, payload, config);
}
/**
* Test issue comment webhook
* @param {object} commentOptions - Options for comment payload
* @returns {Promise<object>} - Test result
*/
async testIssueComment(commentOptions = {}) {
const payload = this.createIssueCommentPayload(commentOptions);
try {
const response = await this.sendWebhook(payload, 'issue_comment');
return {
success: true,
status: response.status,
data: response.data,
payload
};
} catch (error) {
return {
success: false,
error: error.message,
status: error.response?.status,
data: error.response?.data,
payload
};
}
}
/**
* Test issue opened webhook for auto-tagging
* @param {object} issueOptions - Options for issue payload
* @returns {Promise<object>} - Test result
*/
async testIssueOpened(issueOptions = {}) {
const payload = this.createIssueOpenedPayload(issueOptions);
try {
const response = await this.sendWebhook(payload, 'issues');
return {
success: true,
status: response.status,
data: response.data,
payload
};
} catch (error) {
return {
success: false,
error: error.message,
status: error.response?.status,
data: error.response?.data,
payload
};
}
}
/**
* Generate curl command for manual testing
* @param {object} payload - The webhook payload
* @param {string} eventType - The GitHub event type
* @returns {string} - Curl command
*/
generateCurlCommand(payload, eventType = 'issue_comment') {
const headers = SignatureHelper.createWebhookHeaders(payload, this.secret, eventType);
const payloadFile = 'webhook-payload.json';
let cmd = '# Save payload to file:\n';
cmd += `echo '${JSON.stringify(payload, null, 2)}' > ${payloadFile}\n\n`;
cmd += '# Send webhook:\n';
cmd += `curl -X POST \\\n ${this.webhookUrl} \\\n`;
Object.entries(headers).forEach(([key, value]) => {
cmd += ` -H "${key}: ${value}" \\\n`;
});
cmd += ` -d @${payloadFile}`;
return cmd;
}
}
module.exports = WebhookTestHelper;