UNPKG

lightweight-browser-load-tester

Version:

A lightweight load testing tool using real browsers for streaming applications with DRM support

770 lines (681 loc) 25.5 kB
/** * Integration tests for CLI interface and main application entry point */ import { describe, it, expect, beforeEach, afterEach, vi, Mock } from 'vitest'; import { spawn, ChildProcess } from 'child_process'; import { writeFileSync, unlinkSync, existsSync } from 'fs'; import { resolve } from 'path'; import { LoadTesterApp } from './index'; import { TestConfiguration } from './types'; import { ConfigurationManager } from './config'; // Mock external dependencies vi.mock('./controllers/test-runner'); vi.mock('./aggregators/results-aggregator'); vi.mock('./exporters/prometheus-exporter'); vi.mock('./exporters/opentelemetry-exporter'); describe('CLI Interface Integration Tests', () => { const testConfigFile = resolve(__dirname, '../test-config.yaml'); const testOutputFile = resolve(__dirname, '../test-results.json'); beforeEach(() => { // Clean up any existing test files if (existsSync(testConfigFile)) { unlinkSync(testConfigFile); } if (existsSync(testOutputFile)) { unlinkSync(testOutputFile); } }); afterEach(() => { // Clean up test files if (existsSync(testConfigFile)) { unlinkSync(testConfigFile); } if (existsSync(testOutputFile)) { unlinkSync(testOutputFile); } }); describe('CLI Command Parsing', () => { it('should parse basic test command arguments', async () => { // Test actual CLI argument parsing const result = await ConfigurationManager.parseConfiguration({ cliArgs: [ 'node', 'load-tester', 'test', '--concurrent-users', '5', '--test-duration', '60', '--streaming-url', 'https://example.com/stream' ] }); expect(result.config.concurrentUsers).toBe(5); expect(result.config.testDuration).toBe(60); expect(result.config.streamingUrl).toBe('https://example.com/stream'); }); it('should handle DRM configuration from CLI', async () => { const result = await ConfigurationManager.parseConfiguration({ cliArgs: [ 'node', 'load-tester', 'test', '--streaming-url', 'https://example.com/drm-stream', '--drm-type', 'widevine', '--drm-license-url', 'https://example.com/license', '--drm-cert-url', 'https://example.com/cert' ] }); expect(result.config.drmConfig).toBeDefined(); expect(result.config.drmConfig?.type).toBe('widevine'); expect(result.config.drmConfig?.licenseUrl).toBe('https://example.com/license'); }); it('should handle Prometheus configuration from CLI', async () => { const result = await ConfigurationManager.parseConfiguration({ cliArgs: [ 'node', 'load-tester', 'test', '--streaming-url', 'https://example.com/stream', '--prometheus-enabled', '--prometheus-url', 'https://prometheus.example.com/api/v1/write', '--prometheus-username', 'testuser', '--prometheus-password', 'testpass' ] }); expect(result.config.prometheus).toBeDefined(); expect(result.config.prometheus?.enabled).toBe(true); expect(result.config.prometheus?.remoteWriteUrl).toBe('https://prometheus.example.com/api/v1/write'); }); it('should handle OpenTelemetry configuration from CLI', async () => { const result = await ConfigurationManager.parseConfiguration({ cliArgs: [ 'node', 'load-tester', 'test', '--streaming-url', 'https://example.com/stream', '--otel-enabled', '--otel-endpoint', 'https://otel-collector.example.com:4318/v1/metrics', '--otel-protocol', 'grpc', '--otel-service-name', 'my-load-tester', '--otel-service-version', '2.0.0' ] }); expect(result.config.opentelemetry).toBeDefined(); expect(result.config.opentelemetry?.enabled).toBe(true); expect(result.config.opentelemetry?.endpoint).toBe('https://otel-collector.example.com:4318/v1/metrics'); expect(result.config.opentelemetry?.protocol).toBe('grpc'); expect(result.config.opentelemetry?.serviceName).toBe('my-load-tester'); expect(result.config.opentelemetry?.serviceVersion).toBe('2.0.0'); }); }); describe('Configuration File Integration', () => { it('should load configuration from YAML file', async () => { const yamlConfig = ` concurrentUsers: 3 testDuration: 120 rampUpTime: 15 streamingUrl: "https://example.com/yaml-stream" drmConfig: type: "widevine" licenseUrl: "https://example.com/yaml-license" resourceLimits: maxMemoryPerInstance: 1024 maxCpuPercentage: 90 maxConcurrentInstances: 5 requestParameters: - target: "header" name: "Authorization" valueTemplate: "Bearer {{token}}" scope: "per-session" `; writeFileSync(testConfigFile, yamlConfig); const result = await ConfigurationManager.parseConfiguration({ configFile: testConfigFile }); expect(result.config.concurrentUsers).toBe(3); expect(result.config.testDuration).toBe(120); expect(result.config.streamingUrl).toBe('https://example.com/yaml-stream'); expect(result.config.drmConfig?.type).toBe('widevine'); expect(result.config.requestParameters).toHaveLength(1); expect(result.config.requestParameters[0].name).toBe('Authorization'); }); it('should generate example configuration files', async () => { const yamlExample = ConfigurationManager.generateExampleConfig('yaml'); const jsonExample = ConfigurationManager.generateExampleConfig('json'); expect(yamlExample).toContain('concurrentUsers: 5'); expect(yamlExample).toContain('streamingUrl: https://example.com/stream'); expect(yamlExample).toContain('type: widevine'); expect(jsonExample).toContain('"concurrentUsers": 5'); expect(jsonExample).toContain('"streamingUrl": "https://example.com/stream"'); expect(jsonExample).toContain('"type": "widevine"'); }); it('should validate configuration files', async () => { const invalidConfig = ` concurrentUsers: -1 testDuration: 0 rampUpTime: -5 streamingUrl: "not-a-valid-url" resourceLimits: maxMemoryPerInstance: 50 maxCpuPercentage: 150 maxConcurrentInstances: 0 `; writeFileSync(testConfigFile, invalidConfig); // With validateOnly: true, it should not throw even with invalid config const result = await ConfigurationManager.parseConfiguration({ configFile: testConfigFile, validateOnly: true }); // Should return the config without validation expect(result.config).toBeDefined(); expect(result.config.streamingUrl).toBe('not-a-valid-url'); }); }); describe('Environment Variable Integration', () => { const originalEnv = process.env; beforeEach(() => { process.env = { ...originalEnv }; }); afterEach(() => { process.env = originalEnv; }); it('should load configuration from environment variables', async () => { process.env.LOAD_TEST_CONCURRENT_USERS = '7'; process.env.LOAD_TEST_DURATION = '180'; process.env.LOAD_TEST_STREAMING_URL = 'https://example.com/env-stream'; process.env.LOAD_TEST_DRM_TYPE = 'playready'; process.env.LOAD_TEST_DRM_LICENSE_URL = 'https://example.com/env-license'; const result = await ConfigurationManager.parseConfiguration({}); expect(result.config.concurrentUsers).toBe(7); expect(result.config.testDuration).toBe(180); expect(result.config.streamingUrl).toBe('https://example.com/env-stream'); expect(result.config.drmConfig?.type).toBe('playready'); expect(result.config.drmConfig?.licenseUrl).toBe('https://example.com/env-license'); }); }); }); describe.skip('LoadTesterApp Integration Tests', () => { let app: LoadTesterApp; let mockConfig: TestConfiguration; beforeEach(() => { mockConfig = { concurrentUsers: 2, testDuration: 10, rampUpTime: 2, streamingUrl: 'https://example.com/test-stream', requestParameters: [], resourceLimits: { maxMemoryPerInstance: 512, maxCpuPercentage: 80, maxConcurrentInstances: 5 } }; // Mock console methods to avoid noise in tests vi.spyOn(console, 'log').mockImplementation(() => {}); vi.spyOn(console, 'error').mockImplementation(() => {}); vi.spyOn(process.stdout, 'write').mockImplementation(() => true); }); afterEach(async () => { if (app) { await app.stop(); } vi.restoreAllMocks(); }); describe('Application Lifecycle', () => { it.skip('should initialize and coordinate all components', async () => { // TODO: Fix this test - it's causing the test suite to hang // The issue is with the complex interaction between app.start() and the mocked TestRunner events. }); it.skip('should handle graceful shutdown', async () => { // TODO: Fix this test - it's causing the test suite to hang // The issue is with the complex interaction between app.start(), app.stop(), // and the mocked TestRunner events. Need to redesign this test. }); it('should handle application errors gracefully', async () => { app = new LoadTesterApp(mockConfig); const { TestRunner } = await import('./controllers/test-runner'); const MockTestRunner = TestRunner as unknown as Mock; const mockTestRunner = { startTest: vi.fn().mockRejectedValue(new Error('Test startup failed')), stopTest: vi.fn().mockResolvedValue(null), isTestRunning: vi.fn().mockReturnValue(false), on: vi.fn(), once: vi.fn() }; MockTestRunner.mockImplementation(() => mockTestRunner); await expect(app.start()).rejects.toThrow('Test startup failed'); }); }); describe('Prometheus Integration', () => { it('should initialize Prometheus exporter when enabled', async () => { const configWithPrometheus = { ...mockConfig, prometheus: { enabled: true, remoteWriteUrl: 'https://prometheus.example.com/api/v1/write', batchSize: 100, flushInterval: 10, timeout: 5000, retryAttempts: 3, retryDelay: 1000 } }; app = new LoadTesterApp(configWithPrometheus); const { PrometheusExporter } = await import('./exporters/prometheus-exporter'); const MockPrometheusExporter = PrometheusExporter as unknown as Mock; const mockPrometheusExporter = { exportTestSummary: vi.fn(), shutdown: vi.fn().mockResolvedValue(undefined) }; MockPrometheusExporter.mockImplementation(() => mockPrometheusExporter); const { TestRunner } = await import('./controllers/test-runner'); const MockTestRunner = TestRunner as unknown as Mock; const mockTestRunner = { startTest: vi.fn().mockResolvedValue(undefined), stopTest: vi.fn().mockResolvedValue({ summary: { totalRequests: 10, successfulRequests: 10, failedRequests: 0, averageResponseTime: 100, peakConcurrentUsers: 1, testDuration: 2 }, browserMetrics: [], drmMetrics: [], networkMetrics: [], errors: [] }), isTestRunning: vi.fn().mockReturnValue(true), getTestId: vi.fn().mockReturnValue('test-prometheus'), getMonitoringData: vi.fn().mockReturnValue({ activeSessions: 1, completedSessions: 0, failedSessions: 0, totalRequests: 10, successfulRequests: 10, failedRequests: 0, averageResponseTime: 100, currentRps: 2.0, elapsedTime: 2, remainingTime: 8, memoryUsage: 64, cpuUsage: 20 }), on: vi.fn(), once: vi.fn() }; MockTestRunner.mockImplementation(() => mockTestRunner); // Start the application const startPromise = app.start(); // Simulate monitoring update and test completion setTimeout(() => { // First trigger a monitoring update to test Prometheus export const monitoringCallback = mockTestRunner.on.mock.calls.find( call => call[0] === 'monitoring-update' )?.[1]; if (monitoringCallback) { monitoringCallback({ testId: 'test-prometheus', data: { activeSessions: 1, completedSessions: 0, failedSessions: 0, totalRequests: 10, successfulRequests: 10, failedRequests: 0, averageResponseTime: 100, currentRps: 2.0, elapsedTime: 2, remainingTime: 8, memoryUsage: 64, cpuUsage: 20 } }); } // Then complete the test const completionCallback = mockTestRunner.once.mock.calls.find( call => call[0] === 'test-completed' )?.[1]; if (completionCallback) { completionCallback({ results: { summary: { totalRequests: 10, successfulRequests: 10, failedRequests: 0, averageResponseTime: 100, peakConcurrentUsers: 1, testDuration: 2 }, browserMetrics: [], drmMetrics: [], networkMetrics: [], errors: [] } }); } }, 10); await startPromise; expect(mockPrometheusExporter.exportTestSummary).toHaveBeenCalled(); }); }); describe('OpenTelemetry Integration', () => { it('should initialize OpenTelemetry exporter when enabled', async () => { const configWithOpenTelemetry = { ...mockConfig, opentelemetry: { enabled: true, endpoint: 'https://otel-collector.example.com:4318/v1/metrics', protocol: 'http/protobuf' as const, serviceName: 'test-load-tester', serviceVersion: '1.0.0', timeout: 5000, compression: 'gzip' as const, batchTimeout: 5000, maxExportBatchSize: 512, maxQueueSize: 2048, exportTimeout: 30000 } }; app = new LoadTesterApp(configWithOpenTelemetry); const { OpenTelemetryExporter } = await import('./exporters/opentelemetry-exporter'); const MockOpenTelemetryExporter = OpenTelemetryExporter as unknown as Mock; const mockOpenTelemetryExporter = { initialize: vi.fn().mockResolvedValue(undefined), exportTestSummary: vi.fn(), shutdown: vi.fn().mockResolvedValue(undefined) }; MockOpenTelemetryExporter.mockImplementation(() => mockOpenTelemetryExporter); const { TestRunner } = await import('./controllers/test-runner'); const MockTestRunner = TestRunner as unknown as Mock; const mockTestRunner = { startTest: vi.fn().mockResolvedValue(undefined), stopTest: vi.fn().mockResolvedValue({ summary: { totalRequests: 15, successfulRequests: 14, failedRequests: 1, averageResponseTime: 120, peakConcurrentUsers: 1, testDuration: 3 }, browserMetrics: [], drmMetrics: [], networkMetrics: [], errors: [] }), isTestRunning: vi.fn().mockReturnValue(true), getTestId: vi.fn().mockReturnValue('test-opentelemetry'), getMonitoringData: vi.fn().mockReturnValue({ activeSessions: 1, completedSessions: 0, failedSessions: 0, totalRequests: 15, successfulRequests: 14, failedRequests: 1, averageResponseTime: 120, currentRps: 3.0, elapsedTime: 3, remainingTime: 7, memoryUsage: 128, cpuUsage: 25 }), on: vi.fn(), once: vi.fn() }; MockTestRunner.mockImplementation(() => mockTestRunner); // Start the application const startPromise = app.start(); // Simulate monitoring update and test completion setTimeout(() => { // First trigger a monitoring update to test OpenTelemetry export const monitoringCallback = mockTestRunner.on.mock.calls.find( call => call[0] === 'monitoring-update' )?.[1]; if (monitoringCallback) { monitoringCallback({ testId: 'test-opentelemetry', data: { activeSessions: 1, completedSessions: 0, failedSessions: 0, totalRequests: 15, successfulRequests: 14, failedRequests: 1, averageResponseTime: 120, currentRps: 3.0, elapsedTime: 3, remainingTime: 7, memoryUsage: 128, cpuUsage: 25 } }); } // Then complete the test const completionCallback = mockTestRunner.once.mock.calls.find( call => call[0] === 'test-completed' )?.[1]; if (completionCallback) { completionCallback({ results: { summary: { totalRequests: 15, successfulRequests: 14, failedRequests: 1, averageResponseTime: 120, peakConcurrentUsers: 1, testDuration: 3 }, browserMetrics: [], drmMetrics: [], networkMetrics: [], errors: [] } }); } }, 10); await startPromise; expect(mockOpenTelemetryExporter.initialize).toHaveBeenCalled(); expect(mockOpenTelemetryExporter.exportTestSummary).toHaveBeenCalled(); }); it('should support both Prometheus and OpenTelemetry simultaneously', async () => { const configWithBoth = { ...mockConfig, prometheus: { enabled: true, remoteWriteUrl: 'https://prometheus.example.com/api/v1/write', batchSize: 100, flushInterval: 10, timeout: 5000, retryAttempts: 3, retryDelay: 1000 }, opentelemetry: { enabled: true, endpoint: 'https://otel-collector.example.com:4318/v1/metrics', protocol: 'grpc' as const, serviceName: 'dual-export-tester', serviceVersion: '1.0.0', timeout: 5000, compression: 'gzip' as const, batchTimeout: 5000, maxExportBatchSize: 512, maxQueueSize: 2048, exportTimeout: 30000 } }; app = new LoadTesterApp(configWithBoth); // Mock both exporters const { PrometheusExporter } = await import('./exporters/prometheus-exporter'); const MockPrometheusExporter = PrometheusExporter as unknown as Mock; const mockPrometheusExporter = { exportTestSummary: vi.fn(), shutdown: vi.fn().mockResolvedValue(undefined) }; MockPrometheusExporter.mockImplementation(() => mockPrometheusExporter); const { OpenTelemetryExporter } = await import('./exporters/opentelemetry-exporter'); const MockOpenTelemetryExporter = OpenTelemetryExporter as unknown as Mock; const mockOpenTelemetryExporter = { initialize: vi.fn().mockResolvedValue(undefined), exportTestSummary: vi.fn(), shutdown: vi.fn().mockResolvedValue(undefined) }; MockOpenTelemetryExporter.mockImplementation(() => mockOpenTelemetryExporter); const { TestRunner } = await import('./controllers/test-runner'); const MockTestRunner = TestRunner as unknown as Mock; const mockTestRunner = { startTest: vi.fn().mockResolvedValue(undefined), stopTest: vi.fn().mockResolvedValue({ summary: { totalRequests: 20, successfulRequests: 18, failedRequests: 2, averageResponseTime: 140, peakConcurrentUsers: 2, testDuration: 4 }, browserMetrics: [], drmMetrics: [], networkMetrics: [], errors: [] }), isTestRunning: vi.fn().mockReturnValue(true), getTestId: vi.fn().mockReturnValue('test-dual-export'), getMonitoringData: vi.fn().mockReturnValue({ activeSessions: 2, completedSessions: 0, failedSessions: 0, totalRequests: 20, successfulRequests: 18, failedRequests: 2, averageResponseTime: 140, currentRps: 4.0, elapsedTime: 4, remainingTime: 6, memoryUsage: 256, cpuUsage: 35 }), on: vi.fn(), once: vi.fn() }; MockTestRunner.mockImplementation(() => mockTestRunner); // Start the application const startPromise = app.start(); // Simulate monitoring update and test completion setTimeout(() => { // Trigger monitoring update to test both exporters const monitoringCallback = mockTestRunner.on.mock.calls.find( call => call[0] === 'monitoring-update' )?.[1]; if (monitoringCallback) { monitoringCallback({ testId: 'test-dual-export', data: { activeSessions: 2, completedSessions: 0, failedSessions: 0, totalRequests: 20, successfulRequests: 18, failedRequests: 2, averageResponseTime: 140, currentRps: 4.0, elapsedTime: 4, remainingTime: 6, memoryUsage: 256, cpuUsage: 35 } }); } // Complete the test const completionCallback = mockTestRunner.once.mock.calls.find( call => call[0] === 'test-completed' )?.[1]; if (completionCallback) { completionCallback({ results: { summary: { totalRequests: 20, successfulRequests: 18, failedRequests: 2, averageResponseTime: 140, peakConcurrentUsers: 2, testDuration: 4 }, browserMetrics: [], drmMetrics: [], networkMetrics: [], errors: [] } }); } }, 10); await startPromise; // Verify both exporters were initialized and used expect(mockOpenTelemetryExporter.initialize).toHaveBeenCalled(); expect(mockPrometheusExporter.exportTestSummary).toHaveBeenCalled(); expect(mockOpenTelemetryExporter.exportTestSummary).toHaveBeenCalled(); }); }); describe('Status Monitoring', () => { it('should provide accurate status information', async () => { app = new LoadTesterApp(mockConfig); // Initial status let status = app.getStatus(); expect(status.status).toBe('not_started'); const { TestRunner } = await import('./controllers/test-runner'); const MockTestRunner = TestRunner as unknown as Mock; const mockTestRunner = { startTest: vi.fn().mockResolvedValue(undefined), isTestRunning: vi.fn().mockReturnValue(true), getTestId: vi.fn().mockReturnValue('test-status'), getMonitoringData: vi.fn().mockReturnValue({ activeSessions: 2, completedSessions: 0, failedSessions: 0, totalRequests: 20, successfulRequests: 19, failedRequests: 1, averageResponseTime: 110, currentRps: 4.5, elapsedTime: 4, remainingTime: 6, memoryUsage: 128, cpuUsage: 35 }), on: vi.fn(), once: vi.fn() }; MockTestRunner.mockImplementation(() => mockTestRunner); // Start the application const startPromise = app.start(); // Check status after initialization setTimeout(() => { status = app.getStatus(); expect(status.status).toBe('running'); expect(status.testId).toBe('test-status'); expect(status.monitoring?.activeSessions).toBe(2); expect(status.monitoring?.totalRequests).toBe(20); }, 5); // Complete the test setTimeout(() => { mockTestRunner.isTestRunning.mockReturnValue(false); const completionCallback = mockTestRunner.once.mock.calls.find( call => call[0] === 'test-completed' )?.[1]; if (completionCallback) { completionCallback({ results: { summary: { totalRequests: 20, successfulRequests: 19, failedRequests: 1, averageResponseTime: 110, peakConcurrentUsers: 2, testDuration: 4 }, browserMetrics: [], drmMetrics: [], networkMetrics: [], errors: [] } }); } }, 10); await startPromise; // Final status status = app.getStatus(); expect(status.status).toBe('completed'); }); }); });