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