/** * 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'); }); });