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

a45ec30cf0e9bc7468b6351719330ed15bf9e2ce

Tony Gies <tgies@tgies.net>

Signed
1 files changed, +268 -0Ignore whitespace
tests/util-pure.test.js+268 -0
@@ -23,6 +23,14 @@ import {
23 isPathUnderParent,23 isPathUnderParent,
24 isFileURL,24 isFileURL,
25 getRequestURL,25 getRequestURL,
26 delay,
27 formatBytes,
28 sanitizeSafeCharacterReplacements,
29 generateTimestamp,
30 mergeObjectWithYaml,
31 excludeKeysByYaml,
32 Cache,
33 MemoryLimitedMap,
26} from '../src/util';34} from '../src/util';
2735
28describe('keyToEnv', () => {36describe('keyToEnv', () => {
@@ -419,3 +427,263 @@ describe('getRequestURL', () => {
419 expect(() => getRequestURL(42)).toThrow(TypeError);427 expect(() => getRequestURL(42)).toThrow(TypeError);
420 });428 });
421});429});
430
431describe('delay', () => {
432 test('should resolve after the specified time', async () => {
433 jest.useFakeTimers();
434 let resolved = false;
435 delay(50).then(() => { resolved = true; });
436 expect(resolved).toBe(false);
437 jest.advanceTimersByTime(50);
438 await Promise.resolve();
439 expect(resolved).toBe(true);
440 jest.useRealTimers();
441 });
442
443 test('should return a promise', () => {
444 jest.useFakeTimers();
445 const result = delay(0);
446 expect(result).toBeInstanceOf(Promise);
447 jest.useRealTimers();
448 });
449});
450
451describe('formatBytes', () => {
452 test('should format bytes to human-readable string', () => {
453 expect(formatBytes(0)).toBe('0B');
454 expect(formatBytes(1024)).toBe('1KB');
455 expect(formatBytes(1048576)).toBe('1MB');
456 });
457
458 test('should return empty string for null/undefined', () => {
459 expect(formatBytes(null)).toBe('');
460 expect(formatBytes(undefined)).toBe('');
461 });
462});
463
464describe('sanitizeSafeCharacterReplacements', () => {
465 test('should always return underscore', () => {
466 expect(sanitizeSafeCharacterReplacements('/')).toBe('_');
467 expect(sanitizeSafeCharacterReplacements('\\')).toBe('_');
468 expect(sanitizeSafeCharacterReplacements(':')).toBe('_');
469 expect(sanitizeSafeCharacterReplacements('')).toBe('_');
470 });
471});
472
473describe('generateTimestamp', () => {
474 test('should return YYYYMMDD-HHMMSS format for a known date', () => {
475 jest.useFakeTimers();
476 jest.setSystemTime(new Date('2025-07-15T09:30:45'));
477 expect(generateTimestamp()).toBe('20250715-093045');
478 jest.useRealTimers();
479 });
480});
481
482describe('mergeObjectWithYaml', () => {
483 test('should merge a YAML object into the target', () => {
484 const obj = { a: 1 };
485 mergeObjectWithYaml(obj, 'b: 2\nc: 3');
486 expect(obj).toEqual({ a: 1, b: 2, c: 3 });
487 });
488
489 test('should merge a YAML array of objects into the target', () => {
490 const obj = { a: 1 };
491 mergeObjectWithYaml(obj, '- b: 2\n- c: 3');
492 expect(obj).toEqual({ a: 1, b: 2, c: 3 });
493 });
494
495 test('should override existing keys', () => {
496 const obj = { a: 1 };
497 mergeObjectWithYaml(obj, 'a: 99');
498 expect(obj.a).toBe(99);
499 });
500
501 test('should do nothing for empty/falsy yamlString', () => {
502 const obj = { a: 1 };
503 mergeObjectWithYaml(obj, '');
504 mergeObjectWithYaml(obj, null);
505 mergeObjectWithYaml(obj, undefined);
506 expect(obj).toEqual({ a: 1 });
507 });
508
509 test('should not throw on invalid YAML', () => {
510 const obj = { a: 1 };
511 expect(() => mergeObjectWithYaml(obj, '{{{')).not.toThrow();
512 expect(obj).toEqual({ a: 1 });
513 });
514
515 test('should skip non-object items in YAML array', () => {
516 const obj = { a: 1 };
517 mergeObjectWithYaml(obj, '- hello\n- b: 2');
518 expect(obj).toEqual({ a: 1, b: 2 });
519 });
520});
521
522describe('excludeKeysByYaml', () => {
523 test('should delete keys listed in a YAML array', () => {
524 const obj = { a: 1, b: 2, c: 3 };
525 excludeKeysByYaml(obj, '- a\n- c');
526 expect(obj).toEqual({ b: 2 });
527 });
528
529 test('should delete keys from a YAML object', () => {
530 const obj = { a: 1, b: 2 };
531 excludeKeysByYaml(obj, 'a: whatever\nb: whatever');
532 expect(obj).toEqual({});
533 });
534
535 test('should delete a single string key', () => {
536 const obj = { a: 1, b: 2 };
537 excludeKeysByYaml(obj, 'a');
538 expect(obj).toEqual({ b: 2 });
539 });
540
541 test('should do nothing for empty/falsy yamlString', () => {
542 const obj = { a: 1 };
543 excludeKeysByYaml(obj, '');
544 excludeKeysByYaml(obj, null);
545 expect(obj).toEqual({ a: 1 });
546 });
547
548 test('should not throw on invalid YAML', () => {
549 const obj = { a: 1 };
550 expect(() => excludeKeysByYaml(obj, '{{{')).not.toThrow();
551 expect(obj).toEqual({ a: 1 });
552 });
553});
554
555describe('Cache', () => {
556 test('should store and retrieve values', () => {
557 const cache = new Cache(1000);
558 cache.set('key', 'value');
559 expect(cache.get('key')).toBe('value');
560 });
561
562 test('should return null for missing keys', () => {
563 const cache = new Cache(1000);
564 expect(cache.get('missing')).toBeNull();
565 });
566
567 test('should return null for expired entries', () => {
568 jest.useFakeTimers();
569 const cache = new Cache(10);
570 cache.set('key', 'value');
571 jest.advanceTimersByTime(20);
572 expect(cache.get('key')).toBeNull();
573 jest.useRealTimers();
574 });
575
576 test('should remove entries', () => {
577 const cache = new Cache(1000);
578 cache.set('key', 'value');
579 cache.remove('key');
580 expect(cache.get('key')).toBeNull();
581 });
582
583 test('should clear all entries', () => {
584 const cache = new Cache(1000);
585 cache.set('a', 1);
586 cache.set('b', 2);
587 cache.clear();
588 expect(cache.get('a')).toBeNull();
589 expect(cache.get('b')).toBeNull();
590 });
591});
592
593describe('MemoryLimitedMap', () => {
594 test('should store and retrieve values', () => {
595 const map = new MemoryLimitedMap('1 MB');
596 map.set('key', 'value');
597 expect(map.get('key')).toBe('value');
598 expect(map.has('key')).toBe(true);
599 });
600
601 test('should reject non-string keys and values', () => {
602 const map = new MemoryLimitedMap('1 MB');
603 map.set(123, 'value');
604 map.set('key', 123);
605 expect(map.size()).toBe(0);
606 });
607
608 test('should evict oldest entries when memory limit is reached', () => {
609 // 20 bytes capacity = 10 chars (2 bytes per char)
610 const map = new MemoryLimitedMap('20B');
611 map.set('a', '12345'); // 10 bytes
612 map.set('b', '12345'); // 10 bytes, fills capacity
613 map.set('c', '12345'); // 10 bytes, should evict 'a'
614 expect(map.has('a')).toBe(false);
615 expect(map.has('b')).toBe(true);
616 expect(map.has('c')).toBe(true);
617 });
618
619 test('should reject values larger than max memory', () => {
620 const map = new MemoryLimitedMap('10B');
621 map.set('key', '123456'); // 12 bytes > 10 byte limit
622 expect(map.has('key')).toBe(false);
623 });
624
625 test('should do nothing when maxMemory is 0', () => {
626 const map = new MemoryLimitedMap('0B');
627 map.set('key', 'value');
628 expect(map.size()).toBe(0);
629 });
630
631 test('should track memory usage', () => {
632 const map = new MemoryLimitedMap('1 MB');
633 map.set('key', 'hello'); // 10 bytes
634 expect(map.totalMemory()).toBe(10);
635 });
636
637 test('should update memory when overwriting a key', () => {
638 const map = new MemoryLimitedMap('1 MB');
639 map.set('key', 'hi'); // 4 bytes
640 map.set('key', 'hello'); // 10 bytes
641 expect(map.totalMemory()).toBe(10);
642 expect(map.get('key')).toBe('hello');
643 });
644
645 test('should delete entries and free memory', () => {
646 const map = new MemoryLimitedMap('1 MB');
647 map.set('key', 'hello');
648 expect(map.delete('key')).toBe(true);
649 expect(map.totalMemory()).toBe(0);
650 expect(map.has('key')).toBe(false);
651 });
652
653 test('should return false when deleting non-existent key', () => {
654 const map = new MemoryLimitedMap('1 MB');
655 expect(map.delete('nope')).toBe(false);
656 });
657
658 test('should clear all entries and reset memory', () => {
659 const map = new MemoryLimitedMap('1 MB');
660 map.set('a', 'hello');
661 map.set('b', 'world');
662 map.clear();
663 expect(map.size()).toBe(0);
664 expect(map.totalMemory()).toBe(0);
665 });
666
667 test('should iterate with forEach', () => {
668 const map = new MemoryLimitedMap('1 MB');
669 map.set('a', '1');
670 map.set('b', '2');
671 const entries = [];
672 map.forEach((value, key) => entries.push([key, value]));
673 expect(entries).toEqual([['a', '1'], ['b', '2']]);
674 });
675
676 test('should expose keys and values iterators', () => {
677 const map = new MemoryLimitedMap('1 MB');
678 map.set('a', '1');
679 map.set('b', '2');
680 expect([...map.keys()]).toEqual(['a', 'b']);
681 expect([...map.values()]).toEqual(['1', '2']);
682 });
683
684 test('estimateStringSize should return 2 bytes per character', () => {
685 expect(MemoryLimitedMap.estimateStringSize('hello')).toBe(10);
686 expect(MemoryLimitedMap.estimateStringSize('')).toBe(0);
687 expect(MemoryLimitedMap.estimateStringSize(null)).toBe(0);
688 });
689});