| 419 | 427 | expect(() => getRequestURL(42)).toThrow(TypeError); |
| 420 | 428 | }); |
| 421 | 429 | }); |
| 430 | + |
| 431 | +describe('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 | + |
| 451 | +describe('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 | + |
| 464 | +describe('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 | + |
| 473 | +describe('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 | + |
| 482 | +describe('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 | + |
| 522 | +describe('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 | + |
| 555 | +describe('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 | + |
| 593 | +describe('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 | +}); |