feat(automation): implement NODE_ENV-based automation mode with fixture server
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { loadAutomationConfig, AutomationMode } from '../../../packages/infrastructure/config/AutomationConfig';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { loadAutomationConfig, getAutomationMode, AutomationMode } from '../../../packages/infrastructure/config/AutomationConfig';
|
||||
|
||||
describe('AutomationConfig', () => {
|
||||
const originalEnv = process.env;
|
||||
@@ -14,14 +14,114 @@ describe('AutomationConfig', () => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('getAutomationMode', () => {
|
||||
describe('NODE_ENV-based mode detection', () => {
|
||||
it('should return development mode when NODE_ENV=development', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const mode = getAutomationMode();
|
||||
|
||||
expect(mode).toBe('development');
|
||||
});
|
||||
|
||||
it('should return production mode when NODE_ENV=production', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const mode = getAutomationMode();
|
||||
|
||||
expect(mode).toBe('production');
|
||||
});
|
||||
|
||||
it('should return test mode when NODE_ENV=test', () => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const mode = getAutomationMode();
|
||||
|
||||
expect(mode).toBe('test');
|
||||
});
|
||||
|
||||
it('should return development mode when NODE_ENV is not set', () => {
|
||||
delete process.env.NODE_ENV;
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const mode = getAutomationMode();
|
||||
|
||||
expect(mode).toBe('development');
|
||||
});
|
||||
|
||||
it('should return development mode for unknown NODE_ENV values', () => {
|
||||
process.env.NODE_ENV = 'staging';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const mode = getAutomationMode();
|
||||
|
||||
expect(mode).toBe('development');
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy AUTOMATION_MODE support', () => {
|
||||
it('should map legacy dev mode to development with deprecation warning', () => {
|
||||
process.env.AUTOMATION_MODE = 'dev';
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
const mode = getAutomationMode();
|
||||
|
||||
expect(mode).toBe('development');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[DEPRECATED] AUTOMATION_MODE')
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should map legacy mock mode to test with deprecation warning', () => {
|
||||
process.env.AUTOMATION_MODE = 'mock';
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
const mode = getAutomationMode();
|
||||
|
||||
expect(mode).toBe('test');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[DEPRECATED] AUTOMATION_MODE')
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should map legacy production mode to production with deprecation warning', () => {
|
||||
process.env.AUTOMATION_MODE = 'production';
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
const mode = getAutomationMode();
|
||||
|
||||
expect(mode).toBe('production');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[DEPRECATED] AUTOMATION_MODE')
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should ignore invalid AUTOMATION_MODE and use NODE_ENV', () => {
|
||||
process.env.AUTOMATION_MODE = 'invalid-mode';
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
const mode = getAutomationMode();
|
||||
|
||||
expect(mode).toBe('production');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadAutomationConfig', () => {
|
||||
describe('default configuration', () => {
|
||||
it('should return mock mode when AUTOMATION_MODE is not set', () => {
|
||||
it('should return development mode when NODE_ENV is not set', () => {
|
||||
delete process.env.NODE_ENV;
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.mode).toBe('mock');
|
||||
expect(config.mode).toBe('development');
|
||||
});
|
||||
|
||||
it('should return default devTools configuration', () => {
|
||||
@@ -46,15 +146,27 @@ describe('AutomationConfig', () => {
|
||||
expect(config.retryAttempts).toBe(3);
|
||||
expect(config.screenshotOnError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dev mode configuration', () => {
|
||||
it('should return dev mode when AUTOMATION_MODE=dev', () => {
|
||||
process.env.AUTOMATION_MODE = 'dev';
|
||||
it('should return default fixture server configuration', () => {
|
||||
delete process.env.NODE_ENV;
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.mode).toBe('dev');
|
||||
expect(config.fixtureServer?.port).toBe(3456);
|
||||
expect(config.fixtureServer?.autoStart).toBe(true);
|
||||
expect(config.fixtureServer?.fixturesPath).toBe('./resources/iracing-hosted-sessions');
|
||||
});
|
||||
});
|
||||
|
||||
describe('development mode configuration', () => {
|
||||
it('should return development mode when NODE_ENV=development', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.mode).toBe('development');
|
||||
});
|
||||
|
||||
it('should parse CHROME_DEBUG_PORT', () => {
|
||||
@@ -75,8 +187,9 @@ describe('AutomationConfig', () => {
|
||||
});
|
||||
|
||||
describe('production mode configuration', () => {
|
||||
it('should return production mode when AUTOMATION_MODE=production', () => {
|
||||
process.env.AUTOMATION_MODE = 'production';
|
||||
it('should return production mode when NODE_ENV=production', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
@@ -161,28 +274,87 @@ describe('AutomationConfig', () => {
|
||||
expect(config.nutJs?.confidence).toBe(0.9);
|
||||
});
|
||||
|
||||
it('should fallback to mock mode for invalid AUTOMATION_MODE', () => {
|
||||
process.env.AUTOMATION_MODE = 'invalid-mode';
|
||||
it('should fallback to development mode for invalid NODE_ENV', () => {
|
||||
process.env.NODE_ENV = 'invalid-env';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.mode).toBe('mock');
|
||||
expect(config.mode).toBe('development');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fixture server configuration', () => {
|
||||
it('should auto-start fixture server in development mode', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.fixtureServer?.autoStart).toBe(true);
|
||||
});
|
||||
|
||||
it('should not auto-start fixture server in production mode', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.fixtureServer?.autoStart).toBe(false);
|
||||
});
|
||||
|
||||
it('should not auto-start fixture server in test mode', () => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.fixtureServer?.autoStart).toBe(false);
|
||||
});
|
||||
|
||||
it('should parse FIXTURE_SERVER_PORT', () => {
|
||||
process.env.FIXTURE_SERVER_PORT = '4567';
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.fixtureServer?.port).toBe(4567);
|
||||
});
|
||||
|
||||
it('should parse FIXTURE_SERVER_PATH', () => {
|
||||
process.env.FIXTURE_SERVER_PATH = '/custom/fixtures';
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.fixtureServer?.fixturesPath).toBe('/custom/fixtures');
|
||||
});
|
||||
|
||||
it('should respect FIXTURE_SERVER_AUTO_START=false', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
process.env.FIXTURE_SERVER_AUTO_START = 'false';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.fixtureServer?.autoStart).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('full configuration scenario', () => {
|
||||
it('should load complete dev environment configuration', () => {
|
||||
process.env.AUTOMATION_MODE = 'dev';
|
||||
it('should load complete development environment configuration', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
process.env.CHROME_DEBUG_PORT = '9222';
|
||||
process.env.CHROME_WS_ENDPOINT = 'ws://localhost:9222/devtools/browser/test';
|
||||
process.env.AUTOMATION_TIMEOUT = '45000';
|
||||
process.env.RETRY_ATTEMPTS = '2';
|
||||
process.env.SCREENSHOT_ON_ERROR = 'true';
|
||||
process.env.FIXTURE_SERVER_PORT = '3456';
|
||||
process.env.FIXTURE_SERVER_PATH = './resources/iracing-hosted-sessions';
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config).toEqual({
|
||||
mode: 'dev',
|
||||
mode: 'development',
|
||||
devTools: {
|
||||
debuggingPort: 9222,
|
||||
browserWSEndpoint: 'ws://localhost:9222/devtools/browser/test',
|
||||
@@ -194,20 +366,41 @@ describe('AutomationConfig', () => {
|
||||
templatePath: './resources/templates',
|
||||
confidence: 0.9,
|
||||
},
|
||||
fixtureServer: {
|
||||
port: 3456,
|
||||
autoStart: true,
|
||||
fixturesPath: './resources/iracing-hosted-sessions',
|
||||
},
|
||||
defaultTimeout: 45000,
|
||||
retryAttempts: 2,
|
||||
screenshotOnError: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should load complete mock environment configuration', () => {
|
||||
process.env.AUTOMATION_MODE = 'mock';
|
||||
it('should load complete test environment configuration', () => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.mode).toBe('mock');
|
||||
expect(config.mode).toBe('test');
|
||||
expect(config.devTools).toBeDefined();
|
||||
expect(config.nutJs).toBeDefined();
|
||||
expect(config.fixtureServer).toBeDefined();
|
||||
expect(config.fixtureServer?.autoStart).toBe(false);
|
||||
});
|
||||
|
||||
it('should load complete production environment configuration', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.mode).toBe('production');
|
||||
expect(config.devTools).toBeDefined();
|
||||
expect(config.nutJs).toBeDefined();
|
||||
expect(config.fixtureServer).toBeDefined();
|
||||
expect(config.fixtureServer?.autoStart).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
174
tests/unit/infrastructure/FixtureServerService.test.ts
Normal file
174
tests/unit/infrastructure/FixtureServerService.test.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as http from 'http';
|
||||
import * as path from 'path';
|
||||
import { FixtureServerService } from '@/packages/infrastructure/adapters/automation/FixtureServerService';
|
||||
|
||||
describe('FixtureServerService', () => {
|
||||
let service: FixtureServerService;
|
||||
let testPort: number;
|
||||
const fixturesPath = './resources/iracing-hosted-sessions';
|
||||
|
||||
beforeEach(() => {
|
||||
service = new FixtureServerService();
|
||||
testPort = 13400 + Math.floor(Math.random() * 100);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (service.isRunning()) {
|
||||
await service.stop();
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
it('should start the server on specified port', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
expect(service.isRunning()).toBe(true);
|
||||
expect(service.getBaseUrl()).toBe(`http://localhost:${testPort}`);
|
||||
});
|
||||
|
||||
it('should throw error if server is already running', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
await expect(service.start(testPort, fixturesPath)).rejects.toThrow(
|
||||
'Fixture server is already running'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if fixtures path does not exist', async () => {
|
||||
await expect(service.start(testPort, './non-existent-path')).rejects.toThrow(
|
||||
/Fixtures path does not exist/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stop', () => {
|
||||
it('should stop a running server', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
expect(service.isRunning()).toBe(true);
|
||||
|
||||
await service.stop();
|
||||
|
||||
expect(service.isRunning()).toBe(false);
|
||||
});
|
||||
|
||||
it('should resolve when server is not running', async () => {
|
||||
expect(service.isRunning()).toBe(false);
|
||||
|
||||
await expect(service.stop()).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('waitForReady', () => {
|
||||
it('should return true when server is ready', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
const isReady = await service.waitForReady(5000);
|
||||
|
||||
expect(isReady).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when server is not running', async () => {
|
||||
const isReady = await service.waitForReady(500);
|
||||
|
||||
expect(isReady).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBaseUrl', () => {
|
||||
it('should return correct base URL with default port', () => {
|
||||
expect(service.getBaseUrl()).toBe('http://localhost:3456');
|
||||
});
|
||||
|
||||
it('should return correct base URL after starting on custom port', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
expect(service.getBaseUrl()).toBe(`http://localhost:${testPort}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRunning', () => {
|
||||
it('should return false when server is not started', () => {
|
||||
expect(service.isRunning()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when server is running', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
expect(service.isRunning()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTTP serving', () => {
|
||||
it('should serve HTML files from fixtures path', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
const response = await makeRequest(`http://localhost:${testPort}/01-hosted-racing.html`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.headers['content-type']).toBe('text/html');
|
||||
expect(response.body).toContain('<!DOCTYPE html');
|
||||
});
|
||||
|
||||
it('should serve health endpoint', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
const response = await makeRequest(`http://localhost:${testPort}/health`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.headers['content-type']).toBe('application/json');
|
||||
expect(JSON.parse(response.body)).toEqual({ status: 'ok' });
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent files', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
const response = await makeRequest(`http://localhost:${testPort}/non-existent.html`);
|
||||
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should include CORS headers', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
const response = await makeRequest(`http://localhost:${testPort}/health`);
|
||||
|
||||
expect(response.headers['access-control-allow-origin']).toBe('*');
|
||||
});
|
||||
|
||||
it('should return 404 for path traversal attempts', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
const response = await makeRequest(`http://localhost:${testPort}/../package.json`);
|
||||
|
||||
expect([403, 404]).toContain(response.statusCode);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
interface HttpResponse {
|
||||
statusCode: number;
|
||||
headers: Record<string, string>;
|
||||
body: string;
|
||||
}
|
||||
|
||||
function makeRequest(url: string): Promise<HttpResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(url, (res) => {
|
||||
let body = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
resolve({
|
||||
statusCode: res.statusCode || 0,
|
||||
headers: res.headers as Record<string, string>,
|
||||
body,
|
||||
});
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user