@j03fr0st/pubg-ts
Version:
A comprehensive TypeScript wrapper for the PUBG API
487 lines (370 loc) • 13.8 kB
text/typescript
import { createCacheKey, MemoryCache } from '../../src/utils/cache';
describe('MemoryCache', () => {
let cache: MemoryCache;
beforeEach(() => {
cache = new MemoryCache();
});
afterEach(() => {
cache.clear();
});
describe('constructor', () => {
it('should use default options when none provided', () => {
const defaultCache = new MemoryCache();
const stats = defaultCache.getStats();
expect(stats.defaultTtl).toBe(5 * 60 * 1000); // 5 minutes
expect(stats.maxSize).toBe(1000);
});
it('should use custom options when provided', () => {
const customCache = new MemoryCache({
ttl: 10000,
maxSize: 100,
});
const stats = customCache.getStats();
expect(stats.defaultTtl).toBe(10000);
expect(stats.maxSize).toBe(100);
});
it('should handle partial options', () => {
const partialCache = new MemoryCache({ ttl: 30000 });
const stats = partialCache.getStats();
expect(stats.defaultTtl).toBe(30000);
expect(stats.maxSize).toBe(1000); // Default
});
});
describe('set and get operations', () => {
it('should store and retrieve data', () => {
const key = 'test-key';
const data = { name: 'test', value: 42 };
cache.set(key, data);
const retrieved = cache.get(key);
expect(retrieved).toEqual(data);
});
it('should return undefined for non-existent keys', () => {
const result = cache.get('non-existent');
expect(result).toBeUndefined();
});
it('should store different data types', () => {
cache.set('string', 'hello world');
cache.set('number', 123);
cache.set('boolean', true);
cache.set('object', { a: 1, b: 2 });
cache.set('array', [1, 2, 3]);
cache.set('null', null);
expect(cache.get('string')).toBe('hello world');
expect(cache.get('number')).toBe(123);
expect(cache.get('boolean')).toBe(true);
expect(cache.get('object')).toEqual({ a: 1, b: 2 });
expect(cache.get('array')).toEqual([1, 2, 3]);
expect(cache.get('null')).toBeNull();
});
it('should overwrite existing entries', () => {
const key = 'overwrite-test';
cache.set(key, 'first value');
expect(cache.get(key)).toBe('first value');
cache.set(key, 'second value');
expect(cache.get(key)).toBe('second value');
});
});
describe('TTL (Time To Live) functionality', () => {
beforeEach(() => {
// Use fake timers for TTL tests
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should respect custom TTL', () => {
const key = 'ttl-test';
const data = 'test data';
const customTtl = 1000; // 1 second
cache.set(key, data, customTtl);
expect(cache.get(key)).toBe(data);
// Fast-forward time by half the TTL
jest.advanceTimersByTime(500);
expect(cache.get(key)).toBe(data); // Should still be valid
// Fast-forward past TTL
jest.advanceTimersByTime(600);
expect(cache.get(key)).toBeUndefined(); // Should be expired
});
it('should use default TTL when not specified', () => {
const shortTtlCache = new MemoryCache({ ttl: 1000 });
const key = 'default-ttl-test';
const data = 'test data';
shortTtlCache.set(key, data); // Using default TTL
expect(shortTtlCache.get(key)).toBe(data);
// Fast-forward past default TTL
jest.advanceTimersByTime(1500);
expect(shortTtlCache.get(key)).toBeUndefined();
});
it('should handle different TTLs for different entries', () => {
const key1 = 'short-ttl';
const key2 = 'long-ttl';
cache.set(key1, 'data1', 500); // 0.5 seconds
cache.set(key2, 'data2', 2000); // 2 seconds
// After 1 second, first should be expired, second should remain
jest.advanceTimersByTime(1000);
expect(cache.get(key1)).toBeUndefined();
expect(cache.get(key2)).toBe('data2');
// After another 1.5 seconds, both should be expired
jest.advanceTimersByTime(1500);
expect(cache.get(key2)).toBeUndefined();
});
});
describe('has method', () => {
it('should return true for existing non-expired entries', () => {
const key = 'exists-test';
cache.set(key, 'data');
expect(cache.has(key)).toBe(true);
});
it('should return false for non-existent entries', () => {
expect(cache.has('non-existent')).toBe(false);
});
it('should return false for expired entries', () => {
jest.useFakeTimers();
const key = 'expired-test';
cache.set(key, 'data', 1000);
expect(cache.has(key)).toBe(true);
// Fast-forward past TTL
jest.advanceTimersByTime(1500);
expect(cache.has(key)).toBe(false);
jest.useRealTimers();
});
it('should remove expired entries when checking', () => {
jest.useFakeTimers();
const key = 'cleanup-on-has';
cache.set(key, 'data', 1000);
const statsBefore = cache.getStats();
expect(statsBefore.size).toBe(1);
// Fast-forward past TTL
jest.advanceTimersByTime(1500);
cache.has(key); // This should clean up the expired entry
const statsAfter = cache.getStats();
expect(statsAfter.size).toBe(0);
jest.useRealTimers();
});
});
describe('delete method', () => {
it('should delete existing entries', () => {
const key = 'delete-test';
cache.set(key, 'data');
expect(cache.has(key)).toBe(true);
const deleted = cache.delete(key);
expect(deleted).toBe(true);
expect(cache.has(key)).toBe(false);
expect(cache.get(key)).toBeUndefined();
});
it('should return false when deleting non-existent entries', () => {
const deleted = cache.delete('non-existent');
expect(deleted).toBe(false);
});
});
describe('clear method', () => {
it('should remove all entries', () => {
cache.set('key1', 'data1');
cache.set('key2', 'data2');
cache.set('key3', 'data3');
expect(cache.getStats().size).toBe(3);
cache.clear();
expect(cache.getStats().size).toBe(0);
expect(cache.get('key1')).toBeUndefined();
expect(cache.get('key2')).toBeUndefined();
expect(cache.get('key3')).toBeUndefined();
});
it('should work on empty cache', () => {
expect(cache.getStats().size).toBe(0);
cache.clear();
expect(cache.getStats().size).toBe(0);
});
});
describe('cleanup method', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should remove expired entries', () => {
// Add entries with different TTLs
cache.set('short1', 'data1', 1000);
cache.set('short2', 'data2', 1000);
cache.set('long1', 'data3', 5000);
cache.set('long2', 'data4', 5000);
expect(cache.getStats().size).toBe(4);
// Fast-forward past short TTL but before long TTL
jest.advanceTimersByTime(1500);
cache.cleanup();
expect(cache.getStats().size).toBe(2);
expect(cache.get('short1')).toBeUndefined();
expect(cache.get('short2')).toBeUndefined();
expect(cache.get('long1')).toBe('data3');
expect(cache.get('long2')).toBe('data4');
});
it('should handle cleanup with no expired entries', () => {
cache.set('key1', 'data1', 5000);
cache.set('key2', 'data2', 5000);
expect(cache.getStats().size).toBe(2);
// Fast-forward but not past TTL
jest.advanceTimersByTime(1000);
cache.cleanup();
expect(cache.getStats().size).toBe(2);
});
it('should handle cleanup on empty cache', () => {
expect(cache.getStats().size).toBe(0);
cache.cleanup();
expect(cache.getStats().size).toBe(0);
});
});
describe('memory management', () => {
it('should respect maxSize limit', () => {
const smallCache = new MemoryCache({ maxSize: 3 });
smallCache.set('key1', 'data1');
smallCache.set('key2', 'data2');
smallCache.set('key3', 'data3');
expect(smallCache.getStats().size).toBe(3);
// Adding 4th item should trigger cleanup/eviction
smallCache.set('key4', 'data4');
expect(smallCache.getStats().size).toBe(3);
// First entry should be evicted (FIFO)
expect(smallCache.get('key1')).toBeUndefined();
expect(smallCache.get('key4')).toBe('data4');
});
it('should cleanup expired entries before evicting', () => {
jest.useFakeTimers();
const smallCache = new MemoryCache({ maxSize: 2, ttl: 1000 });
// Add entries that will expire
smallCache.set('expired1', 'data1', 500);
smallCache.set('expired2', 'data2', 500);
// Fast-forward to expire them
jest.advanceTimersByTime(1000);
// Add new entry - should cleanup expired ones first
smallCache.set('new1', 'newdata1');
expect(smallCache.getStats().size).toBe(1);
expect(smallCache.get('expired1')).toBeUndefined();
expect(smallCache.get('expired2')).toBeUndefined();
expect(smallCache.get('new1')).toBe('newdata1');
jest.useRealTimers();
});
it("should handle eviction when cleanup doesn't free enough space", () => {
const smallCache = new MemoryCache({ maxSize: 2 });
// Fill cache to capacity
smallCache.set('key1', 'data1');
smallCache.set('key2', 'data2');
expect(smallCache.getStats().size).toBe(2);
// Add another entry - should evict oldest
smallCache.set('key3', 'data3');
expect(smallCache.getStats().size).toBe(2);
expect(smallCache.get('key1')).toBeUndefined(); // Evicted
expect(smallCache.get('key2')).toBe('data2');
expect(smallCache.get('key3')).toBe('data3');
});
});
describe('getStats method', () => {
it('should return correct statistics', () => {
const customCache = new MemoryCache({
ttl: 10000,
maxSize: 500,
});
const initialStats = customCache.getStats();
expect(initialStats).toEqual({
size: 0,
maxSize: 500,
defaultTtl: 10000,
hits: 0,
misses: 0,
hitRate: 0,
});
customCache.set('key1', 'data1');
customCache.set('key2', 'data2');
const afterAddingStats = customCache.getStats();
expect(afterAddingStats.size).toBe(2);
expect(afterAddingStats.maxSize).toBe(500);
expect(afterAddingStats.defaultTtl).toBe(10000);
});
it('should update size when entries are added/removed', () => {
expect(cache.getStats().size).toBe(0);
cache.set('key1', 'data1');
expect(cache.getStats().size).toBe(1);
cache.set('key2', 'data2');
expect(cache.getStats().size).toBe(2);
cache.delete('key1');
expect(cache.getStats().size).toBe(1);
cache.clear();
expect(cache.getStats().size).toBe(0);
});
});
describe('edge cases', () => {
it('should handle undefined and null values', () => {
cache.set('undefined-key', undefined);
cache.set('null-key', null);
expect(cache.get('undefined-key')).toBeUndefined();
expect(cache.get('null-key')).toBeNull();
expect(cache.has('undefined-key')).toBe(true);
expect(cache.has('null-key')).toBe(true);
});
it('should handle empty strings as keys', () => {
cache.set('', 'empty key data');
expect(cache.get('')).toBe('empty key data');
expect(cache.has('')).toBe(true);
});
it('should handle very long keys', () => {
const longKey = 'a'.repeat(1000);
cache.set(longKey, 'long key data');
expect(cache.get(longKey)).toBe('long key data');
expect(cache.has(longKey)).toBe(true);
});
it('should handle zero TTL', () => {
jest.useFakeTimers();
cache.set('zero-ttl', 'data', 0);
// Should expire immediately
jest.advanceTimersByTime(1);
expect(cache.get('zero-ttl')).toBeUndefined();
jest.useRealTimers();
});
it('should handle negative TTL', () => {
jest.useFakeTimers();
cache.set('negative-ttl', 'data', -1000);
// Should be expired immediately
expect(cache.get('negative-ttl')).toBeUndefined();
jest.useRealTimers();
});
});
describe('concurrent operations simulation', () => {
it('should handle multiple rapid operations', () => {
// Simulate rapid concurrent-like operations
for (let i = 0; i < 100; i++) {
cache.set(`key${i}`, `data${i}`);
}
expect(cache.getStats().size).toBe(100);
// Check all entries exist
for (let i = 0; i < 100; i++) {
expect(cache.get(`key${i}`)).toBe(`data${i}`);
}
// Delete every other entry
for (let i = 0; i < 100; i += 2) {
cache.delete(`key${i}`);
}
expect(cache.getStats().size).toBe(50);
// Verify correct entries remain
for (let i = 1; i < 100; i += 2) {
expect(cache.get(`key${i}`)).toBe(`data${i}`);
}
});
});
});
describe('createCacheKey', () => {
it('should create cache keys with prefix and parts', () => {
const key = createCacheKey('http', 'GET', '/api/test', 'param1=value1');
expect(key).toBe('http:GET:/api/test:param1=value1');
});
it('should handle different data types', () => {
const key = createCacheKey('cache', 'string', 123, 'boolean');
expect(key).toBe('cache:string:123:boolean');
});
it('should handle empty parts array', () => {
const key = createCacheKey('prefix');
expect(key).toBe('prefix:');
});
it('should handle numbers and strings', () => {
const key = createCacheKey('test', 'part1', 42, 'part2');
expect(key).toBe('test:part1:42:part2');
});
});