Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
192
dashcaddy-api/__tests__/update-manager.test.js
Normal file
192
dashcaddy-api/__tests__/update-manager.test.js
Normal file
@@ -0,0 +1,192 @@
|
||||
// update-manager.js creates a Docker instance at module level.
|
||||
// On test machines without Docker, this is fine — Docker methods are only called
|
||||
// in async methods that we won't invoke in unit tests.
|
||||
|
||||
const updateManager = require('../update-manager');
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset singleton state
|
||||
updateManager.history = [];
|
||||
updateManager.availableUpdates = new Map();
|
||||
updateManager.config = { autoUpdate: {} };
|
||||
updateManager.checking = false;
|
||||
if (updateManager.checkInterval) {
|
||||
clearInterval(updateManager.checkInterval);
|
||||
updateManager.checkInterval = null;
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
updateManager.stop();
|
||||
});
|
||||
|
||||
describe('extractTag', () => {
|
||||
test('extracts tag from "nginx:latest"', () => {
|
||||
expect(updateManager.extractTag('nginx:latest')).toBe('latest');
|
||||
});
|
||||
|
||||
test('returns "latest" when no tag specified', () => {
|
||||
expect(updateManager.extractTag('nginx')).toBe('latest');
|
||||
});
|
||||
|
||||
test('extracts tag from registry/repo:tag format', () => {
|
||||
expect(updateManager.extractTag('docker.io/library/nginx:1.21')).toBe('1.21');
|
||||
});
|
||||
|
||||
test('handles tags with dots and hyphens', () => {
|
||||
expect(updateManager.extractTag('myapp:v1.2.3-rc1')).toBe('v1.2.3-rc1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseAuthHeader', () => {
|
||||
test('returns null for null header', () => {
|
||||
expect(updateManager.parseAuthHeader(null)).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null for non-Bearer header', () => {
|
||||
expect(updateManager.parseAuthHeader('Basic realm="test"')).toBeNull();
|
||||
});
|
||||
|
||||
test('parses Bearer realm URL', () => {
|
||||
const header = 'Bearer realm="https://auth.docker.io/token"';
|
||||
const result = updateManager.parseAuthHeader(header);
|
||||
expect(result).toContain('https://auth.docker.io/token');
|
||||
});
|
||||
|
||||
test('includes service parameter', () => {
|
||||
const header = 'Bearer realm="https://auth.docker.io/token",service="registry.docker.io"';
|
||||
const result = updateManager.parseAuthHeader(header);
|
||||
expect(result).toContain('service=registry.docker.io');
|
||||
});
|
||||
|
||||
test('includes scope parameter', () => {
|
||||
const header = 'Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/nginx:pull"';
|
||||
const result = updateManager.parseAuthHeader(header);
|
||||
expect(result).toContain('scope=');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAvailableUpdates', () => {
|
||||
test('returns empty array initially', () => {
|
||||
expect(updateManager.getAvailableUpdates()).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns array from availableUpdates map', () => {
|
||||
updateManager.availableUpdates.set('c1', { containerId: 'c1', imageName: 'nginx' });
|
||||
const updates = updateManager.getAvailableUpdates();
|
||||
expect(updates).toHaveLength(1);
|
||||
expect(updates[0].containerId).toBe('c1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHistory', () => {
|
||||
test('returns entries in reverse order', () => {
|
||||
updateManager.addToHistory({ containerId: 'c1', status: 'success' });
|
||||
updateManager.addToHistory({ containerId: 'c2', status: 'success' });
|
||||
const history = updateManager.getHistory();
|
||||
expect(history[0].containerId).toBe('c2');
|
||||
});
|
||||
|
||||
test('returns empty array when no history', () => {
|
||||
expect(updateManager.getHistory()).toEqual([]);
|
||||
});
|
||||
|
||||
test('respects limit parameter', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
updateManager.addToHistory({ containerId: `c${i}` });
|
||||
}
|
||||
expect(updateManager.getHistory(3)).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addToHistory', () => {
|
||||
test('appends entry', () => {
|
||||
updateManager.addToHistory({ containerId: 'c1' });
|
||||
expect(updateManager.history).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('trims to 100 entries', () => {
|
||||
for (let i = 0; i < 105; i++) {
|
||||
updateManager.addToHistory({ containerId: `c${i}` });
|
||||
}
|
||||
expect(updateManager.history.length).toBeLessThanOrEqual(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('configureAutoUpdate', () => {
|
||||
test('creates autoUpdate config section', () => {
|
||||
updateManager.configureAutoUpdate('c1', { enabled: true });
|
||||
expect(updateManager.config.autoUpdate['c1']).toBeDefined();
|
||||
});
|
||||
|
||||
test('stores container-specific config', () => {
|
||||
updateManager.configureAutoUpdate('c1', {
|
||||
enabled: true,
|
||||
schedule: 'daily',
|
||||
securityOnly: true
|
||||
});
|
||||
expect(updateManager.config.autoUpdate['c1'].schedule).toBe('daily');
|
||||
expect(updateManager.config.autoUpdate['c1'].securityOnly).toBe(true);
|
||||
});
|
||||
|
||||
test('defaults autoRollback to true', () => {
|
||||
updateManager.configureAutoUpdate('c1', { enabled: true });
|
||||
expect(updateManager.config.autoUpdate['c1'].autoRollback).toBe(true);
|
||||
});
|
||||
|
||||
test('defaults schedule to weekly', () => {
|
||||
updateManager.configureAutoUpdate('c1', {});
|
||||
expect(updateManager.config.autoUpdate['c1'].schedule).toBe('weekly');
|
||||
});
|
||||
});
|
||||
|
||||
describe('scheduleUpdate', () => {
|
||||
test('throws for past scheduled time', () => {
|
||||
const past = new Date(Date.now() - 60000).toISOString();
|
||||
expect(() => updateManager.scheduleUpdate('c1', past)).toThrow('Scheduled time must be in the future');
|
||||
});
|
||||
|
||||
test('accepts future scheduled time', () => {
|
||||
jest.useFakeTimers();
|
||||
const future = new Date(Date.now() + 60000).toISOString();
|
||||
expect(() => updateManager.scheduleUpdate('c1', future)).not.toThrow();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChangelog', () => {
|
||||
test('returns placeholder response', async () => {
|
||||
const result = await updateManager.getChangelog('nginx:latest');
|
||||
expect(result.imageName).toBe('nginx:latest');
|
||||
expect(result.changelog).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('start / stop', () => {
|
||||
test('start sets checking flag', () => {
|
||||
jest.useFakeTimers();
|
||||
updateManager.start();
|
||||
expect(updateManager.checking).toBe(true);
|
||||
updateManager.stop();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('stop clears interval', () => {
|
||||
jest.useFakeTimers();
|
||||
updateManager.start();
|
||||
updateManager.stop();
|
||||
expect(updateManager.checking).toBe(false);
|
||||
expect(updateManager.checkInterval).toBeNull();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('start is idempotent', () => {
|
||||
jest.useFakeTimers();
|
||||
updateManager.start();
|
||||
const first = updateManager.checkInterval;
|
||||
updateManager.start();
|
||||
expect(updateManager.checkInterval).toBe(first);
|
||||
updateManager.stop();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user