forked from claude-did-this/claude-hub
- Remove complex file-based secrets system and secureCredentials.ts - Switch to standard .env file approach for all credentials - Update all code to use process.env directly instead of secureCredentials - Remove docker-compose.secrets.yml and setup-secure-credentials.sh - Update tests to use environment variables directly - Simplify Docker Compose configuration to use env vars This change reduces complexity while maintaining the same security level, as both approaches store plaintext credentials on the host. The .env approach is simpler, more standard, and easier to manage. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
382 lines
12 KiB
JavaScript
382 lines
12 KiB
JavaScript
// Mock dependencies
|
|
jest.mock('../../../src/services/claudeService');
|
|
jest.mock('../../../src/providers/ProviderFactory');
|
|
jest.mock('../../../src/utils/logger', () => ({
|
|
createLogger: () => ({
|
|
info: jest.fn(),
|
|
warn: jest.fn(),
|
|
error: jest.fn(),
|
|
debug: jest.fn()
|
|
})
|
|
}));
|
|
|
|
|
|
// Set required environment variables for claudeService
|
|
process.env.BOT_USERNAME = 'testbot';
|
|
process.env.DEFAULT_AUTHORIZED_USER = 'testuser';
|
|
|
|
const chatbotController = require('../../../src/controllers/chatbotController');
|
|
const claudeService = require('../../../src/services/claudeService');
|
|
const providerFactory = require('../../../src/providers/ProviderFactory');
|
|
jest.mock('../../../src/utils/sanitize', () => ({
|
|
sanitizeBotMentions: jest.fn(msg => msg)
|
|
}));
|
|
|
|
describe('chatbotController', () => {
|
|
let req, res, mockProvider;
|
|
|
|
beforeEach(() => {
|
|
req = {
|
|
method: 'POST',
|
|
path: '/api/webhooks/chatbot/discord',
|
|
headers: {
|
|
'user-agent': 'Discord-Webhooks/1.0',
|
|
'content-type': 'application/json'
|
|
},
|
|
body: {}
|
|
};
|
|
|
|
res = {
|
|
status: jest.fn().mockReturnThis(),
|
|
json: jest.fn().mockReturnThis()
|
|
};
|
|
|
|
mockProvider = {
|
|
verifyWebhookSignature: jest.fn().mockReturnValue(true),
|
|
parseWebhookPayload: jest.fn(),
|
|
extractBotCommand: jest.fn(),
|
|
sendResponse: jest.fn().mockResolvedValue(),
|
|
getUserId: jest.fn(),
|
|
isUserAuthorized: jest.fn().mockReturnValue(true),
|
|
formatErrorMessage: jest
|
|
.fn()
|
|
.mockReturnValue(
|
|
'🚫 **Error Processing Command**\n\n**Reference ID:** `test-error-id`\n**Time:** 2023-01-01T00:00:00.000Z\n\nPlease contact an administrator with the reference ID above.'
|
|
),
|
|
getProviderName: jest.fn().mockReturnValue('DiscordProvider'),
|
|
getBotMention: jest.fn().mockReturnValue('@claude')
|
|
};
|
|
|
|
providerFactory.getProvider.mockReturnValue(mockProvider);
|
|
providerFactory.createFromEnvironment.mockResolvedValue(mockProvider);
|
|
providerFactory.getStats.mockReturnValue({
|
|
totalRegistered: 1,
|
|
totalInitialized: 1,
|
|
availableProviders: ['discord'],
|
|
initializedProviders: ['discord']
|
|
});
|
|
providerFactory.getAllProviders.mockReturnValue(new Map([['discord', mockProvider]]));
|
|
|
|
claudeService.processCommand.mockResolvedValue('Claude response');
|
|
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('handleChatbotWebhook', () => {
|
|
it('should handle successful webhook with valid signature', async () => {
|
|
mockProvider.parseWebhookPayload.mockReturnValue({
|
|
type: 'command',
|
|
content: 'help me',
|
|
userId: 'user123',
|
|
username: 'testuser',
|
|
channelId: 'channel123',
|
|
repo: 'owner/test-repo',
|
|
branch: 'main'
|
|
});
|
|
mockProvider.extractBotCommand.mockReturnValue({
|
|
command: 'help me',
|
|
originalMessage: 'help me'
|
|
});
|
|
mockProvider.getUserId.mockReturnValue('user123');
|
|
|
|
await chatbotController.handleChatbotWebhook(req, res, 'discord');
|
|
|
|
expect(mockProvider.verifyWebhookSignature).toHaveBeenCalledWith(req);
|
|
expect(mockProvider.parseWebhookPayload).toHaveBeenCalledWith(req.body);
|
|
expect(claudeService.processCommand).toHaveBeenCalledWith({
|
|
repoFullName: 'owner/test-repo',
|
|
issueNumber: null,
|
|
command: 'help me',
|
|
isPullRequest: false,
|
|
branchName: 'main',
|
|
chatbotContext: {
|
|
provider: 'discord',
|
|
userId: 'user123',
|
|
username: 'testuser',
|
|
channelId: 'channel123',
|
|
guildId: undefined,
|
|
repo: 'owner/test-repo',
|
|
branch: 'main'
|
|
}
|
|
});
|
|
expect(mockProvider.sendResponse).toHaveBeenCalled();
|
|
expect(res.status).toHaveBeenCalledWith(200);
|
|
expect(res.json).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
success: true,
|
|
message: 'Command processed successfully'
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should return 401 for invalid webhook signature', async () => {
|
|
mockProvider.verifyWebhookSignature.mockReturnValue(false);
|
|
|
|
await chatbotController.handleChatbotWebhook(req, res, 'discord');
|
|
|
|
expect(res.status).toHaveBeenCalledWith(401);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Invalid webhook signature'
|
|
});
|
|
expect(claudeService.processCommand).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle signature verification errors', async () => {
|
|
mockProvider.verifyWebhookSignature.mockImplementation(() => {
|
|
throw new Error('Signature verification failed');
|
|
});
|
|
|
|
await chatbotController.handleChatbotWebhook(req, res, 'discord');
|
|
|
|
expect(res.status).toHaveBeenCalledWith(401);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Signature verification failed',
|
|
message: 'Signature verification failed'
|
|
});
|
|
});
|
|
|
|
it('should handle immediate responses like Discord PING', async () => {
|
|
mockProvider.parseWebhookPayload.mockReturnValue({
|
|
type: 'ping',
|
|
shouldRespond: true,
|
|
responseData: { type: 1 }
|
|
});
|
|
|
|
await chatbotController.handleChatbotWebhook(req, res, 'discord');
|
|
|
|
expect(res.json).toHaveBeenCalledWith({ type: 1 });
|
|
expect(claudeService.processCommand).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should skip processing for unknown message types', async () => {
|
|
mockProvider.parseWebhookPayload.mockReturnValue({
|
|
type: 'unknown',
|
|
shouldRespond: false
|
|
});
|
|
|
|
await chatbotController.handleChatbotWebhook(req, res, 'discord');
|
|
|
|
expect(res.status).toHaveBeenCalledWith(200);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
message: 'Webhook received but no command detected'
|
|
});
|
|
expect(claudeService.processCommand).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should skip processing when no bot command is found', async () => {
|
|
mockProvider.parseWebhookPayload.mockReturnValue({
|
|
type: 'command',
|
|
content: 'hello world',
|
|
userId: 'user123'
|
|
});
|
|
mockProvider.extractBotCommand.mockReturnValue(null);
|
|
|
|
await chatbotController.handleChatbotWebhook(req, res, 'discord');
|
|
|
|
expect(res.status).toHaveBeenCalledWith(200);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
message: 'Webhook received but no bot mention found'
|
|
});
|
|
expect(claudeService.processCommand).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle unauthorized users', async () => {
|
|
mockProvider.parseWebhookPayload.mockReturnValue({
|
|
type: 'command',
|
|
content: 'help me',
|
|
userId: 'unauthorized_user',
|
|
username: 'baduser'
|
|
});
|
|
mockProvider.extractBotCommand.mockReturnValue({
|
|
command: 'help me'
|
|
});
|
|
mockProvider.getUserId.mockReturnValue('unauthorized_user');
|
|
mockProvider.isUserAuthorized.mockReturnValue(false);
|
|
|
|
await chatbotController.handleChatbotWebhook(req, res, 'discord');
|
|
|
|
expect(mockProvider.sendResponse).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
'❌ Sorry, only authorized users can trigger Claude commands.'
|
|
);
|
|
expect(res.status).toHaveBeenCalledWith(200);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
message: 'Unauthorized user - command ignored',
|
|
context: {
|
|
provider: 'discord',
|
|
userId: 'unauthorized_user'
|
|
}
|
|
});
|
|
expect(claudeService.processCommand).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle missing repository parameter', async () => {
|
|
mockProvider.parseWebhookPayload.mockReturnValue({
|
|
type: 'command',
|
|
content: 'help me',
|
|
userId: 'user123',
|
|
username: 'testuser',
|
|
repo: null, // No repo provided
|
|
branch: null
|
|
});
|
|
mockProvider.extractBotCommand.mockReturnValue({
|
|
command: 'help me'
|
|
});
|
|
mockProvider.getUserId.mockReturnValue('user123');
|
|
|
|
await chatbotController.handleChatbotWebhook(req, res, 'discord');
|
|
|
|
expect(mockProvider.sendResponse).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.stringContaining('Repository Required')
|
|
);
|
|
expect(res.status).toHaveBeenCalledWith(400);
|
|
expect(res.json).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
success: false,
|
|
error: 'Repository parameter is required'
|
|
})
|
|
);
|
|
expect(claudeService.processCommand).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle Claude service errors gracefully', async () => {
|
|
mockProvider.parseWebhookPayload.mockReturnValue({
|
|
type: 'command',
|
|
content: 'help me',
|
|
userId: 'user123',
|
|
username: 'testuser',
|
|
repo: 'owner/test-repo',
|
|
branch: 'main'
|
|
});
|
|
mockProvider.extractBotCommand.mockReturnValue({
|
|
command: 'help me'
|
|
});
|
|
mockProvider.getUserId.mockReturnValue('user123');
|
|
|
|
claudeService.processCommand.mockRejectedValue(new Error('Claude service error'));
|
|
|
|
await chatbotController.handleChatbotWebhook(req, res, 'discord');
|
|
|
|
expect(mockProvider.sendResponse).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.stringContaining('🚫 **Error Processing Command**')
|
|
);
|
|
expect(res.status).toHaveBeenCalledWith(500);
|
|
expect(res.json).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
success: false,
|
|
error: 'Failed to process command'
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should handle provider initialization failure', async () => {
|
|
providerFactory.getProvider.mockReturnValue(null);
|
|
providerFactory.createFromEnvironment.mockRejectedValue(new Error('Provider init failed'));
|
|
|
|
await chatbotController.handleChatbotWebhook(req, res, 'discord');
|
|
|
|
expect(res.status).toHaveBeenCalledWith(500);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Provider initialization failed',
|
|
message: 'Provider init failed'
|
|
});
|
|
});
|
|
|
|
it('should handle payload parsing errors', async () => {
|
|
mockProvider.parseWebhookPayload.mockImplementation(() => {
|
|
throw new Error('Invalid payload');
|
|
});
|
|
|
|
await chatbotController.handleChatbotWebhook(req, res, 'discord');
|
|
|
|
expect(res.status).toHaveBeenCalledWith(400);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Invalid payload format',
|
|
message: 'Invalid payload'
|
|
});
|
|
});
|
|
|
|
it('should handle unexpected errors', async () => {
|
|
providerFactory.getProvider.mockImplementation(() => {
|
|
throw new Error('Unexpected error');
|
|
});
|
|
|
|
await chatbotController.handleChatbotWebhook(req, res, 'discord');
|
|
|
|
expect(res.status).toHaveBeenCalledWith(500);
|
|
expect(res.json).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
error: 'Provider initialization failed',
|
|
message: 'Unexpected error'
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('handleDiscordWebhook', () => {
|
|
it('should call handleChatbotWebhook with discord provider', async () => {
|
|
// Mock a simple provider response to avoid validation
|
|
mockProvider.parseWebhookPayload.mockReturnValue({
|
|
type: 'ping',
|
|
shouldRespond: true,
|
|
responseData: { type: 1 }
|
|
});
|
|
|
|
await chatbotController.handleDiscordWebhook(req, res);
|
|
|
|
expect(res.json).toHaveBeenCalledWith({ type: 1 });
|
|
expect(res.status).not.toHaveBeenCalledWith(400); // Should not trigger repo validation
|
|
});
|
|
});
|
|
|
|
describe('getProviderStats', () => {
|
|
it('should return provider statistics successfully', async () => {
|
|
await chatbotController.getProviderStats(req, res);
|
|
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
stats: {
|
|
totalRegistered: 1,
|
|
totalInitialized: 1,
|
|
availableProviders: ['discord'],
|
|
initializedProviders: ['discord']
|
|
},
|
|
providers: {
|
|
discord: {
|
|
name: 'DiscordProvider',
|
|
initialized: true,
|
|
botMention: '@claude'
|
|
}
|
|
},
|
|
timestamp: expect.any(String)
|
|
});
|
|
});
|
|
|
|
it('should handle errors when getting stats', async () => {
|
|
providerFactory.getStats.mockImplementation(() => {
|
|
throw new Error('Stats error');
|
|
});
|
|
|
|
await chatbotController.getProviderStats(req, res);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(500);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Failed to get provider statistics',
|
|
message: 'Stats error'
|
|
});
|
|
});
|
|
});
|
|
});
|