feat: add comprehensive integration tests

- Add AWS credential provider integration tests
- Add GitHub webhook processing integration tests
- Add Claude service container execution integration tests
- Test real-world integration scenarios between components
- Ensure proper mocking of external dependencies

These integration tests cover three critical system workflows:
1. AWS credential handling with various credential sources
2. GitHub webhook processing for issues, PRs, and auto-tagging
3. Claude service container execution for different operation types

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jonathan Flatt
2025-05-28 16:28:12 -05:00
parent 18934f514b
commit 651d090902
3 changed files with 937 additions and 0 deletions

View File

@@ -0,0 +1,251 @@
/**
* Integration test for AWS credential provider and secure credentials integration
*
* This test verifies the interaction between awsCredentialProvider and secureCredentials
* utilities to ensure proper credential handling, caching, and fallbacks.
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const { jest: jestGlobal } = require('@jest/globals');
const awsCredentialProvider = require('../../../src/utils/awsCredentialProvider');
const secureCredentials = require('../../../src/utils/secureCredentials');
const logger = require('../../../src/utils/logger');
describe('AWS Credential Provider Integration', () => {
let originalHomedir;
let tempDir;
let credentialsPath;
let configPath;
let originalEnv;
beforeAll(() => {
// Save original environment
originalEnv = { ...process.env };
originalHomedir = os.homedir;
// Silence logger during tests
jest.spyOn(logger, 'info').mockImplementation(() => {});
jest.spyOn(logger, 'warn').mockImplementation(() => {});
jest.spyOn(logger, 'error').mockImplementation(() => {});
jest.spyOn(logger, 'debug').mockImplementation(() => {});
});
beforeEach(async () => {
// Create temporary AWS credentials directory
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aws-cred-test-'));
// Create temporary .aws directory structure
const awsDir = path.join(tempDir, '.aws');
fs.mkdirSync(awsDir, { recursive: true });
// Set paths
credentialsPath = path.join(awsDir, 'credentials');
configPath = path.join(awsDir, 'config');
// Mock home directory to use our temporary directory
os.homedir = jest.fn().mockReturnValue(tempDir);
// Reset credential provider
awsCredentialProvider.clearCache();
// Start with clean environment for each test
process.env = { NODE_ENV: 'test' };
});
afterEach(() => {
// Clean up temporary directory
fs.rmSync(tempDir, { recursive: true, force: true });
// Restore environment variables
process.env = { ...originalEnv };
// Clear any mocks
jest.restoreAllMocks();
});
afterAll(() => {
// Restore original homedir function
os.homedir = originalHomedir;
});
test('should retrieve credentials from AWS profile', async () => {
// Create credentials file
const credentialsContent = `
[test-profile]
aws_access_key_id = AKIATESTKEY123456789
aws_secret_access_key = testsecretkey123456789012345678901234567890
`;
// Create config file
const configContent = `
[profile test-profile]
region = us-west-2
`;
// Write test files
fs.writeFileSync(credentialsPath, credentialsContent);
fs.writeFileSync(configPath, configContent);
// Set environment variable
process.env.AWS_PROFILE = 'test-profile';
// Test credential retrieval
const result = await awsCredentialProvider.getCredentials();
// Verify results
expect(result.credentials.accessKeyId).toBe('AKIATESTKEY123456789');
expect(result.credentials.secretAccessKey).toBe('testsecretkey123456789012345678901234567890');
expect(result.region).toBe('us-west-2');
expect(result.source.type).toBe('profile');
expect(result.source.profileName).toBe('test-profile');
// Verify caching
expect(awsCredentialProvider.hasCachedCredentials()).toBe(true);
// Get cached credentials
const cachedResult = await awsCredentialProvider.getCredentials();
expect(cachedResult.credentials).toEqual(result.credentials);
});
test('should fall back to environment variables when profile not found', async () => {
// Set environment variables
process.env.AWS_ACCESS_KEY_ID = 'AKIAENVKEY123456789';
process.env.AWS_SECRET_ACCESS_KEY = 'envsecretkey123456789012345678901234567890';
process.env.AWS_REGION = 'us-east-1';
// Set non-existent profile
process.env.AWS_PROFILE = 'non-existent-profile';
// Mock secureCredentials to mimic environment-based retrieval
jest.spyOn(secureCredentials, 'get').mockImplementation(key => {
if (key === 'AWS_ACCESS_KEY_ID') return 'AKIAENVKEY123456789';
if (key === 'AWS_SECRET_ACCESS_KEY') return 'envsecretkey123456789012345678901234567890';
if (key === 'AWS_REGION') return 'us-east-1';
return null;
});
// Test credential retrieval with fallback
const result = await awsCredentialProvider.getCredentials();
// Verify results
expect(result.credentials.accessKeyId).toBe('AKIAENVKEY123456789');
expect(result.credentials.secretAccessKey).toBe('envsecretkey123456789012345678901234567890');
expect(result.region).toBe('us-east-1');
expect(result.source.type).toBe('environment');
});
test('should retrieve credentials from secure credentials store', async () => {
// Mock secureCredentials
jest.spyOn(secureCredentials, 'get').mockImplementation(key => {
if (key === 'AWS_ACCESS_KEY_ID') return 'AKIASECUREKEY123456789';
if (key === 'AWS_SECRET_ACCESS_KEY') return 'securesecretkey123456789012345678901234567890';
if (key === 'AWS_REGION') return 'eu-west-1';
return null;
});
// Test credential retrieval
const result = await awsCredentialProvider.getCredentials();
// Verify results
expect(result.credentials.accessKeyId).toBe('AKIASECUREKEY123456789');
expect(result.credentials.secretAccessKey).toBe('securesecretkey123456789012345678901234567890');
expect(result.region).toBe('eu-west-1');
expect(result.source.type).toBe('environment');
});
test('should refresh credentials when explicitly requested', async () => {
// Create credentials file
const credentialsContent = `
[test-profile]
aws_access_key_id = AKIATESTKEY123456789
aws_secret_access_key = testsecretkey123456789012345678901234567890
`;
// Write credentials file
fs.writeFileSync(credentialsPath, credentialsContent);
// Set environment variable
process.env.AWS_PROFILE = 'test-profile';
// Get initial credentials
const initialResult = await awsCredentialProvider.getCredentials();
expect(initialResult.credentials.accessKeyId).toBe('AKIATESTKEY123456789');
// Modify credentials file
const updatedCredentialsContent = `
[test-profile]
aws_access_key_id = AKIANEWKEY987654321
aws_secret_access_key = newsecretkey123456789012345678901234567890
`;
// Write updated credentials
fs.writeFileSync(credentialsPath, updatedCredentialsContent);
// Get cached credentials (should be unchanged)
const cachedResult = await awsCredentialProvider.getCredentials();
expect(cachedResult.credentials.accessKeyId).toBe('AKIATESTKEY123456789');
// Clear cache
awsCredentialProvider.clearCache();
// Get fresh credentials
const refreshedResult = await awsCredentialProvider.getCredentials();
expect(refreshedResult.credentials.accessKeyId).toBe('AKIANEWKEY987654321');
});
test('should handle Docker environment credentials', async () => {
// Mock Docker environment detection
process.env.CONTAINER_ID = 'mock-container-id';
process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI = '/credentials/path';
// Skip actual HTTP request to metadata service
jest.spyOn(awsCredentialProvider, '_getContainerCredentials')
.mockResolvedValue({
AccessKeyId: 'AKIADOCKERKEY123456789',
SecretAccessKey: 'dockersecretkey123456789012345678901234567890',
Token: 'docker-token-123',
Expiration: new Date(Date.now() + 3600000).toISOString()
});
// Test credential retrieval
const result = await awsCredentialProvider.getCredentials();
// Verify results
expect(result.credentials.accessKeyId).toBe('AKIADOCKERKEY123456789');
expect(result.credentials.secretAccessKey).toBe('dockersecretkey123456789012345678901234567890');
expect(result.credentials.sessionToken).toBe('docker-token-123');
expect(result.source.type).toBe('container');
});
test('should integrate with secureCredentials when retrieving AWS profile', async () => {
// Create credentials file
const credentialsContent = `
[secure-profile]
aws_access_key_id = AKIASECPROFILE123456789
aws_secret_access_key = secprofilesecret123456789012345678901234567890
`;
// Write credentials file
fs.writeFileSync(credentialsPath, credentialsContent);
// Mock secureCredentials to return AWS_PROFILE
jest.spyOn(secureCredentials, 'get').mockImplementation(key => {
if (key === 'AWS_PROFILE') return 'secure-profile';
return null;
});
// Don't set AWS_PROFILE in environment - it should come from secureCredentials
// Test credential retrieval
const result = await awsCredentialProvider.getCredentials();
// Verify results
expect(result.credentials.accessKeyId).toBe('AKIASECPROFILE123456789');
expect(result.credentials.secretAccessKey).toBe('secprofilesecret123456789012345678901234567890');
expect(result.source.type).toBe('profile');
expect(result.source.profileName).toBe('secure-profile');
});
});

View File

@@ -0,0 +1,293 @@
/**
* Integration test for Claude Service and container execution
*
* This test verifies the integration between claudeService, Docker container execution,
* and environment configuration.
*/
const { jest: jestGlobal } = require('@jest/globals');
const path = require('path');
const childProcess = require('child_process');
const claudeService = require('../../../src/services/claudeService');
const secureCredentials = require('../../../src/utils/secureCredentials');
const logger = require('../../../src/utils/logger');
// Mock child_process execFile
jest.mock('child_process', () => ({
...jest.requireActual('child_process'),
execFile: jest.fn(),
execFileSync: jest.fn()
}));
describe('Claude Service Container Execution Integration', () => {
let originalEnv;
beforeAll(() => {
// Save original environment
originalEnv = { ...process.env };
// Silence logger during tests
jest.spyOn(logger, 'info').mockImplementation(() => {});
jest.spyOn(logger, 'warn').mockImplementation(() => {});
jest.spyOn(logger, 'error').mockImplementation(() => {});
jest.spyOn(logger, 'debug').mockImplementation(() => {});
});
beforeEach(() => {
// Reset mocks
jest.clearAllMocks();
// Mock Docker inspect to find the image
childProcess.execFileSync.mockImplementation((cmd, args) => {
if (cmd === 'docker' && args[0] === 'inspect') {
return JSON.stringify([{ Id: 'mock-container-id' }]);
}
return '';
});
// Mock Docker execFile to return a successful result
childProcess.execFile.mockImplementation((cmd, args, options, callback) => {
callback(null, {
stdout: 'Claude container execution result',
stderr: ''
});
});
// Set production environment
process.env = {
NODE_ENV: 'production',
BOT_USERNAME: '@TestBot',
BOT_EMAIL: 'testbot@example.com',
ENABLE_CONTAINER_FIREWALL: 'false',
CLAUDE_CONTAINER_IMAGE: 'claude-code-runner:latest',
ALLOWED_TOOLS: 'Read,GitHub,Bash,Edit,Write'
};
// Mock secureCredentials
jest.spyOn(secureCredentials, 'get').mockImplementation(key => {
if (key === 'GITHUB_TOKEN') return 'github-test-token';
if (key === 'ANTHROPIC_API_KEY') return 'claude-test-key';
return null;
});
});
afterEach(() => {
// Restore environment variables
process.env = { ...originalEnv };
});
test('should build Docker command correctly for standard execution', async () => {
// Execute Claude command
const result = await claudeService.processCommand({
repoFullName: 'test/repo',
issueNumber: 123,
command: 'Test command',
isPullRequest: false,
branchName: null
});
// Verify result
expect(result).toBe('Claude container execution result');
// Verify Docker execution
expect(childProcess.execFile).toHaveBeenCalledTimes(1);
// Extract args from call
const callArgs = childProcess.execFile.mock.calls[0];
const [cmd, args] = callArgs;
// Verify basic Docker command
expect(cmd).toBe('docker');
expect(args[0]).toBe('run');
expect(args).toContain('--rm'); // Container is removed after execution
// Verify environment variables
expect(args).toContain('-e');
expect(args).toContain('GITHUB_TOKEN=github-test-token');
expect(args).toContain('ANTHROPIC_API_KEY=claude-test-key');
expect(args).toContain('REPO_FULL_NAME=test/repo');
expect(args).toContain('ISSUE_NUMBER=123');
expect(args).toContain('IS_PULL_REQUEST=false');
// Verify command is passed correctly
expect(args).toContain('Test command');
// Verify entrypoint
const entrypointIndex = args.indexOf('--entrypoint');
expect(entrypointIndex).not.toBe(-1);
expect(args[entrypointIndex + 1]).toContain('claudecode-entrypoint.sh');
// Verify allowed tools
expect(args).toContain('--allowedTools');
expect(args).toContain('Read,GitHub,Bash,Edit,Write');
});
test('should build Docker command correctly for PR review', async () => {
// Execute Claude command for PR
const result = await claudeService.processCommand({
repoFullName: 'test/repo',
issueNumber: 456,
command: 'Review PR',
isPullRequest: true,
branchName: 'feature-branch'
});
// Verify result
expect(result).toBe('Claude container execution result');
// Verify Docker execution
expect(childProcess.execFile).toHaveBeenCalledTimes(1);
// Extract args from call
const callArgs = childProcess.execFile.mock.calls[0];
const [cmd, args] = callArgs;
// Verify PR-specific variables
expect(args).toContain('-e');
expect(args).toContain('IS_PULL_REQUEST=true');
expect(args).toContain('BRANCH_NAME=feature-branch');
});
test('should build Docker command correctly for auto-tagging', async () => {
// Execute Claude command for auto-tagging
const result = await claudeService.processCommand({
repoFullName: 'test/repo',
issueNumber: 789,
command: 'Auto-tag this issue',
isPullRequest: false,
branchName: null,
operationType: 'auto-tagging'
});
// Verify result
expect(result).toBe('Claude container execution result');
// Verify Docker execution
expect(childProcess.execFile).toHaveBeenCalledTimes(1);
// Extract args from call
const callArgs = childProcess.execFile.mock.calls[0];
const [cmd, args] = callArgs;
// Verify auto-tagging specific settings
expect(args).toContain('-e');
expect(args).toContain('OPERATION_TYPE=auto-tagging');
// Verify entrypoint is specific to tagging
const entrypointIndex = args.indexOf('--entrypoint');
expect(entrypointIndex).not.toBe(-1);
expect(args[entrypointIndex + 1]).toContain('claudecode-tagging-entrypoint.sh');
// Auto-tagging only allows Read and GitHub tools
expect(args).toContain('--allowedTools');
expect(args).toContain('Read,GitHub');
});
test('should handle Docker container errors', async () => {
// Mock Docker execution to fail
childProcess.execFile.mockImplementation((cmd, args, options, callback) => {
callback(new Error('Docker execution failed'), {
stdout: '',
stderr: 'Container error: command failed'
});
});
// Expect promise rejection
await expect(claudeService.processCommand({
repoFullName: 'test/repo',
issueNumber: 123,
command: 'Test command',
isPullRequest: false,
branchName: null
})).rejects.toThrow('Docker execution failed');
});
test('should handle missing Docker image and try to build it', async () => {
// Mock Docker inspect to not find the image first time, then find it
let inspectCallCount = 0;
childProcess.execFileSync.mockImplementation((cmd, args) => {
if (cmd === 'docker' && args[0] === 'inspect') {
inspectCallCount++;
if (inspectCallCount === 1) {
// First call - image not found
throw new Error('No such image');
} else {
// Second call - image found after build
return JSON.stringify([{ Id: 'mock-container-id' }]);
}
}
// Return success for other commands (like build)
return 'Success';
});
// Execute Claude command
const result = await claudeService.processCommand({
repoFullName: 'test/repo',
issueNumber: 123,
command: 'Test command',
isPullRequest: false,
branchName: null
});
// Verify result
expect(result).toBe('Claude container execution result');
// Verify Docker build was attempted
expect(childProcess.execFileSync).toHaveBeenCalledWith(
'docker',
expect.arrayContaining(['build']),
expect.anything()
);
});
test('should use test mode in non-production environments', async () => {
// Set test environment
process.env.NODE_ENV = 'test';
// Mock test mode response
jest.spyOn(claudeService, '_getTestModeResponse').mockReturnValue('Test mode response');
// Execute Claude command
const result = await claudeService.processCommand({
repoFullName: 'test/repo',
issueNumber: 123,
command: 'Test command',
isPullRequest: false,
branchName: null
});
// Verify test mode response
expect(result).toBe('Test mode response');
// Verify Docker was not called
expect(childProcess.execFile).not.toHaveBeenCalled();
});
test('should sanitize command input before passing to container', async () => {
// Test with command containing shell-unsafe characters
const unsafeCommand = 'Test command with $(dangerous) `characters` && injection;';
// Execute Claude command
await claudeService.processCommand({
repoFullName: 'test/repo',
issueNumber: 123,
command: unsafeCommand,
isPullRequest: false,
branchName: null
});
// Extract args from call
const callArgs = childProcess.execFile.mock.calls[0];
const [cmd, args] = callArgs;
// Verify command was properly sanitized
const commandIndex = args.indexOf(unsafeCommand);
expect(commandIndex).toBe(-1); // Raw command should not be there
// The command should be sanitized and passed as the last argument
const lastArg = args[args.length - 1];
expect(lastArg).not.toContain('$(dangerous)');
expect(lastArg).not.toContain('`characters`');
});
});

View File

@@ -0,0 +1,393 @@
/**
* Integration test for GitHub webhook processing flow
*
* This test verifies the integration between githubController, claudeService,
* and githubService when processing GitHub webhook events.
*/
const { jest: jestGlobal } = require('@jest/globals');
const crypto = require('crypto');
const express = require('express');
const bodyParser = require('body-parser');
const request = require('supertest');
// Services
const claudeService = require('../../../src/services/claudeService');
const githubService = require('../../../src/services/githubService');
const secureCredentials = require('../../../src/utils/secureCredentials');
// Controller
const githubController = require('../../../src/controllers/githubController');
// Mock dependencies
jest.mock('../../../src/services/claudeService');
jest.mock('../../../src/services/githubService');
describe('GitHub Webhook Processing Integration', () => {
let app;
let originalEnv;
beforeAll(() => {
// Save original environment
originalEnv = { ...process.env };
// Create express app for testing
app = express();
app.use(bodyParser.json({
verify: (req, res, buf) => {
req.rawBody = buf;
}
}));
// Add webhook route
app.post('/api/webhooks/github', githubController.handleWebhook);
});
beforeEach(() => {
// Reset mocks
jest.clearAllMocks();
// Set test environment
process.env.NODE_ENV = 'test';
process.env.BOT_USERNAME = '@TestBot';
process.env.AUTHORIZED_USERS = 'testuser,admin';
process.env.GITHUB_WEBHOOK_SECRET = 'test-webhook-secret';
// Mock secureCredentials
jest.spyOn(secureCredentials, 'get').mockImplementation(key => {
if (key === 'GITHUB_WEBHOOK_SECRET') return 'test-webhook-secret';
if (key === 'GITHUB_TOKEN') return 'github-test-token';
if (key === 'ANTHROPIC_API_KEY') return 'claude-test-key';
return null;
});
// Mock claudeService
claudeService.processCommand.mockResolvedValue('Claude response for test command');
// Mock githubService
githubService.postComment.mockResolvedValue({
id: 'test-comment-id',
body: 'Claude response',
created_at: new Date().toISOString()
});
});
afterEach(() => {
// Restore environment variables
process.env = { ...originalEnv };
});
test('should process issue comment webhook with bot mention', async () => {
// Create webhook payload for issue comment with bot mention
const payload = {
action: 'created',
issue: {
number: 123,
title: 'Test Issue',
body: 'This is a test issue',
user: { login: 'testuser' }
},
comment: {
id: 456,
body: '@TestBot help me with this issue',
user: { login: 'testuser' }
},
repository: {
full_name: 'test/repo',
owner: { login: 'test' },
name: 'repo'
},
sender: { login: 'testuser' }
};
// Calculate signature
const payloadString = JSON.stringify(payload);
const signature = 'sha256=' +
crypto.createHmac('sha256', 'test-webhook-secret')
.update(payloadString)
.digest('hex');
// Send request to webhook endpoint
const response = await request(app)
.post('/api/webhooks/github')
.set('X-GitHub-Event', 'issue_comment')
.set('X-GitHub-Delivery', 'test-delivery-id')
.set('X-Hub-Signature-256', signature)
.send(payload);
// Verify response
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
// Verify service calls
expect(claudeService.processCommand).toHaveBeenCalledWith({
repoFullName: 'test/repo',
issueNumber: 123,
command: 'help me with this issue',
isPullRequest: false,
branchName: null
});
expect(githubService.postComment).toHaveBeenCalledWith({
repoOwner: 'test',
repoName: 'repo',
issueNumber: 123,
body: 'Claude response for test command'
});
});
test('should process pull request comment webhook', async () => {
// Create webhook payload for PR comment with bot mention
const payload = {
action: 'created',
issue: {
number: 456,
title: 'Test PR',
body: 'This is a test PR',
user: { login: 'testuser' },
pull_request: { url: 'https://api.github.com/repos/test/repo/pulls/456' }
},
comment: {
id: 789,
body: '@TestBot review this PR',
user: { login: 'testuser' }
},
repository: {
full_name: 'test/repo',
owner: { login: 'test' },
name: 'repo'
},
sender: { login: 'testuser' }
};
// Calculate signature
const payloadString = JSON.stringify(payload);
const signature = 'sha256=' +
crypto.createHmac('sha256', 'test-webhook-secret')
.update(payloadString)
.digest('hex');
// Mock PR-specific GitHub service calls
githubService.getPullRequestDetails.mockResolvedValue({
number: 456,
head: { ref: 'feature-branch' }
});
// Send request to webhook endpoint
const response = await request(app)
.post('/api/webhooks/github')
.set('X-GitHub-Event', 'issue_comment')
.set('X-GitHub-Delivery', 'test-delivery-id')
.set('X-Hub-Signature-256', signature)
.send(payload);
// Verify response
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
// Verify PR details were retrieved
expect(githubService.getPullRequestDetails).toHaveBeenCalledWith({
repoOwner: 'test',
repoName: 'repo',
prNumber: 456
});
// Verify service calls with PR information
expect(claudeService.processCommand).toHaveBeenCalledWith({
repoFullName: 'test/repo',
issueNumber: 456,
command: 'review this PR',
isPullRequest: true,
branchName: 'feature-branch'
});
expect(githubService.postComment).toHaveBeenCalledWith({
repoOwner: 'test',
repoName: 'repo',
issueNumber: 456,
body: 'Claude response for test command'
});
});
test('should reject webhook with invalid signature', async () => {
// Create webhook payload
const payload = {
action: 'created',
issue: { number: 123 },
comment: {
body: '@TestBot help me',
user: { login: 'testuser' }
},
repository: {
full_name: 'test/repo',
owner: { login: 'test' },
name: 'repo'
},
sender: { login: 'testuser' }
};
// Use invalid signature
const invalidSignature = 'sha256=invalid_signature_value';
// Send request with invalid signature
const response = await request(app)
.post('/api/webhooks/github')
.set('X-GitHub-Event', 'issue_comment')
.set('X-GitHub-Delivery', 'test-delivery-id')
.set('X-Hub-Signature-256', invalidSignature)
.send(payload);
// Verify rejection
expect(response.status).toBe(401);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid webhook signature');
// Verify services were not called
expect(claudeService.processCommand).not.toHaveBeenCalled();
expect(githubService.postComment).not.toHaveBeenCalled();
});
test('should ignore comments without bot mention', async () => {
// Create webhook payload without bot mention
const payload = {
action: 'created',
issue: { number: 123 },
comment: {
body: 'This is a regular comment without bot mention',
user: { login: 'testuser' }
},
repository: {
full_name: 'test/repo',
owner: { login: 'test' },
name: 'repo'
},
sender: { login: 'testuser' }
};
// Calculate signature
const payloadString = JSON.stringify(payload);
const signature = 'sha256=' +
crypto.createHmac('sha256', 'test-webhook-secret')
.update(payloadString)
.digest('hex');
// Send request to webhook endpoint
const response = await request(app)
.post('/api/webhooks/github')
.set('X-GitHub-Event', 'issue_comment')
.set('X-GitHub-Delivery', 'test-delivery-id')
.set('X-Hub-Signature-256', signature)
.send(payload);
// Verify response
expect(response.status).toBe(200);
// Verify services were not called
expect(claudeService.processCommand).not.toHaveBeenCalled();
expect(githubService.postComment).not.toHaveBeenCalled();
});
test('should handle auto-tagging on new issue', async () => {
// Create issue opened payload
const payload = {
action: 'opened',
issue: {
number: 789,
title: 'Bug in API endpoint',
body: 'The /api/data endpoint returns a 500 error',
user: { login: 'testuser' }
},
repository: {
full_name: 'test/repo',
owner: { login: 'test' },
name: 'repo'
},
sender: { login: 'testuser' }
};
// Calculate signature
const payloadString = JSON.stringify(payload);
const signature = 'sha256=' +
crypto.createHmac('sha256', 'test-webhook-secret')
.update(payloadString)
.digest('hex');
// Mock Claude service for auto-tagging
claudeService.processCommand.mockResolvedValue('Added labels: bug, api, high-priority');
// Mock GitHub service
githubService.getFallbackLabels.mockReturnValue(['type:bug', 'priority:high', 'component:api']);
githubService.addLabelsToIssue.mockResolvedValue([
{ name: 'type:bug' },
{ name: 'priority:high' },
{ name: 'component:api' }
]);
// Send request to webhook endpoint
const response = await request(app)
.post('/api/webhooks/github')
.set('X-GitHub-Event', 'issues')
.set('X-GitHub-Delivery', 'test-delivery-id')
.set('X-Hub-Signature-256', signature)
.send(payload);
// Verify response
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
// Verify Claude auto-tagging was called
expect(claudeService.processCommand).toHaveBeenCalledWith(expect.objectContaining({
repoFullName: 'test/repo',
issueNumber: 789,
operationType: 'auto-tagging'
}));
});
test('should handle Claude service errors gracefully', async () => {
// Create webhook payload
const payload = {
action: 'created',
issue: { number: 123 },
comment: {
body: '@TestBot help me with this issue',
user: { login: 'testuser' }
},
repository: {
full_name: 'test/repo',
owner: { login: 'test' },
name: 'repo'
},
sender: { login: 'testuser' }
};
// Calculate signature
const payloadString = JSON.stringify(payload);
const signature = 'sha256=' +
crypto.createHmac('sha256', 'test-webhook-secret')
.update(payloadString)
.digest('hex');
// Mock Claude service error
claudeService.processCommand.mockRejectedValue(new Error('Claude service error'));
// Send request to webhook endpoint
const response = await request(app)
.post('/api/webhooks/github')
.set('X-GitHub-Event', 'issue_comment')
.set('X-GitHub-Delivery', 'test-delivery-id')
.set('X-Hub-Signature-256', signature)
.send(payload);
// Verify response
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
// Verify error was posted as comment
expect(githubService.postComment).toHaveBeenCalledWith(expect.objectContaining({
repoOwner: 'test',
repoName: 'repo',
issueNumber: 123,
body: expect.stringContaining('Error processing command')
}));
});
});