From 6fe1af28aeb04a210b2a3af78efe227fca5c2fd9 Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 12 Aug 2026 13:12:38 -0700 Subject: [PATCH] Add tests for DC-100 discover + DC-107 disaster recovery endpoints 8 new tests covering: - Service discovery: 503 without Docker, pattern matching, empty list, errors - Disaster recovery: status, backup creation, restore validation, file restoration - 1656 tests pass, 73 suites --- .../routes/discover-disaster.routes.test.js | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 dashcaddy-api/__tests__/routes/discover-disaster.routes.test.js diff --git a/dashcaddy-api/__tests__/routes/discover-disaster.routes.test.js b/dashcaddy-api/__tests__/routes/discover-disaster.routes.test.js new file mode 100644 index 0000000..0d2037c --- /dev/null +++ b/dashcaddy-api/__tests__/routes/discover-disaster.routes.test.js @@ -0,0 +1,138 @@ +/** + * DC-100: Service discovery + DC-107: Disaster recovery endpoint tests + */ +const express = require('express'); +const request = require('supertest'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +function createDiscoverApp(docker, servicesStateManager) { + const app = express(); + app.use(express.json()); + const routes = require('../../routes/discover'); + const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); + app.use('/api/v1', routes({ docker, servicesStateManager, asyncHandler: wrap })); + return app; +} + +function createDisasterApp(platformPaths, log) { + const app = express(); + app.use(express.json()); + const routes = require('../../routes/disaster-recovery'); + const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); + app.use('/api/v1', routes({ platformPaths, log: log || { info: jest.fn(), error: jest.fn() }, asyncHandler: wrap })); + return app; +} + +describe('DC-100: Service Discovery', () => { + it('returns 503 when Docker is not available', async () => { + const app = createDiscoverApp(null, null); + const res = await request(app).get('/api/v1/discover'); + expect(res.status).toBe(503); + expect(res.body.success).toBe(false); + }); + + it('discovers running containers with pattern matching', async () => { + const mockDocker = { + client: { + listContainers: jest.fn().mockResolvedValue([ + { + Id: 'abc123def456', + Names: ['/plex-server'], + Image: 'plexinc/pms-docker:latest', + State: 'running', + Ports: [{ IP: '0.0.0.0', PrivatePort: 32400, PublicPort: 32400, Type: 'tcp' }], + Labels: {}, + }, + ]), + }, + }; + + const app = createDiscoverApp(mockDocker, { read: jest.fn().mockResolvedValue([]) }); + const res = await request(app).get('/api/v1/discover'); + + expect(res.status).toBe(200); + expect(res.body.total).toBe(1); + expect(res.body.discovered[0].suggested.type).toBe('plex'); + }); + + it('handles empty container list', async () => { + const app = createDiscoverApp({ client: { listContainers: jest.fn().mockResolvedValue([]) } }, null); + const res = await request(app).get('/api/v1/discover'); + expect(res.status).toBe(200); + expect(res.body.total).toBe(0); + }); + + it('returns 500 on Docker error', async () => { + const app = createDiscoverApp({ client: { listContainers: jest.fn().mockRejectedValue(new Error('fail')) } }, null); + const res = await request(app).get('/api/v1/discover'); + expect(res.status).toBe(500); + }); +}); + +describe('DC-107: Disaster Recovery', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dc-dr-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('GET /disaster/status returns empty status initially', async () => { + const app = createDisasterApp({ dataDir: tmpDir }); + const res = await request(app).get('/api/v1/disaster/status'); + expect(res.status).toBe(200); + expect(res.body.lastBackup).toBeTruthy(); + expect(res.body.lastBackup.status).toBeNull(); + }); + + it('POST /disaster/backup creates snapshot', async () => { + // Create a services.json so backup has data + fs.writeFileSync(path.join(tmpDir, 'services.json'), JSON.stringify([{ id: 'test' }])); + fs.writeFileSync(path.join(tmpDir, 'config.json'), JSON.stringify({ tld: '.sami' })); + + const app = createDisasterApp({ dataDir: tmpDir }); + const res = await request(app).post('/api/v1/disaster/backup'); + + expect(res.status).toBe(200); + expect(res.body.version).toBe('1.0'); + expect(res.body.files.services).toBeTruthy(); + expect(res.body.files.config).toBeTruthy(); + expect(res.body.checksum).toBeTruthy(); + }); + + it('POST /disaster/restore rejects invalid snapshot', async () => { + const app = createDisasterApp({ dataDir: tmpDir }); + const res = await request(app) + .post('/api/v1/disaster/restore') + .send({ foo: 'bar' }); + + expect(res.status).toBe(400); + }); + + it('POST /disaster/restore restores files', async () => { + const app = createDisasterApp({ dataDir: tmpDir }); + const res = await request(app) + .post('/api/v1/disaster/restore') + .send({ + version: '1.0', + files: { + services: [{ id: 'restored-svc' }], + config: { tld: '.test' }, + }, + }); + + expect(res.status).toBe(200); + expect(res.body.status).toBe('success'); + expect(res.body.restored).toContain('services.json'); + expect(res.body.restored).toContain('config.json'); + + // Verify files were written + const svc = JSON.parse(fs.readFileSync(path.join(tmpDir, 'services.json'), 'utf8')); + expect(svc[0].id).toBe('restored-svc'); + }); +});