lightweight-browser-load-tester
Version:
A lightweight load testing tool using real browsers for streaming applications with DRM support
501 lines (416 loc) • 14.8 kB
text/typescript
/**
* Tests for OpenTelemetry exporter
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { OpenTelemetryExporter } from './opentelemetry-exporter';
import { OpenTelemetryConfiguration, BrowserMetrics, DRMMetrics, NetworkMetrics, ErrorLog, TestSummary } from '../types';
// Mock OpenTelemetry modules
vi.mock('@opentelemetry/sdk-node');
vi.mock('@opentelemetry/exporter-otlp-http');
vi.mock('@opentelemetry/exporter-otlp-grpc');
vi.mock('@opentelemetry/resources');
vi.mock('@opentelemetry/semantic-conventions');
vi.mock('@opentelemetry/api');
vi.mock('@opentelemetry/sdk-metrics');
describe('OpenTelemetryExporter', () => {
let exporter: OpenTelemetryExporter;
let mockConfig: OpenTelemetryConfiguration;
// Mock objects
let _mockMeterProvider: any;
let _mockMeter: any;
let _mockMetricInstruments: any;
beforeEach(() => {
// Reset all mocks
vi.clearAllMocks();
// Setup mock configuration
mockConfig = {
enabled: true,
endpoint: 'http://localhost:4318/v1/metrics',
protocol: 'http/protobuf',
serviceName: 'test-service',
serviceVersion: '1.0.0',
timeout: 5000,
compression: 'gzip',
batchTimeout: 5000,
maxExportBatchSize: 512,
maxQueueSize: 2048,
exportTimeout: 30000
};
// Mock fetch for HTTP requests
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
statusText: 'OK'
});
exporter = new OpenTelemetryExporter(mockConfig);
});
afterEach(async () => {
if (exporter) {
await exporter.shutdown();
}
});
describe('Constructor', () => {
it('should create exporter with default configuration', () => {
const minimalConfig: OpenTelemetryConfiguration = {
enabled: true,
endpoint: 'http://localhost:4318/v1/metrics',
protocol: 'http/protobuf'
};
const exporter = new OpenTelemetryExporter(minimalConfig);
const config = exporter.getConfig();
expect(config.serviceName).toBe('lightweight-browser-load-tester');
expect(config.serviceVersion).toBe('1.0.0');
expect(config.timeout).toBe(5000);
expect(config.compression).toBe('gzip');
expect(config.batchTimeout).toBe(5000);
expect(config.maxExportBatchSize).toBe(512);
expect(config.maxQueueSize).toBe(2048);
expect(config.exportTimeout).toBe(30000);
});
it('should override default configuration with provided values', () => {
const config = exporter.getConfig();
expect(config.serviceName).toBe('test-service');
expect(config.serviceVersion).toBe('1.0.0');
expect(config.endpoint).toBe('http://localhost:4318/v1/metrics');
expect(config.protocol).toBe('http/protobuf');
});
});
describe('Initialization', () => {
it('should initialize successfully', async () => {
await exporter.initialize();
expect(exporter.isReady()).toBe(true);
});
it('should not initialize if disabled', async () => {
const disabledConfig = { ...mockConfig, enabled: false };
const disabledExporter = new OpenTelemetryExporter(disabledConfig);
await disabledExporter.initialize();
expect(disabledExporter.isReady()).toBe(false);
});
it('should not initialize twice', async () => {
await exporter.initialize();
await exporter.initialize(); // Second call should be ignored
expect(exporter.isReady()).toBe(true);
});
});
describe('Browser Metrics Export', () => {
beforeEach(async () => {
await exporter.initialize();
});
it('should export browser metrics successfully', async () => {
const browserMetrics: BrowserMetrics = {
instanceId: 'browser-1',
memoryUsage: 256,
cpuUsage: 45,
requestCount: 100,
errorCount: 2,
uptime: 300
};
// Mock fetch to verify the HTTP request
global.fetch = vi.fn().mockResolvedValue({
ok: true,
text: () => Promise.resolve('OK')
});
await exporter.exportBrowserMetrics(browserMetrics);
// Since our implementation uses a buffer, we need to flush to trigger the HTTP request
await exporter.flush();
expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:4318/v1/metrics',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json'
})
})
);
});
it('should not export if disabled', async () => {
const disabledConfig = { ...mockConfig, enabled: false };
const disabledExporter = new OpenTelemetryExporter(disabledConfig);
const browserMetrics: BrowserMetrics = {
instanceId: 'browser-1',
memoryUsage: 256,
cpuUsage: 45,
requestCount: 100,
errorCount: 2,
uptime: 300
};
global.fetch = vi.fn();
await disabledExporter.exportBrowserMetrics(browserMetrics);
await disabledExporter.flush();
expect(global.fetch).not.toHaveBeenCalled();
});
it('should not export if not initialized', async () => {
const uninitializedExporter = new OpenTelemetryExporter(mockConfig);
const browserMetrics: BrowserMetrics = {
instanceId: 'browser-1',
memoryUsage: 256,
cpuUsage: 45,
requestCount: 100,
errorCount: 2,
uptime: 300
};
global.fetch = vi.fn();
await uninitializedExporter.exportBrowserMetrics(browserMetrics);
await uninitializedExporter.flush();
expect(global.fetch).not.toHaveBeenCalled();
});
});
describe('DRM Metrics Export', () => {
beforeEach(async () => {
await exporter.initialize();
});
it('should export DRM metrics successfully', async () => {
const drmMetrics: DRMMetrics = {
licenseRequestCount: 50,
averageLicenseTime: 150,
licenseSuccessRate: 95.5,
drmType: 'widevine',
errors: [
{
timestamp: new Date(),
errorCode: 'DRM_001',
errorMessage: 'License request failed',
licenseUrl: 'https://example.com/license',
drmType: 'widevine'
}
]
};
global.fetch = vi.fn().mockResolvedValue({
ok: true,
text: () => Promise.resolve('OK')
});
await exporter.exportDRMMetrics(drmMetrics);
await exporter.flush();
expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:4318/v1/metrics',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json'
})
})
);
});
});
describe('Network Metrics Export', () => {
beforeEach(async () => {
await exporter.initialize();
});
it('should export network metrics successfully', async () => {
await exporter.initialize();
const networkMetrics: NetworkMetrics[] = [
{
url: 'https://example.com/api',
method: 'GET',
responseTime: 200,
statusCode: 200,
timestamp: new Date(),
requestSize: 1024,
responseSize: 2048,
isStreamingRelated: true,
streamingType: 'manifest'
},
{
url: 'https://example.com/segment',
method: 'GET',
responseTime: 150,
statusCode: 200,
timestamp: new Date(),
requestSize: 512,
responseSize: 4096,
isStreamingRelated: true,
streamingType: 'segment'
}
];
await exporter.exportNetworkMetrics(networkMetrics);
await exporter.flush();
// Verify HTTP request was made to OTLP endpoint
expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:4318/v1/metrics',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json'
})
})
);
});
it('should handle empty network metrics array', async () => {
await exporter.exportNetworkMetrics([]);
await exporter.flush();
// Should not make HTTP request for empty metrics
expect(global.fetch).not.toHaveBeenCalled();
});
});
describe('Test Summary Export', () => {
beforeEach(async () => {
await exporter.initialize();
});
it('should export test summary metrics successfully', async () => {
const testSummary: TestSummary = {
totalRequests: 1000,
successfulRequests: 950,
failedRequests: 50,
averageResponseTime: 175,
peakConcurrentUsers: 10,
testDuration: 300
};
await exporter.exportTestSummary(testSummary);
await exporter.flush();
// Verify HTTP request was made to OTLP endpoint
expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:4318/v1/metrics',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json'
})
})
);
});
});
describe('Error Metrics Export', () => {
beforeEach(async () => {
await exporter.initialize();
});
it('should export error metrics successfully', async () => {
const errors: ErrorLog[] = [
{
timestamp: new Date(),
level: 'error',
message: 'Test error 1',
context: { test: true }
},
{
timestamp: new Date(),
level: 'error',
message: 'Test error 2',
context: { test: true }
},
{
timestamp: new Date(),
level: 'warning',
message: 'Test warning',
context: { test: true }
}
];
await exporter.exportErrorMetrics(errors);
await exporter.flush();
// Verify HTTP request was made to OTLP endpoint
expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:4318/v1/metrics',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json'
})
})
);
});
it('should handle empty errors array', async () => {
await exporter.exportErrorMetrics([]);
await exporter.flush();
// Should not make HTTP request for empty errors
expect(global.fetch).not.toHaveBeenCalled();
});
});
describe('Flush and Shutdown', () => {
beforeEach(async () => {
await exporter.initialize();
});
it('should flush metrics successfully', async () => {
// Add some metrics to the buffer first
const browserMetrics: BrowserMetrics = {
instanceId: 'browser-1',
memoryUsage: 256,
cpuUsage: 45,
requestCount: 100,
errorCount: 2,
uptime: 300
};
await exporter.exportBrowserMetrics(browserMetrics);
await exporter.flush();
// Verify HTTP request was made
expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:4318/v1/metrics',
expect.objectContaining({
method: 'POST'
})
);
});
it('should handle flush errors gracefully', async () => {
// Mock fetch to fail
global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
// Should not throw even if HTTP request fails
await expect(exporter.flush()).resolves.toBeUndefined();
});
it('should shutdown successfully', async () => {
await exporter.shutdown();
expect(exporter.isReady()).toBe(false);
});
it('should handle shutdown errors gracefully', async () => {
// Mock fetch to fail during flush
global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
// Should not throw even if flush fails during shutdown
await expect(exporter.shutdown()).resolves.toBeUndefined();
expect(exporter.isReady()).toBe(false);
});
it('should not flush or shutdown if not initialized', async () => {
const uninitializedExporter = new OpenTelemetryExporter(mockConfig);
global.fetch = vi.fn();
await uninitializedExporter.flush();
await uninitializedExporter.shutdown();
// Should not make HTTP requests if not initialized
expect(global.fetch).not.toHaveBeenCalled();
});
it('should not shutdown twice', async () => {
await exporter.shutdown();
expect(exporter.isReady()).toBe(false);
await exporter.shutdown(); // Second call should be ignored
expect(exporter.isReady()).toBe(false);
});
});
describe('Configuration', () => {
it('should return readonly configuration', () => {
const config = exporter.getConfig();
expect(config).toEqual(mockConfig);
// Verify it's readonly by attempting to modify
expect(() => {
(config as any).endpoint = 'modified';
}).not.toThrow(); // TypeScript would prevent this, but runtime allows it
// Original config should remain unchanged
expect(exporter.getConfig().endpoint).toBe('http://localhost:4318/v1/metrics');
});
it('should handle different compression settings', () => {
const noCompressionConfig = { ...mockConfig, compression: 'none' as const };
const noCompressionExporter = new OpenTelemetryExporter(noCompressionConfig);
expect(noCompressionExporter.getConfig().compression).toBe('none');
});
it('should handle different protocol settings', () => {
const jsonConfig = { ...mockConfig, protocol: 'http/json' as const };
const jsonExporter = new OpenTelemetryExporter(jsonConfig);
expect(jsonExporter.getConfig().protocol).toBe('http/json');
});
});
describe('Status Checking', () => {
it('should report not ready before initialization', () => {
const newExporter = new OpenTelemetryExporter(mockConfig);
expect(newExporter.isReady()).toBe(false);
});
it('should report ready after initialization', async () => {
await exporter.initialize();
expect(exporter.isReady()).toBe(true);
});
it('should report not ready after shutdown', async () => {
await exporter.initialize();
expect(exporter.isReady()).toBe(true);
await exporter.shutdown();
expect(exporter.isReady()).toBe(false);
});
it('should report not ready if disabled', () => {
const disabledConfig = { ...mockConfig, enabled: false };
const disabledExporter = new OpenTelemetryExporter(disabledConfig);
expect(disabledExporter.isReady()).toBe(false);
});
});
});