Files
dashcaddy/dashcaddy-api/__tests__/update-manager.test.js
Sami f61e85d9a7 Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend,
DashCA certificate distribution, installer script, and deployment skills.
2026-03-05 02:26:12 -08:00

193 lines
6.1 KiB
JavaScript

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