/**
* API Endpoint Tests
*
* Comprehensive tests for critical DashCaddy API endpoints
* Tests the migrated StateManager integration and core functionality
*/
const request = require('supertest');
const fs = require('fs');
const path = require('path');
const os = require('os');
// Create a test instance of the app
// Note: We need to mock the service file to avoid affecting production
const testServicesFile = path.join(os.tmpdir(), `test-services-${Date.now()}.json`);
const testConfigFile = path.join(os.tmpdir(), `test-config-${Date.now()}.json`);
// Set test environment
process.env.SERVICES_FILE = testServicesFile;
process.env.CONFIG_FILE = testConfigFile;
process.env.CADDYFILE_PATH = path.join(os.tmpdir(), 'test-Caddyfile');
process.env.CADDY_ADMIN_URL = 'http://localhost:2019';
process.env.ENABLE_HEALTH_CHECKER = 'false'; // Disable to avoid background processes
process.env.NODE_ENV = 'test';
// Initialize test files
fs.writeFileSync(testServicesFile, '[]', 'utf8');
fs.writeFileSync(testConfigFile, '{}', 'utf8');
fs.writeFileSync(process.env.CADDYFILE_PATH, '# Test Caddyfile', 'utf8');
// Now require the app (after env setup)
const app = require('../server');
describe('API Endpoints', () => {
// Clean up before each test
beforeEach(() => {
fs.writeFileSync(testServicesFile, '[]', 'utf8');
});
// Clean up after all tests
afterAll(() => {
try {
fs.unlinkSync(testServicesFile);
fs.unlinkSync(testConfigFile);
fs.unlinkSync(process.env.CADDYFILE_PATH);
} catch (e) {
// Ignore cleanup errors
}
});
describe('GET /api/health', () => {
test('should return healthy status', async () => {
const res = await request(app).get('/api/health');
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('status', 'ok');
expect(res.body).toHaveProperty('timestamp');
});
});
describe('GET /api/services', () => {
test('should return empty array initially', async () => {
const res = await request(app).get('/api/services');
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
expect(res.body.length).toBe(0);
});
test('should return services after adding', async () => {
// Add a service first
await request(app)
.post('/api/services')
.send({
id: 'test-service',
name: 'Test Service',
logo: '/assets/test.png',
ip: 'localhost',
tailscaleOnly: false,
});
// Now get services
const res = await request(app).get('/api/services');
expect(res.statusCode).toBe(200);
expect(res.body.length).toBe(1);
expect(res.body[0]).toMatchObject({
id: 'test-service',
name: 'Test Service',
});
});
test('should use StateManager (thread-safe)', async () => {
// This test verifies StateManager is being used
// by checking that the file is read correctly
// Manually write to file
const testData = [{ id: 'manual', name: 'Manual Service' }];
fs.writeFileSync(testServicesFile, JSON.stringify(testData, null, 2));
const res = await request(app).get('/api/services');
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(testData);
});
});
describe('POST /api/services', () => {
test('should add a new service', async () => {
const newService = {
id: 'plex',
name: 'Plex',
logo: '/assets/plex.png',
ip: 'localhost',
tailscaleOnly: false,
};
const res = await request(app)
.post('/api/services')
.send(newService);
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('success', true);
// Verify service was added
const services = JSON.parse(fs.readFileSync(testServicesFile, 'utf8'));
expect(services.length).toBe(1);
expect(services[0].id).toBe(newService.id);
expect(services[0].name).toBe(newService.name);
expect(services[0].logo).toBe(newService.logo);
});
test('should reject duplicate service IDs', async () => {
const service = {
id: 'duplicate',
name: 'Duplicate Service',
};
// Add first time
await request(app).post('/api/services').send(service);
// Try to add again
const res = await request(app).post('/api/services').send(service);
expect(res.statusCode).toBe(409); // Conflict is the correct status code
expect(res.body).toHaveProperty('success', false);
expect(res.body.error).toContain('already exists');
});
test('should validate required fields', async () => {
const res = await request(app)
.post('/api/services')
.send({
// Missing 'id' and 'name'
logo: '/assets/test.png',
});
expect(res.statusCode).toBe(400);
expect(res.body).toHaveProperty('success', false);
});
test('should sanitize user input (XSS protection)', async () => {
const maliciousService = {
id: 'test',
name: '
',
logo: '/assets/test.png',
};
const res = await request(app)
.post('/api/services')
.send(maliciousService);
// Input should be sanitized or rejected
const services = JSON.parse(fs.readFileSync(testServicesFile, 'utf8'));
// If the service was added, script tags should be removed or escaped
if (services.length > 0) {
expect(services[0].id).not.toContain('