odesli.js
Version:
Node.js Client to query odesli.co (song.link/album.link) API
424 lines (349 loc) • 12.9 kB
JavaScript
const { RateLimiter } = require('../lib/rate-limiter.js');
const { MetricsCollector } = require('../lib/metrics.js');
const {
PluginSystem,
loggingPlugin,
analyticsPlugin,
} = require('../lib/plugin-system.js');
describe('Advanced Features', () => {
describe('RateLimiter', () => {
let rateLimiter;
beforeEach(() => {
rateLimiter = new RateLimiter({
maxRequests: 5,
windowMs: 1000, // 1 second for faster tests
strategy: 'token-bucket',
});
});
test('should initialize with correct settings', () => {
expect(rateLimiter.maxRequests).toBe(5);
expect(rateLimiter.windowMs).toBe(1000);
expect(rateLimiter.strategy).toBe('token-bucket');
});
test('should allow requests within limit', async () => {
const startTime = Date.now();
// Should allow 5 requests immediately
for (let i = 0; i < 5; i++) {
await rateLimiter.waitForSlot();
}
const endTime = Date.now();
expect(endTime - startTime).toBeLessThan(100); // Should be very fast
});
test('should throttle requests beyond limit', async () => {
// Use up all tokens
for (let i = 0; i < 5; i++) {
await rateLimiter.waitForSlot();
}
const startTime = Date.now();
await rateLimiter.waitForSlot(); // This should wait
const endTime = Date.now();
expect(endTime - startTime).toBeGreaterThan(800); // Should wait close to window time
});
test('should handle rate limit responses', async () => {
const startTime = Date.now();
await rateLimiter.handleRateLimitResponse(2); // 2 seconds
const endTime = Date.now();
expect(endTime - startTime).toBeGreaterThan(1900); // Should wait ~2 seconds
});
test('should provide status information', () => {
const status = rateLimiter.getStatus();
expect(status).toHaveProperty('available');
expect(status).toHaveProperty('max');
expect(status).toHaveProperty('refillRate');
});
test('should work with different strategies', async () => {
const slidingWindowLimiter = new RateLimiter({
maxRequests: 3,
windowMs: 1000,
strategy: 'sliding-window',
});
const startTime = Date.now();
for (let i = 0; i < 3; i++) {
await slidingWindowLimiter.waitForSlot();
}
const endTime = Date.now();
expect(endTime - startTime).toBeLessThan(100);
});
});
describe('MetricsCollector', () => {
let metrics;
beforeEach(() => {
metrics = new MetricsCollector({
enabled: true,
retentionMs: 1000, // 1 second for faster tests
maxDataPoints: 10,
});
});
test('should initialize with correct settings', () => {
expect(metrics.enabled).toBe(true);
expect(metrics.retentionMs).toBe(1000);
expect(metrics.maxDataPoints).toBe(10);
});
test('should record successful requests', () => {
const startTime = Date.now();
metrics.recordRequest({
url: 'https://example.com',
startTime,
endTime: startTime + 100,
success: true,
statusCode: 200,
platform: 'spotify',
country: 'US',
});
const summary = metrics.getSummary();
expect(summary.counters.totalRequests).toBe(1);
expect(summary.counters.successfulRequests).toBe(1);
expect(summary.counters.failedRequests).toBe(0);
expect(summary.rates.successRate).toBe(1);
});
test('should record failed requests', () => {
const startTime = Date.now();
metrics.recordRequest({
url: 'https://example.com',
startTime,
endTime: startTime + 100,
success: false,
error: new Error('Network error'),
platform: 'spotify',
country: 'US',
});
const summary = metrics.getSummary();
expect(summary.counters.totalRequests).toBe(1);
expect(summary.counters.successfulRequests).toBe(0);
expect(summary.counters.failedRequests).toBe(1);
expect(summary.rates.successRate).toBe(0);
});
test('should record cache hits and misses', () => {
metrics.recordRequest({ cacheHit: true });
metrics.recordRequest({ cacheHit: false });
metrics.recordRequest({ cacheHit: true });
const summary = metrics.getSummary();
expect(summary.counters.cacheHits).toBe(2);
expect(summary.counters.cacheMisses).toBe(1);
expect(summary.rates.cacheHitRate).toBeCloseTo(0.67, 2);
});
test('should record rate limit hits', () => {
metrics.recordRateLimit(1000);
metrics.recordRateLimit(2000);
const summary = metrics.getSummary();
expect(summary.counters.rateLimitHits).toBe(2);
expect(summary.rateLimits.hits).toBe(2);
});
test('should record errors', () => {
const error = new Error('Test error');
metrics.recordError(error, { url: 'https://example.com' });
const detailed = metrics.getDetailedMetrics();
expect(detailed).toBeDefined();
expect(typeof detailed).toBe('object');
});
test('should group metrics by platform', () => {
metrics.recordRequest({
url: 'https://spotify.com/track/123',
platform: 'spotify',
success: true,
});
metrics.recordRequest({
url: 'https://youtube.com/watch?v=abc',
platform: 'youtube',
success: true,
});
const platformMetrics = metrics.getDetailedMetrics({
groupBy: 'platform',
});
expect(platformMetrics.spotify.requests).toBe(1);
expect(platformMetrics.youtube.requests).toBe(1);
});
test('should clean up old data', () => {
// Add some old data
metrics.metrics.requests.push({
timestamp: Date.now() - 2000, // 2 seconds ago
url: 'old-request',
});
metrics.cleanup();
expect(metrics.metrics.requests).toHaveLength(0);
});
test('should export metrics', () => {
metrics.recordRequest({ success: true });
const exported = metrics.export();
expect(exported).toHaveProperty('summary');
expect(exported).toHaveProperty('detailed');
expect(exported).toHaveProperty('raw');
});
test('should reset metrics', () => {
metrics.recordRequest({ success: true });
expect(metrics.counters.totalRequests).toBe(1);
metrics.reset();
expect(metrics.counters.totalRequests).toBe(0);
});
});
describe('PluginSystem', () => {
let pluginSystem;
beforeEach(() => {
pluginSystem = new PluginSystem();
});
test('should initialize with built-in hooks', () => {
const hooks = Array.from(pluginSystem.hooks.keys());
expect(hooks).toContain('beforeRequest');
expect(hooks).toContain('afterRequest');
expect(hooks).toContain('onError');
expect(hooks).toContain('onRateLimit');
});
test('should register plugins', () => {
pluginSystem.registerPlugin('test', {
name: 'test',
version: '1.0.0',
description: 'Test plugin',
});
expect(pluginSystem.hasPlugin('test')).toBe(true);
expect(pluginSystem.getPlugins()).toContain('test');
});
test('should not register duplicate plugins', () => {
pluginSystem.registerPlugin('test', { name: 'test' });
expect(() => {
pluginSystem.registerPlugin('test', { name: 'test' });
}).toThrow('Plugin "test" is already registered');
});
test('should unregister plugins', () => {
pluginSystem.registerPlugin('test', { name: 'test' });
expect(pluginSystem.hasPlugin('test')).toBe(true);
pluginSystem.unregisterPlugin('test');
expect(pluginSystem.hasPlugin('test')).toBe(false);
});
test('should execute hooks', async () => {
let executed = false;
pluginSystem.registerHookHandler('beforeRequest', async context => {
executed = true;
expect(context.url).toBe('https://example.com');
});
await pluginSystem.executeHook('beforeRequest', {
url: 'https://example.com',
});
expect(executed).toBe(true);
});
test('should handle hook errors gracefully', async () => {
pluginSystem.registerHookHandler('beforeRequest', async () => {
throw new Error('Hook error');
});
let errorExecuted = false;
pluginSystem.registerHookHandler('onError', async context => {
errorExecuted = true;
expect(context.error.message).toBe('Hook error');
});
await pluginSystem.executeHook('beforeRequest', {});
expect(errorExecuted).toBe(true);
});
test('should execute middleware chain', async () => {
const middleware1 = async (context, next) => {
context.value = (context.value || 0) + 1;
return await next();
};
const middleware2 = async (context, next) => {
context.value = context.value * 2;
return await next();
};
pluginSystem.middleware.push({ name: 'test1', middleware: middleware1 });
pluginSystem.middleware.push({ name: 'test2', middleware: middleware2 });
const context = { value: 0 };
const result = await pluginSystem.executeMiddleware(context, async () => {
return context.value + 10;
});
expect(result).toBe(12); // (0 + 1) * 2 + 10
});
test('should transform data', async () => {
const transformer = async (data, _context) => {
data.transformed = true;
return data;
};
pluginSystem.transformers.push({
name: 'test',
transformers: { song: transformer },
});
const originalData = { title: 'Test Song' };
const transformed = await pluginSystem.transformData(
originalData,
'song'
);
expect(transformed.transformed).toBe(true);
expect(transformed.title).toBe('Test Song');
});
test('should provide plugin information', () => {
const plugin = {
name: 'test',
version: '1.0.0',
description: 'Test plugin',
hooks: { beforeRequest: () => {} },
middleware: async () => {},
transformers: { song: async () => {} },
};
pluginSystem.registerPlugin('test', plugin);
const info = pluginSystem.getPluginInfo('test');
expect(info.name).toBe('test');
expect(info.version).toBe('1.0.0');
expect(info.description).toBe('Test plugin');
expect(info.hooks).toContain('beforeRequest');
expect(info.hasMiddleware).toBe(true);
expect(info.hasTransformers).toBe(true);
});
test('should work with built-in plugins', () => {
pluginSystem.registerPlugin('logging', loggingPlugin);
pluginSystem.registerPlugin('analytics', analyticsPlugin);
expect(pluginSystem.hasPlugin('logging')).toBe(true);
expect(pluginSystem.hasPlugin('analytics')).toBe(true);
expect(pluginSystem.getPlugins()).toHaveLength(2);
});
});
describe('Integration Tests', () => {
test('should work together - rate limiting with metrics', async () => {
const rateLimiter = new RateLimiter({
maxRequests: 3,
windowMs: 100,
strategy: 'token-bucket',
});
const metrics = new MetricsCollector({ enabled: true });
for (let i = 0; i < 5; i++) {
await rateLimiter.waitForSlot();
const startTime = Date.now();
metrics.recordRequest({
url: `https://example.com/test${i}`,
startTime,
endTime: Date.now(),
success: true,
});
}
const summary = metrics.getSummary();
expect(summary.counters.totalRequests).toBe(5);
});
test('should work together - plugins with metrics', async () => {
const pluginSystem = new PluginSystem();
const metrics = new MetricsCollector({ enabled: true });
// Create a custom plugin that records metrics
const metricsPlugin = {
name: 'metrics-plugin',
hooks: {
beforeRequest: async context => {
context.startTime = Date.now();
},
afterRequest: async context => {
metrics.recordRequest({
url: context.url,
startTime: context.startTime,
endTime: Date.now(),
success: context.success || true,
});
},
},
};
pluginSystem.registerPlugin('metrics', metricsPlugin);
await pluginSystem.executeHook('beforeRequest', {
url: 'https://example.com',
});
await pluginSystem.executeHook('afterRequest', {
url: 'https://example.com',
success: true,
});
const summary = metrics.getSummary();
expect(summary.counters.totalRequests).toBe(1);
expect(summary.counters.successfulRequests).toBe(1);
});
});
});