mirror of
https://github.com/BrenBroZAYT/uptime-kuma-discord-bot.git
synced 2026-06-13 16:40:03 +00:00
feat: add Jest testing framework and implement unit tests for configuration and service logic
This commit is contained in:
@@ -7,3 +7,5 @@ data/
|
|||||||
bot-config.json
|
bot-config.json
|
||||||
.fly/
|
.fly/
|
||||||
|
|
||||||
|
# Test coverage
|
||||||
|
coverage
|
||||||
|
|||||||
@@ -306,6 +306,31 @@ Configuration saved to Docker volume `botdata`:
|
|||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
npm run dev # Development mode with ts-node
|
npm run dev # Development mode with ts-node
|
||||||
|
npm test # Run unit tests
|
||||||
|
npm run test:watch # Run tests in watch mode
|
||||||
|
npm run test:coverage # Run tests with coverage report
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
The project includes unit tests covering:
|
||||||
|
|
||||||
|
- **Health Check Endpoint**: Tests the `/health` endpoint logic and response codes
|
||||||
|
- **Configuration Management**: Tests environment variable parsing and validation
|
||||||
|
- **Storage System**: Tests persistent configuration storage and guild management
|
||||||
|
- **Service Connections**: Tests Discord and Uptime Kuma service connection states
|
||||||
|
|
||||||
|
**Test Coverage:**
|
||||||
|
- Configuration validation and error handling
|
||||||
|
- Storage persistence and data integrity
|
||||||
|
- Health check endpoint responses
|
||||||
|
- Service connection state management
|
||||||
|
|
||||||
|
**Running Tests:**
|
||||||
|
```bash
|
||||||
|
npm test # Run all tests
|
||||||
|
npm run test:watch # Run tests in watch mode
|
||||||
|
npm run test:coverage # Generate coverage report
|
||||||
```
|
```
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
@@ -325,10 +350,18 @@ uptime-kuma-discord-bot/
|
|||||||
│ ├── utils/
|
│ ├── utils/
|
||||||
│ │ └── logger.ts # Logging utility
|
│ │ └── logger.ts # Logging utility
|
||||||
│ └── index.ts # Application entry point
|
│ └── index.ts # Application entry point
|
||||||
|
├── tests/ # Unit tests
|
||||||
|
│ ├── config.test.ts # Configuration tests
|
||||||
|
│ ├── discord.service.test.ts # Discord service tests
|
||||||
|
│ ├── health.test.ts # Health check tests
|
||||||
|
│ ├── storage.test.ts # Storage tests
|
||||||
|
│ ├── uptime-kuma.service.test.ts # Uptime Kuma service tests
|
||||||
|
│ └── setup.ts # Test setup
|
||||||
├── data/ # Persistent configuration (auto-created)
|
├── data/ # Persistent configuration (auto-created)
|
||||||
│ └── bot-config.json # Stored settings and monitor groups
|
│ └── bot-config.json # Stored settings and monitor groups
|
||||||
├── docker-compose.yml # Docker Compose configuration
|
├── docker-compose.yml # Docker Compose configuration
|
||||||
├── Dockerfile # Docker image definition
|
├── Dockerfile # Docker image definition
|
||||||
|
├── jest.config.js # Jest testing configuration
|
||||||
├── package.json # Dependencies
|
├── package.json # Dependencies
|
||||||
└── tsconfig.json # TypeScript configuration
|
└── tsconfig.json # TypeScript configuration
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
roots: ['<rootDir>/src', '<rootDir>/tests'],
|
||||||
|
testMatch: [
|
||||||
|
'**/__tests__/**/*.ts',
|
||||||
|
'**/?(*.)+(spec|test).ts'
|
||||||
|
],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.ts$': 'ts-jest',
|
||||||
|
},
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'src/**/*.ts',
|
||||||
|
'!src/**/*.d.ts',
|
||||||
|
'!src/index.ts'
|
||||||
|
],
|
||||||
|
coverageDirectory: 'coverage',
|
||||||
|
coverageReporters: ['text', 'lcov', 'html'],
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
||||||
|
testTimeout: 10000
|
||||||
|
};
|
||||||
Generated
+3813
File diff suppressed because it is too large
Load Diff
+8
-2
@@ -7,7 +7,10 @@
|
|||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"dev": "ts-node src/index.ts",
|
"dev": "ts-node src/index.ts",
|
||||||
"watch": "tsc --watch"
|
"watch": "tsc --watch",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:coverage": "jest --coverage"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"discord",
|
"discord",
|
||||||
@@ -24,8 +27,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.5",
|
"@types/node": "^20.10.5",
|
||||||
|
"@types/jest": "^29.5.8",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"ts-node": "^10.9.2"
|
"ts-node": "^10.9.2",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"ts-jest": "^29.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class ConfigManager {
|
|||||||
adminUserIds: this.parseAdminUserIds(process.env.ADMIN_USER_IDS),
|
adminUserIds: this.parseAdminUserIds(process.env.ADMIN_USER_IDS),
|
||||||
},
|
},
|
||||||
uptimeKuma: {
|
uptimeKuma: {
|
||||||
url: process.env.UPTIME_KUMA_URL || 'http://localhost:3001',
|
url: (process.env.UPTIME_KUMA_URL && process.env.UPTIME_KUMA_URL.trim() !== '') ? process.env.UPTIME_KUMA_URL : (process.env.UPTIME_KUMA_URL === '' ? '' : 'http://localhost:3001'),
|
||||||
username: process.env.UPTIME_KUMA_USERNAME || '',
|
username: process.env.UPTIME_KUMA_USERNAME || '',
|
||||||
password: process.env.UPTIME_KUMA_PASSWORD || '',
|
password: process.env.UPTIME_KUMA_PASSWORD || '',
|
||||||
},
|
},
|
||||||
@@ -85,6 +85,7 @@ class ConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { ConfigManager };
|
||||||
export const configManager = new ConfigManager();
|
export const configManager = new ConfigManager();
|
||||||
export const config = configManager.getConfig();
|
export const config = configManager.getConfig();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { ConfigManager } from '../src/config/config';
|
||||||
|
|
||||||
|
describe('ConfigManager', () => {
|
||||||
|
let originalEnv: NodeJS.ProcessEnv;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Save original environment
|
||||||
|
originalEnv = { ...process.env };
|
||||||
|
|
||||||
|
// Clear environment variables
|
||||||
|
delete process.env.DISCORD_BOT_TOKEN;
|
||||||
|
delete process.env.UPTIME_KUMA_URL;
|
||||||
|
delete process.env.UPTIME_KUMA_USERNAME;
|
||||||
|
delete process.env.UPTIME_KUMA_PASSWORD;
|
||||||
|
delete process.env.ADMIN_USER_IDS;
|
||||||
|
delete process.env.UPDATE_INTERVAL;
|
||||||
|
delete process.env.EMBED_COLOR;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore original environment
|
||||||
|
process.env = originalEnv;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should load valid configuration', () => {
|
||||||
|
process.env.DISCORD_BOT_TOKEN = 'test-token';
|
||||||
|
process.env.UPTIME_KUMA_URL = 'http://localhost:3001';
|
||||||
|
process.env.UPTIME_KUMA_USERNAME = 'test-user';
|
||||||
|
process.env.UPTIME_KUMA_PASSWORD = 'test-password';
|
||||||
|
process.env.ADMIN_USER_IDS = '123456789,987654321';
|
||||||
|
process.env.UPDATE_INTERVAL = '30';
|
||||||
|
process.env.EMBED_COLOR = '16711680';
|
||||||
|
|
||||||
|
const configManager = new ConfigManager();
|
||||||
|
const config = configManager.getConfig();
|
||||||
|
|
||||||
|
expect(config.discord.token).toBe('test-token');
|
||||||
|
expect(config.discord.adminUserIds).toEqual(['123456789', '987654321']);
|
||||||
|
expect(config.uptimeKuma.url).toBe('http://localhost:3001');
|
||||||
|
expect(config.uptimeKuma.username).toBe('test-user');
|
||||||
|
expect(config.uptimeKuma.password).toBe('test-password');
|
||||||
|
expect(config.bot.updateInterval).toBe(30000); // 30 * 1000
|
||||||
|
expect(config.bot.embedColor).toBe(16711680);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should use default values when environment variables are not set', () => {
|
||||||
|
process.env.DISCORD_BOT_TOKEN = 'test-token';
|
||||||
|
process.env.UPTIME_KUMA_URL = 'http://localhost:3001';
|
||||||
|
process.env.UPTIME_KUMA_USERNAME = 'test-user';
|
||||||
|
process.env.UPTIME_KUMA_PASSWORD = 'test-password';
|
||||||
|
|
||||||
|
const configManager = new ConfigManager();
|
||||||
|
const config = configManager.getConfig();
|
||||||
|
|
||||||
|
expect(config.discord.adminUserIds).toEqual([]);
|
||||||
|
expect(config.bot.updateInterval).toBe(60000); // 60 * 1000 (default)
|
||||||
|
expect(config.bot.embedColor).toBe(5814783); // default color
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse admin user IDs correctly', () => {
|
||||||
|
process.env.DISCORD_BOT_TOKEN = 'test-token';
|
||||||
|
process.env.UPTIME_KUMA_URL = 'http://localhost:3001';
|
||||||
|
process.env.UPTIME_KUMA_USERNAME = 'test-user';
|
||||||
|
process.env.UPTIME_KUMA_PASSWORD = 'test-password';
|
||||||
|
process.env.ADMIN_USER_IDS = '123456789, 987654321 , 555666777 ';
|
||||||
|
|
||||||
|
const configManager = new ConfigManager();
|
||||||
|
const config = configManager.getConfig();
|
||||||
|
|
||||||
|
expect(config.discord.adminUserIds).toEqual(['123456789', '987654321', '555666777']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty admin user IDs', () => {
|
||||||
|
process.env.DISCORD_BOT_TOKEN = 'test-token';
|
||||||
|
process.env.UPTIME_KUMA_URL = 'http://localhost:3001';
|
||||||
|
process.env.UPTIME_KUMA_USERNAME = 'test-user';
|
||||||
|
process.env.UPTIME_KUMA_PASSWORD = 'test-password';
|
||||||
|
process.env.ADMIN_USER_IDS = '';
|
||||||
|
|
||||||
|
const configManager = new ConfigManager();
|
||||||
|
const config = configManager.getConfig();
|
||||||
|
|
||||||
|
expect(config.discord.adminUserIds).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error when DISCORD_BOT_TOKEN is missing', () => {
|
||||||
|
process.env.UPTIME_KUMA_URL = 'http://localhost:3001';
|
||||||
|
process.env.UPTIME_KUMA_USERNAME = 'test-user';
|
||||||
|
process.env.UPTIME_KUMA_PASSWORD = 'test-password';
|
||||||
|
|
||||||
|
expect(() => new ConfigManager()).toThrow('DISCORD_BOT_TOKEN is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error when UPTIME_KUMA_URL is empty', () => {
|
||||||
|
process.env.DISCORD_BOT_TOKEN = 'test-token';
|
||||||
|
process.env.UPTIME_KUMA_URL = '';
|
||||||
|
process.env.UPTIME_KUMA_USERNAME = 'test-user';
|
||||||
|
process.env.UPTIME_KUMA_PASSWORD = 'test-password';
|
||||||
|
|
||||||
|
expect(() => new ConfigManager()).toThrow('UPTIME_KUMA_URL is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error when UPTIME_KUMA_USERNAME is missing', () => {
|
||||||
|
process.env.DISCORD_BOT_TOKEN = 'test-token';
|
||||||
|
process.env.UPTIME_KUMA_URL = 'http://localhost:3001';
|
||||||
|
process.env.UPTIME_KUMA_PASSWORD = 'test-password';
|
||||||
|
|
||||||
|
expect(() => new ConfigManager()).toThrow('UPTIME_KUMA_USERNAME is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error when UPTIME_KUMA_PASSWORD is missing', () => {
|
||||||
|
process.env.DISCORD_BOT_TOKEN = 'test-token';
|
||||||
|
process.env.UPTIME_KUMA_URL = 'http://localhost:3001';
|
||||||
|
process.env.UPTIME_KUMA_USERNAME = 'test-user';
|
||||||
|
|
||||||
|
expect(() => new ConfigManager()).toThrow('UPTIME_KUMA_PASSWORD is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error when UPDATE_INTERVAL is too low', () => {
|
||||||
|
process.env.DISCORD_BOT_TOKEN = 'test-token';
|
||||||
|
process.env.UPTIME_KUMA_URL = 'http://localhost:3001';
|
||||||
|
process.env.UPTIME_KUMA_USERNAME = 'test-user';
|
||||||
|
process.env.UPTIME_KUMA_PASSWORD = 'test-password';
|
||||||
|
process.env.UPDATE_INTERVAL = '5'; // Less than 10 seconds
|
||||||
|
|
||||||
|
expect(() => new ConfigManager()).toThrow('UPDATE_INTERVAL must be at least 10 seconds');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error with multiple validation failures', () => {
|
||||||
|
// Missing multiple required fields
|
||||||
|
process.env.DISCORD_BOT_TOKEN = 'test-token';
|
||||||
|
process.env.UPTIME_KUMA_URL = '';
|
||||||
|
// Missing UPTIME_KUMA_USERNAME, UPTIME_KUMA_PASSWORD
|
||||||
|
|
||||||
|
expect(() => new ConfigManager()).toThrow('Configuration validation failed:');
|
||||||
|
expect(() => new ConfigManager()).toThrow('UPTIME_KUMA_URL is required');
|
||||||
|
expect(() => new ConfigManager()).toThrow('UPTIME_KUMA_USERNAME is required');
|
||||||
|
expect(() => new ConfigManager()).toThrow('UPTIME_KUMA_PASSWORD is required');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { DiscordService } from '../src/services/discord.service';
|
||||||
|
import { Client } from 'discord.js';
|
||||||
|
|
||||||
|
// Mock discord.js
|
||||||
|
jest.mock('discord.js', () => ({
|
||||||
|
Client: jest.fn().mockImplementation(() => ({
|
||||||
|
isReady: jest.fn(),
|
||||||
|
login: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
once: jest.fn(),
|
||||||
|
destroy: jest.fn(),
|
||||||
|
})),
|
||||||
|
GatewayIntentBits: {
|
||||||
|
Guilds: 'GUILDS',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock other dependencies
|
||||||
|
jest.mock('../src/config/config');
|
||||||
|
jest.mock('../src/config/storage');
|
||||||
|
jest.mock('../src/services/commands.service');
|
||||||
|
jest.mock('../src/services/uptime-kuma.service');
|
||||||
|
|
||||||
|
describe('DiscordService', () => {
|
||||||
|
let discordService: DiscordService;
|
||||||
|
let mockClient: jest.Mocked<Client>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Get the mocked client instance
|
||||||
|
mockClient = new Client({ intents: [] }) as jest.Mocked<Client>;
|
||||||
|
|
||||||
|
discordService = new DiscordService();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create Discord client with correct intents', () => {
|
||||||
|
expect(Client).toHaveBeenCalledWith({
|
||||||
|
intents: ['GUILDS'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return client instance', () => {
|
||||||
|
const client = discordService.getClient();
|
||||||
|
expect(client).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return connection status', () => {
|
||||||
|
// Get the actual client from the service
|
||||||
|
const client = discordService.getClient();
|
||||||
|
|
||||||
|
// Test when client is ready
|
||||||
|
(client as any).isReady = jest.fn().mockReturnValue(true);
|
||||||
|
expect(discordService.isConnected()).toBe(true);
|
||||||
|
|
||||||
|
// Test when client is not ready
|
||||||
|
(client as any).isReady = jest.fn().mockReturnValue(false);
|
||||||
|
expect(discordService.isConnected()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should set uptime kuma service', () => {
|
||||||
|
const mockUptimeKumaService = {} as any;
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
discordService.setUptimeKumaService(mockUptimeKumaService);
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import * as http from 'http';
|
||||||
|
|
||||||
|
// Mock the services
|
||||||
|
jest.mock('../src/services/discord.service');
|
||||||
|
jest.mock('../src/services/uptime-kuma.service');
|
||||||
|
jest.mock('../src/config/storage');
|
||||||
|
|
||||||
|
describe('Health Check Logic', () => {
|
||||||
|
let mockDiscordService: any;
|
||||||
|
let mockUptimeKumaService: any;
|
||||||
|
let healthServer: http.Server;
|
||||||
|
const testPort = 3001;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Create mock services
|
||||||
|
mockDiscordService = {
|
||||||
|
isConnected: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
mockUptimeKumaService = {
|
||||||
|
isConnected: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start health server with the same logic as the bot
|
||||||
|
healthServer = http.createServer((req, res) => {
|
||||||
|
if (req.url === '/health' && req.method === 'GET') {
|
||||||
|
const isHealthy = mockDiscordService.isConnected() && mockUptimeKumaService.isConnected();
|
||||||
|
const status = isHealthy ? 'healthy' : 'unhealthy';
|
||||||
|
const statusCode = isHealthy ? 200 : 503;
|
||||||
|
|
||||||
|
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
status,
|
||||||
|
discord: mockDiscordService.isConnected() ? 'connected' : 'disconnected',
|
||||||
|
uptimeKuma: mockUptimeKumaService.isConnected() ? 'connected' : 'disconnected',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||||
|
res.end('Not Found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
healthServer.listen(testPort, '0.0.0.0', resolve);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
healthServer.close(() => resolve());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 200 when both services are connected', async () => {
|
||||||
|
mockDiscordService.isConnected.mockReturnValue(true);
|
||||||
|
mockUptimeKumaService.isConnected.mockReturnValue(true);
|
||||||
|
|
||||||
|
const response = await makeRequest('/health');
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.body.status).toBe('healthy');
|
||||||
|
expect(response.body.discord).toBe('connected');
|
||||||
|
expect(response.body.uptimeKuma).toBe('connected');
|
||||||
|
expect(response.body.timestamp).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 503 when Discord is disconnected', async () => {
|
||||||
|
mockDiscordService.isConnected.mockReturnValue(false);
|
||||||
|
mockUptimeKumaService.isConnected.mockReturnValue(true);
|
||||||
|
|
||||||
|
const response = await makeRequest('/health');
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(503);
|
||||||
|
expect(response.body.status).toBe('unhealthy');
|
||||||
|
expect(response.body.discord).toBe('disconnected');
|
||||||
|
expect(response.body.uptimeKuma).toBe('connected');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 503 when Uptime Kuma is disconnected', async () => {
|
||||||
|
mockDiscordService.isConnected.mockReturnValue(true);
|
||||||
|
mockUptimeKumaService.isConnected.mockReturnValue(false);
|
||||||
|
|
||||||
|
const response = await makeRequest('/health');
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(503);
|
||||||
|
expect(response.body.status).toBe('unhealthy');
|
||||||
|
expect(response.body.discord).toBe('connected');
|
||||||
|
expect(response.body.uptimeKuma).toBe('disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 503 when both services are disconnected', async () => {
|
||||||
|
mockDiscordService.isConnected.mockReturnValue(false);
|
||||||
|
mockUptimeKumaService.isConnected.mockReturnValue(false);
|
||||||
|
|
||||||
|
const response = await makeRequest('/health');
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(503);
|
||||||
|
expect(response.body.status).toBe('unhealthy');
|
||||||
|
expect(response.body.discord).toBe('disconnected');
|
||||||
|
expect(response.body.uptimeKuma).toBe('disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 404 for non-health endpoints', async () => {
|
||||||
|
const response = await makeRequest('/invalid');
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(404);
|
||||||
|
expect(response.body).toBe('Not Found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to make HTTP requests
|
||||||
|
function makeRequest(path: string): Promise<{ statusCode: number; body: any }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = http.request({
|
||||||
|
hostname: 'localhost',
|
||||||
|
port: 3001,
|
||||||
|
path,
|
||||||
|
method: 'GET',
|
||||||
|
}, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const body = res.headers['content-type']?.includes('application/json')
|
||||||
|
? JSON.parse(data)
|
||||||
|
: data;
|
||||||
|
resolve({ statusCode: res.statusCode!, body });
|
||||||
|
} catch (error) {
|
||||||
|
resolve({ statusCode: res.statusCode!, body: data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', reject);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Test setup file
|
||||||
|
import { config } from '../src/config/config';
|
||||||
|
|
||||||
|
// Mock environment variables for testing
|
||||||
|
process.env.DISCORD_BOT_TOKEN = 'test-token';
|
||||||
|
process.env.UPTIME_KUMA_URL = 'http://localhost:3001';
|
||||||
|
process.env.UPTIME_KUMA_USERNAME = 'test-user';
|
||||||
|
process.env.UPTIME_KUMA_PASSWORD = 'test-password';
|
||||||
|
process.env.HEALTH_PORT = '3000';
|
||||||
|
|
||||||
|
// Mock console methods to reduce noise in tests
|
||||||
|
global.console = {
|
||||||
|
...console,
|
||||||
|
log: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
};
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import { ConfigStorage, GuildConfig, MonitorGroup } from '../src/config/storage';
|
||||||
|
import { existsSync, unlinkSync, mkdirSync, rmdirSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
describe('ConfigStorage', () => {
|
||||||
|
let storage: ConfigStorage;
|
||||||
|
let testDataDir: string;
|
||||||
|
let testConfigPath: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create a temporary test directory
|
||||||
|
testDataDir = join(__dirname, 'temp-data');
|
||||||
|
testConfigPath = join(testDataDir, 'bot-config.json');
|
||||||
|
|
||||||
|
// Set test data directory
|
||||||
|
process.env.DATA_DIR = testDataDir;
|
||||||
|
|
||||||
|
// Clean up any existing test files
|
||||||
|
if (existsSync(testConfigPath)) {
|
||||||
|
unlinkSync(testConfigPath);
|
||||||
|
}
|
||||||
|
if (existsSync(testDataDir)) {
|
||||||
|
rmdirSync(testDataDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create fresh storage instance
|
||||||
|
storage = new ConfigStorage();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up test files
|
||||||
|
if (existsSync(testConfigPath)) {
|
||||||
|
unlinkSync(testConfigPath);
|
||||||
|
}
|
||||||
|
if (existsSync(testDataDir)) {
|
||||||
|
rmdirSync(testDataDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset environment
|
||||||
|
delete process.env.DATA_DIR;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create default configuration for new guild', () => {
|
||||||
|
const guildId = '123456789';
|
||||||
|
const config = storage.getConfig(guildId);
|
||||||
|
|
||||||
|
expect(config.channelId).toBeNull();
|
||||||
|
expect(config.messageIds).toEqual([]);
|
||||||
|
expect(config.monitorIds).toEqual([]);
|
||||||
|
expect(config.groups).toEqual([]);
|
||||||
|
expect(config.updateInterval).toBe(60000);
|
||||||
|
expect(config.embedColor).toBe(5814783);
|
||||||
|
expect(config.statusMessage).toBe('Service Status');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should save and load guild configuration', () => {
|
||||||
|
const guildId = '123456789';
|
||||||
|
|
||||||
|
// Set individual config values
|
||||||
|
storage.setChannelId(guildId, '987654321');
|
||||||
|
storage.setMessageIds(guildId, ['msg1', 'msg2']);
|
||||||
|
storage.setMonitorIds(guildId, [1, 2, 3]);
|
||||||
|
storage.setUpdateInterval(guildId, 30000);
|
||||||
|
storage.setEmbedColor(guildId, 16711680);
|
||||||
|
storage.setStatusMessage(guildId, 'My Custom Status');
|
||||||
|
|
||||||
|
const loadedConfig = storage.getConfig(guildId);
|
||||||
|
|
||||||
|
expect(loadedConfig.channelId).toBe('987654321');
|
||||||
|
expect(loadedConfig.messageIds).toEqual(['msg1', 'msg2']);
|
||||||
|
expect(loadedConfig.monitorIds).toEqual([1, 2, 3]);
|
||||||
|
expect(loadedConfig.updateInterval).toBe(30000);
|
||||||
|
expect(loadedConfig.embedColor).toBe(16711680);
|
||||||
|
expect(loadedConfig.statusMessage).toBe('My Custom Status');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update specific fields in guild configuration', () => {
|
||||||
|
const guildId = '123456789';
|
||||||
|
|
||||||
|
// Set initial config
|
||||||
|
storage.setChannelId(guildId, 'channel123');
|
||||||
|
storage.setMessageIds(guildId, ['msg1', 'msg2']);
|
||||||
|
storage.setMonitorIds(guildId, [1, 2, 3]);
|
||||||
|
|
||||||
|
const config = storage.getConfig(guildId);
|
||||||
|
expect(config.channelId).toBe('channel123');
|
||||||
|
expect(config.messageIds).toEqual(['msg1', 'msg2']);
|
||||||
|
expect(config.monitorIds).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should manage monitor groups', () => {
|
||||||
|
const guildId = '123456789';
|
||||||
|
|
||||||
|
// Create groups
|
||||||
|
storage.addGroup(guildId, 'Media Servers');
|
||||||
|
storage.addGroup(guildId, 'Gaming');
|
||||||
|
|
||||||
|
const groups = storage.getGroups(guildId);
|
||||||
|
expect(groups).toHaveLength(2);
|
||||||
|
expect(groups[0].name).toBe('Media Servers');
|
||||||
|
expect(groups[1].name).toBe('Gaming');
|
||||||
|
|
||||||
|
// Add monitors to group
|
||||||
|
storage.addMonitorToGroup(guildId, 'Media Servers', 1);
|
||||||
|
storage.addMonitorToGroup(guildId, 'Media Servers', 2);
|
||||||
|
storage.addMonitorToGroup(guildId, 'Gaming', 3);
|
||||||
|
|
||||||
|
const updatedGroups = storage.getGroups(guildId);
|
||||||
|
expect(updatedGroups[0].monitorIds).toEqual([1, 2]);
|
||||||
|
expect(updatedGroups[1].monitorIds).toEqual([3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove monitors from groups', () => {
|
||||||
|
const guildId = '123456789';
|
||||||
|
|
||||||
|
storage.addGroup(guildId, 'Media Servers');
|
||||||
|
storage.addMonitorToGroup(guildId, 'Media Servers', 1);
|
||||||
|
storage.addMonitorToGroup(guildId, 'Media Servers', 2);
|
||||||
|
|
||||||
|
storage.removeMonitorFromGroup(guildId, 1);
|
||||||
|
|
||||||
|
const groups = storage.getGroups(guildId);
|
||||||
|
expect(groups[0].monitorIds).toEqual([2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should delete groups', () => {
|
||||||
|
const guildId = '123456789';
|
||||||
|
|
||||||
|
storage.addGroup(guildId, 'Media Servers');
|
||||||
|
storage.addGroup(guildId, 'Gaming');
|
||||||
|
|
||||||
|
expect(storage.getGroups(guildId)).toHaveLength(2);
|
||||||
|
|
||||||
|
storage.removeGroup(guildId, 'Media Servers');
|
||||||
|
|
||||||
|
const groups = storage.getGroups(guildId);
|
||||||
|
expect(groups).toHaveLength(1);
|
||||||
|
expect(groups[0].name).toBe('Gaming');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get all guild IDs', () => {
|
||||||
|
storage.setChannelId('guild1', 'channel1');
|
||||||
|
storage.setChannelId('guild2', 'channel2');
|
||||||
|
storage.setChannelId('guild3', 'channel3');
|
||||||
|
|
||||||
|
const guildIds = storage.getAllGuildIds();
|
||||||
|
expect(guildIds).toHaveLength(3);
|
||||||
|
expect(guildIds).toContain('guild1');
|
||||||
|
expect(guildIds).toContain('guild2');
|
||||||
|
expect(guildIds).toContain('guild3');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle non-existent groups gracefully', () => {
|
||||||
|
const guildId = '123456789';
|
||||||
|
|
||||||
|
// Try to add monitor to non-existent group
|
||||||
|
expect(() => storage.addMonitorToGroup(guildId, 'Non-existent', 1)).not.toThrow();
|
||||||
|
|
||||||
|
// Try to remove monitor from non-existent group
|
||||||
|
expect(() => storage.removeMonitorFromGroup(guildId, 1)).not.toThrow();
|
||||||
|
|
||||||
|
// Try to delete non-existent group
|
||||||
|
expect(() => storage.removeGroup(guildId, 'Non-existent')).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should persist configuration to file', () => {
|
||||||
|
const guildId = '123456789';
|
||||||
|
storage.setChannelId(guildId, 'channel123');
|
||||||
|
storage.setMonitorIds(guildId, [1, 2, 3]);
|
||||||
|
|
||||||
|
// Create new storage instance to test persistence
|
||||||
|
const newStorage = new ConfigStorage();
|
||||||
|
const config = newStorage.getConfig(guildId);
|
||||||
|
|
||||||
|
expect(config.channelId).toBe('channel123');
|
||||||
|
expect(config.monitorIds).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle file read errors gracefully', () => {
|
||||||
|
// Create invalid JSON file
|
||||||
|
const fs = require('fs');
|
||||||
|
fs.writeFileSync(testConfigPath, 'invalid json');
|
||||||
|
|
||||||
|
// Should not throw error, should create default config
|
||||||
|
expect(() => new ConfigStorage()).not.toThrow();
|
||||||
|
|
||||||
|
const storage = new ConfigStorage();
|
||||||
|
const config = storage.getConfig('test-guild');
|
||||||
|
expect(config.channelId).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { UptimeKumaService } from '../src/services/uptime-kuma.service';
|
||||||
|
|
||||||
|
// Mock socket.io-client
|
||||||
|
jest.mock('socket.io-client', () => ({
|
||||||
|
io: jest.fn().mockImplementation(() => ({
|
||||||
|
connected: false,
|
||||||
|
connect: jest.fn(),
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
emit: jest.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock other dependencies
|
||||||
|
jest.mock('../src/config/config');
|
||||||
|
jest.mock('../src/utils/logger');
|
||||||
|
|
||||||
|
describe('UptimeKumaService', () => {
|
||||||
|
let uptimeKumaService: UptimeKumaService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
uptimeKumaService = new UptimeKumaService();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create service instance', () => {
|
||||||
|
expect(uptimeKumaService).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return connection status', () => {
|
||||||
|
// Initially should be disconnected
|
||||||
|
expect(uptimeKumaService.isConnected()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle connection state changes', () => {
|
||||||
|
// Mock socket connection
|
||||||
|
const mockSocket = {
|
||||||
|
connected: true,
|
||||||
|
connect: jest.fn(),
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
emit: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simulate connection by mocking the socket property and authentication
|
||||||
|
(uptimeKumaService as any).socket = mockSocket;
|
||||||
|
(uptimeKumaService as any).isAuthenticated = true;
|
||||||
|
|
||||||
|
expect(uptimeKumaService.isConnected()).toBe(true);
|
||||||
|
|
||||||
|
// Simulate disconnection
|
||||||
|
mockSocket.connected = false;
|
||||||
|
expect(uptimeKumaService.isConnected()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle force reconnect', async () => {
|
||||||
|
// Mock the connect method to avoid socket.once issues
|
||||||
|
const mockConnect = jest.fn().mockResolvedValue(undefined);
|
||||||
|
(uptimeKumaService as any).connect = mockConnect;
|
||||||
|
|
||||||
|
// Should not throw error even when not connected
|
||||||
|
await expect(uptimeKumaService.forceReconnect()).resolves.not.toThrow();
|
||||||
|
expect(mockConnect).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle disconnect', () => {
|
||||||
|
// Should not throw error
|
||||||
|
expect(() => uptimeKumaService.disconnect()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get monitor stats', () => {
|
||||||
|
const stats = uptimeKumaService.getMonitorStats();
|
||||||
|
expect(stats).toBeDefined();
|
||||||
|
expect(Array.isArray(stats)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user