/** * 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('