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
This commit is contained in:
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user