237 lines
7.6 KiB
TypeScript
237 lines
7.6 KiB
TypeScript
/**
|
|
* Rolling Restart Tests
|
|
*
|
|
* Test suite for zero-downtime rolling restart orchestrator
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import {
|
|
validatePreRestart,
|
|
validatePostRestart,
|
|
restartService,
|
|
rollingRestart,
|
|
systemctl,
|
|
getServiceStatus,
|
|
checkHttpHealth,
|
|
} from './rolling-restart.js';
|
|
import { getProductionServiceConfig } from './prod-services.js';
|
|
|
|
// Mock child_process
|
|
vi.mock('node:child_process', () => ({
|
|
exec: vi.fn((cmd: string, opts: unknown, callback: (err: Error | null, result: { stdout: string; stderr: string }) => void) => {
|
|
if (typeof opts === 'function') {
|
|
callback = opts as typeof callback;
|
|
}
|
|
// Default mock: command succeeds
|
|
callback(null, { stdout: '', stderr: '' });
|
|
}),
|
|
}));
|
|
|
|
// Mock fs/promises
|
|
vi.mock('node:fs/promises', () => ({
|
|
access: vi.fn().mockResolvedValue(undefined),
|
|
readFile: vi.fn().mockResolvedValue(''),
|
|
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
}));
|
|
|
|
describe('Rolling Restart Orchestrator', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('systemctl', () => {
|
|
it('should execute systemctl command', async () => {
|
|
const result = await systemctl('status', 'lilith-sso-api.service', false);
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('should handle dry-run mode', async () => {
|
|
const result = await systemctl('restart', 'lilith-sso-api.service', true);
|
|
expect(result.success).toBe(true);
|
|
expect(result.output).toBe('[dry-run]');
|
|
});
|
|
});
|
|
|
|
describe('checkHttpHealth', () => {
|
|
it('should check HTTP health endpoint', async () => {
|
|
const result = await checkHttpHealth('http://localhost:3000/health');
|
|
expect(result).toHaveProperty('healthy');
|
|
expect(result).toHaveProperty('responseTime');
|
|
});
|
|
|
|
it('should handle connection failures', async () => {
|
|
const result = await checkHttpHealth('http://localhost:9999/health');
|
|
expect(result.healthy).toBe(false);
|
|
expect(result).toHaveProperty('error');
|
|
});
|
|
});
|
|
|
|
describe('validatePreRestart', () => {
|
|
it('should validate service health before restart', async () => {
|
|
const config = getProductionServiceConfig('sso.api', 3001);
|
|
const result = await validatePreRestart('sso.api', config);
|
|
expect(typeof result).toBe('boolean');
|
|
});
|
|
|
|
it('should skip validation in force mode', async () => {
|
|
const config = getProductionServiceConfig('sso.api', 3001);
|
|
const result = await validatePreRestart('sso.api', config, true);
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('validatePostRestart', () => {
|
|
it('should validate service health after restart', async () => {
|
|
const config = getProductionServiceConfig('sso.api', 3001);
|
|
const result = await validatePostRestart('sso.api', config);
|
|
expect(typeof result).toBe('boolean');
|
|
});
|
|
|
|
it('should skip validation in force mode', async () => {
|
|
const config = getProductionServiceConfig('sso.api', 3001);
|
|
const result = await validatePostRestart('sso.api', config, true);
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('restartService', () => {
|
|
it('should restart a single service', async () => {
|
|
const result = await restartService('sso.api', { dryRun: true });
|
|
expect(typeof result).toBe('boolean');
|
|
});
|
|
|
|
it('should handle dry-run mode', async () => {
|
|
const result = await restartService('sso.api', { dryRun: true });
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should handle force mode', async () => {
|
|
const result = await restartService('sso.api', {
|
|
dryRun: true,
|
|
force: true,
|
|
});
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should skip migrations when requested', async () => {
|
|
const result = await restartService('sso.api', {
|
|
dryRun: true,
|
|
skipMigrations: true,
|
|
});
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('rollingRestart', () => {
|
|
it('should restart all services in dependency order', async () => {
|
|
const result = await rollingRestart(undefined, { dryRun: true });
|
|
|
|
expect(result).toHaveProperty('success');
|
|
expect(result).toHaveProperty('servicesRestarted');
|
|
expect(result).toHaveProperty('servicesFailed');
|
|
expect(result).toHaveProperty('totalTime');
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('should restart specific services only', async () => {
|
|
const services = ['sso.api'];
|
|
const result = await rollingRestart(services, { dryRun: true });
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('should handle empty service list', async () => {
|
|
const result = await rollingRestart([], { dryRun: true });
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.servicesRestarted.length).toBe(0);
|
|
});
|
|
|
|
it('should handle force mode', async () => {
|
|
const result = await rollingRestart(undefined, {
|
|
dryRun: true,
|
|
force: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Dependency Ordering', () => {
|
|
it('should restart infrastructure before APIs', async () => {
|
|
const services = ['sso.api', 'sso.postgresql', 'sso.redis'];
|
|
const result = await rollingRestart(services, { dryRun: true });
|
|
|
|
expect(result.success).toBe(true);
|
|
// In dry-run, services aren't actually restarted, so we just verify no errors
|
|
});
|
|
|
|
it('should restart dependencies before dependents', async () => {
|
|
const services = ['marketplace.api', 'sso.api', 'profile.api'];
|
|
const result = await rollingRestart(services, { dryRun: true });
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should abort on first failure', async () => {
|
|
// This test would require mocking a failure scenario
|
|
// In a real test, we'd mock systemctl to fail for a specific service
|
|
const result = await rollingRestart(['sso.api'], { dryRun: true });
|
|
|
|
expect(result).toHaveProperty('servicesFailed');
|
|
});
|
|
|
|
it('should rollback on post-restart failure', async () => {
|
|
// This test would require mocking post-restart health check failure
|
|
// Implementation depends on mocking strategy
|
|
expect(true).toBe(true); // Placeholder
|
|
});
|
|
});
|
|
|
|
describe('getServiceStatus', () => {
|
|
it('should get systemd service status', async () => {
|
|
const status = await getServiceStatus('lilith-sso-api.service');
|
|
|
|
expect(status).toHaveProperty('active');
|
|
expect(status).toHaveProperty('running');
|
|
expect(status).toHaveProperty('failed');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Integration Tests', () => {
|
|
describe('End-to-End Rolling Restart', () => {
|
|
it('should perform complete rolling restart in dry-run', async () => {
|
|
const result = await rollingRestart(undefined, { dryRun: true });
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.servicesRestarted).toEqual([]);
|
|
expect(result.servicesFailed).toEqual([]);
|
|
});
|
|
|
|
it('should restart single service with all steps', async () => {
|
|
const result = await restartService('sso.api', {
|
|
dryRun: true,
|
|
skipMigrations: false,
|
|
deployCode: false,
|
|
});
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Rollback Scenarios', () => {
|
|
it('should rollback on health check failure', async () => {
|
|
// Mock implementation would set up a scenario where health check fails
|
|
// and verify rollback is triggered
|
|
expect(true).toBe(true); // Placeholder
|
|
});
|
|
|
|
it('should restore backup unit file', async () => {
|
|
// Mock implementation would verify backup restoration
|
|
expect(true).toBe(true); // Placeholder
|
|
});
|
|
});
|
|
});
|