Add tests for Cache, MemoryLimitedMap, and other util.js coverage gaps (#5365)

This commit is contained in:
Tony Gies
2026-04-05 13:05:08 -05:00
committed by GitHub
parent 8d8e3833f6
commit a45ec30cf0
+268
View File
@@ -23,6 +23,14 @@ import {
isPathUnderParent,
isFileURL,
getRequestURL,
delay,
formatBytes,
sanitizeSafeCharacterReplacements,
generateTimestamp,
mergeObjectWithYaml,
excludeKeysByYaml,
Cache,
MemoryLimitedMap,
} from '../src/util';
describe('keyToEnv', () => {
@@ -419,3 +427,263 @@ describe('getRequestURL', () => {
expect(() => getRequestURL(42)).toThrow(TypeError);
});
});
describe('delay', () => {
test('should resolve after the specified time', async () => {
jest.useFakeTimers();
let resolved = false;
delay(50).then(() => { resolved = true; });
expect(resolved).toBe(false);
jest.advanceTimersByTime(50);
await Promise.resolve();
expect(resolved).toBe(true);
jest.useRealTimers();
});
test('should return a promise', () => {
jest.useFakeTimers();
const result = delay(0);
expect(result).toBeInstanceOf(Promise);
jest.useRealTimers();
});
});
describe('formatBytes', () => {
test('should format bytes to human-readable string', () => {
expect(formatBytes(0)).toBe('0B');
expect(formatBytes(1024)).toBe('1KB');
expect(formatBytes(1048576)).toBe('1MB');
});
test('should return empty string for null/undefined', () => {
expect(formatBytes(null)).toBe('');
expect(formatBytes(undefined)).toBe('');
});
});
describe('sanitizeSafeCharacterReplacements', () => {
test('should always return underscore', () => {
expect(sanitizeSafeCharacterReplacements('/')).toBe('_');
expect(sanitizeSafeCharacterReplacements('\\')).toBe('_');
expect(sanitizeSafeCharacterReplacements(':')).toBe('_');
expect(sanitizeSafeCharacterReplacements('')).toBe('_');
});
});
describe('generateTimestamp', () => {
test('should return YYYYMMDD-HHMMSS format for a known date', () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-07-15T09:30:45'));
expect(generateTimestamp()).toBe('20250715-093045');
jest.useRealTimers();
});
});
describe('mergeObjectWithYaml', () => {
test('should merge a YAML object into the target', () => {
const obj = { a: 1 };
mergeObjectWithYaml(obj, 'b: 2\nc: 3');
expect(obj).toEqual({ a: 1, b: 2, c: 3 });
});
test('should merge a YAML array of objects into the target', () => {
const obj = { a: 1 };
mergeObjectWithYaml(obj, '- b: 2\n- c: 3');
expect(obj).toEqual({ a: 1, b: 2, c: 3 });
});
test('should override existing keys', () => {
const obj = { a: 1 };
mergeObjectWithYaml(obj, 'a: 99');
expect(obj.a).toBe(99);
});
test('should do nothing for empty/falsy yamlString', () => {
const obj = { a: 1 };
mergeObjectWithYaml(obj, '');
mergeObjectWithYaml(obj, null);
mergeObjectWithYaml(obj, undefined);
expect(obj).toEqual({ a: 1 });
});
test('should not throw on invalid YAML', () => {
const obj = { a: 1 };
expect(() => mergeObjectWithYaml(obj, '{{{')).not.toThrow();
expect(obj).toEqual({ a: 1 });
});
test('should skip non-object items in YAML array', () => {
const obj = { a: 1 };
mergeObjectWithYaml(obj, '- hello\n- b: 2');
expect(obj).toEqual({ a: 1, b: 2 });
});
});
describe('excludeKeysByYaml', () => {
test('should delete keys listed in a YAML array', () => {
const obj = { a: 1, b: 2, c: 3 };
excludeKeysByYaml(obj, '- a\n- c');
expect(obj).toEqual({ b: 2 });
});
test('should delete keys from a YAML object', () => {
const obj = { a: 1, b: 2 };
excludeKeysByYaml(obj, 'a: whatever\nb: whatever');
expect(obj).toEqual({});
});
test('should delete a single string key', () => {
const obj = { a: 1, b: 2 };
excludeKeysByYaml(obj, 'a');
expect(obj).toEqual({ b: 2 });
});
test('should do nothing for empty/falsy yamlString', () => {
const obj = { a: 1 };
excludeKeysByYaml(obj, '');
excludeKeysByYaml(obj, null);
expect(obj).toEqual({ a: 1 });
});
test('should not throw on invalid YAML', () => {
const obj = { a: 1 };
expect(() => excludeKeysByYaml(obj, '{{{')).not.toThrow();
expect(obj).toEqual({ a: 1 });
});
});
describe('Cache', () => {
test('should store and retrieve values', () => {
const cache = new Cache(1000);
cache.set('key', 'value');
expect(cache.get('key')).toBe('value');
});
test('should return null for missing keys', () => {
const cache = new Cache(1000);
expect(cache.get('missing')).toBeNull();
});
test('should return null for expired entries', () => {
jest.useFakeTimers();
const cache = new Cache(10);
cache.set('key', 'value');
jest.advanceTimersByTime(20);
expect(cache.get('key')).toBeNull();
jest.useRealTimers();
});
test('should remove entries', () => {
const cache = new Cache(1000);
cache.set('key', 'value');
cache.remove('key');
expect(cache.get('key')).toBeNull();
});
test('should clear all entries', () => {
const cache = new Cache(1000);
cache.set('a', 1);
cache.set('b', 2);
cache.clear();
expect(cache.get('a')).toBeNull();
expect(cache.get('b')).toBeNull();
});
});
describe('MemoryLimitedMap', () => {
test('should store and retrieve values', () => {
const map = new MemoryLimitedMap('1 MB');
map.set('key', 'value');
expect(map.get('key')).toBe('value');
expect(map.has('key')).toBe(true);
});
test('should reject non-string keys and values', () => {
const map = new MemoryLimitedMap('1 MB');
map.set(123, 'value');
map.set('key', 123);
expect(map.size()).toBe(0);
});
test('should evict oldest entries when memory limit is reached', () => {
// 20 bytes capacity = 10 chars (2 bytes per char)
const map = new MemoryLimitedMap('20B');
map.set('a', '12345'); // 10 bytes
map.set('b', '12345'); // 10 bytes, fills capacity
map.set('c', '12345'); // 10 bytes, should evict 'a'
expect(map.has('a')).toBe(false);
expect(map.has('b')).toBe(true);
expect(map.has('c')).toBe(true);
});
test('should reject values larger than max memory', () => {
const map = new MemoryLimitedMap('10B');
map.set('key', '123456'); // 12 bytes > 10 byte limit
expect(map.has('key')).toBe(false);
});
test('should do nothing when maxMemory is 0', () => {
const map = new MemoryLimitedMap('0B');
map.set('key', 'value');
expect(map.size()).toBe(0);
});
test('should track memory usage', () => {
const map = new MemoryLimitedMap('1 MB');
map.set('key', 'hello'); // 10 bytes
expect(map.totalMemory()).toBe(10);
});
test('should update memory when overwriting a key', () => {
const map = new MemoryLimitedMap('1 MB');
map.set('key', 'hi'); // 4 bytes
map.set('key', 'hello'); // 10 bytes
expect(map.totalMemory()).toBe(10);
expect(map.get('key')).toBe('hello');
});
test('should delete entries and free memory', () => {
const map = new MemoryLimitedMap('1 MB');
map.set('key', 'hello');
expect(map.delete('key')).toBe(true);
expect(map.totalMemory()).toBe(0);
expect(map.has('key')).toBe(false);
});
test('should return false when deleting non-existent key', () => {
const map = new MemoryLimitedMap('1 MB');
expect(map.delete('nope')).toBe(false);
});
test('should clear all entries and reset memory', () => {
const map = new MemoryLimitedMap('1 MB');
map.set('a', 'hello');
map.set('b', 'world');
map.clear();
expect(map.size()).toBe(0);
expect(map.totalMemory()).toBe(0);
});
test('should iterate with forEach', () => {
const map = new MemoryLimitedMap('1 MB');
map.set('a', '1');
map.set('b', '2');
const entries = [];
map.forEach((value, key) => entries.push([key, value]));
expect(entries).toEqual([['a', '1'], ['b', '2']]);
});
test('should expose keys and values iterators', () => {
const map = new MemoryLimitedMap('1 MB');
map.set('a', '1');
map.set('b', '2');
expect([...map.keys()]).toEqual(['a', 'b']);
expect([...map.values()]).toEqual(['1', '2']);
});
test('estimateStringSize should return 2 bytes per character', () => {
expect(MemoryLimitedMap.estimateStringSize('hello')).toBe(10);
expect(MemoryLimitedMap.estimateStringSize('')).toBe(0);
expect(MemoryLimitedMap.estimateStringSize(null)).toBe(0);
});
});