import { beforeEach, describe, expect, it } from 'vitest'; import { Config } from './config'; describe('Config', () => { let config: Config; beforeEach(() => { config = new Config(); }); describe('constructor', () => { it('should create empty config by default', () => { const emptyConfig = new Config(); expect(emptyConfig.getAll()).toEqual({}); }); it('should accept initial configuration', () => { const initial = { api: { timeout: 5000 }, debug: true }; const configWithInitial = new Config(initial); expect(configWithInitial.getAll()).toEqual(initial); }); it('should not mutate initial config object', () => { const initial = { api: { timeout: 5000 } }; const configWithInitial = new Config(initial); configWithInitial.set('api.timeout', 10000); expect(initial.api.timeout).toBe(5000); // Original unchanged }); }); describe('get()', () => { beforeEach(() => { config = new Config({ api: { timeout: 5000, retries: 3, headers: { authorization: 'Bearer token', }, }, debug: true, }); }); it('should get top-level values', () => { expect(config.get('debug')).toBe(true); }); it('should get nested values with dot-notation', () => { expect(config.get('api.timeout')).toBe(5000); expect(config.get('api.retries')).toBe(3); }); it('should get deeply nested values', () => { expect(config.get('api.headers.authorization')).toBe('Bearer token'); }); it('should return undefined for non-existent paths', () => { expect(config.get('nonexistent')).toBeUndefined(); expect(config.get('api.nonexistent')).toBeUndefined(); expect(config.get('api.headers.nonexistent')).toBeUndefined(); }); it('should return undefined when traversing through null', () => { config.set('nullable', null); expect(config.get('nullable.nested')).toBeUndefined(); }); it('should support type parameter', () => { const timeout = config.get('api.timeout'); expect(typeof timeout).toBe('number'); const debug = config.get('debug'); expect(typeof debug).toBe('boolean'); }); it('should handle numeric string keys', () => { config.set('array.0', 'first'); expect(config.get('array.0')).toBe('first'); }); }); describe('set()', () => { it('should set top-level values', () => { config.set('debug', true); expect(config.get('debug')).toBe(true); }); it('should set nested values', () => { config.set('api.timeout', 5000); expect(config.get('api.timeout')).toBe(5000); }); it('should set deeply nested values', () => { config.set('api.headers.authorization', 'Bearer token'); expect(config.get('api.headers.authorization')).toBe('Bearer token'); }); it('should create intermediate objects', () => { config.set('deeply.nested.value', 42); expect(config.get('deeply')).toEqual({ nested: { value: 42 }, }); }); it('should overwrite existing values', () => { config.set('api.timeout', 5000); expect(config.get('api.timeout')).toBe(5000); config.set('api.timeout', 10000); expect(config.get('api.timeout')).toBe(10000); }); it('should handle setting null', () => { config.set('nullable', null); expect(config.get('nullable')).toBeNull(); }); it('should handle setting undefined', () => { config.set('undefinable', undefined); expect(config.get('undefinable')).toBeUndefined(); }); it('should handle various value types', () => { config.set('string', 'value'); config.set('number', 42); config.set('boolean', true); config.set('array', [1, 2, 3]); config.set('object', { key: 'value' }); expect(config.get('string')).toBe('value'); expect(config.get('number')).toBe(42); expect(config.get('boolean')).toBe(true); expect(config.get('array')).toEqual([1, 2, 3]); expect(config.get('object')).toEqual({ key: 'value' }); }); it('should replace non-object intermediate values', () => { config.set('path', 'string value'); config.set('path.nested', 'new value'); expect(config.get('path.nested')).toBe('new value'); expect(config.get('path')).toEqual({ nested: 'new value' }); }); }); describe('defaults()', () => { it('should set defaults for empty config', () => { config.defaults({ api: { timeout: 3000 }, debug: false, }); expect(config.get('api.timeout')).toBe(3000); expect(config.get('debug')).toBe(false); }); it('should not overwrite existing values (underwrite pattern)', () => { config = new Config({ api: { timeout: 10000 }, }); config.defaults({ api: { timeout: 3000, retries: 3 }, debug: false, }); expect(config.get('api.timeout')).toBe(10000); // User value wins expect(config.get('api.retries')).toBe(3); // Default used expect(config.get('debug')).toBe(false); // Default used }); it('should deep merge nested objects', () => { config = new Config({ analytics: { tracking: { enabled: false, }, }, }); config.defaults({ analytics: { tracking: { enabled: true, sampleRate: 1.0, }, reporting: { enabled: true, }, }, }); expect(config.get('analytics.tracking.enabled')).toBe(false); // User wins expect(config.get('analytics.tracking.sampleRate')).toBe(1.0); // Default expect(config.get('analytics.reporting.enabled')).toBe(true); // Default }); it('should be callable multiple times', () => { config.defaults({ a: 1 }); config.defaults({ b: 2 }); config.defaults({ c: 3 }); expect(config.getAll()).toEqual({ a: 1, b: 2, c: 3 }); }); it('should work with layers of defaults', () => { // Simulate plugin defaults being applied in order config = new Config({ api: { timeout: 10000 } }); config.defaults({ api: { timeout: 5000, retries: 3 } }); config.defaults({ api: { timeout: 3000, retries: 1, baseUrl: 'https://api' } }); expect(config.get('api.timeout')).toBe(10000); // User value expect(config.get('api.retries')).toBe(3); // First default expect(config.get('api.baseUrl')).toBe('https://api'); // Second default }); it('should preserve user-set null values', () => { config = new Config({ disabled: null }); config.defaults({ disabled: true }); expect(config.get('disabled')).toBeNull(); // User's null wins }); it('should use defaults for undefined values', () => { config = new Config({ enabled: undefined }); config.defaults({ enabled: true }); expect(config.get('enabled')).toBe(true); // Default used }); }); describe('getAll()', () => { beforeEach(() => { config = new Config({ api: { timeout: 5000 }, debug: true, }); }); it('should return all configuration', () => { expect(config.getAll()).toEqual({ api: { timeout: 5000 }, debug: true, }); }); it('should return a copy (immutable)', () => { const all = config.getAll(); all.newKey = 'new value'; all.api.timeout = 999; expect(config.get('newKey')).toBeUndefined(); expect(config.get('api.timeout')).toBe(5000); }); it('should return empty object for empty config', () => { const emptyConfig = new Config(); expect(emptyConfig.getAll()).toEqual({}); }); it('should reflect latest state after modifications', () => { config.set('new.value', 42); const all = config.getAll(); expect(all).toEqual({ api: { timeout: 5000 }, debug: true, new: { value: 42 }, }); }); }); describe('markRequired() and isRequired()', () => { it('should mark namespaces as required', () => { config.markRequired('analytics'); expect(config.isRequired('analytics')).toBe(true); }); it('should handle multiple required namespaces', () => { config.markRequired('analytics'); config.markRequired('storage'); config.markRequired('tracking'); expect(config.isRequired('analytics')).toBe(true); expect(config.isRequired('storage')).toBe(true); expect(config.isRequired('tracking')).toBe(true); }); it('should return false for non-required namespaces', () => { config.markRequired('analytics'); expect(config.isRequired('storage')).toBe(false); expect(config.isRequired('other')).toBe(false); }); it('should handle calling markRequired without namespace', () => { expect(() => { config.markRequired(); }).not.toThrow(); expect(config.isRequired(undefined as any)).toBe(false); }); it('should be case-sensitive', () => { config.markRequired('Analytics'); expect(config.isRequired('Analytics')).toBe(true); expect(config.isRequired('analytics')).toBe(false); }); it('should handle empty string namespace', () => { config.markRequired(''); expect(config.isRequired('')).toBe(true); }); }); describe('integration scenarios', () => { it('should handle complex plugin configuration scenario', () => { // Initial user config config = new Config({ analytics: { tracking: { enabled: false, // User explicitly disabled }, }, api: { timeout: 10000, // User wants longer timeout }, }); // Plugin 1 sets defaults config.defaults({ analytics: { tracking: { enabled: true, sampleRate: 1.0, endpoint: '/api/track', }, reporting: { enabled: true, }, }, }); // Plugin 2 sets defaults config.defaults({ api: { timeout: 5000, retries: 3, baseUrl: 'https://api.example.com', }, storage: { type: 'localStorage', }, }); // Verify final config expect(config.get('analytics.tracking.enabled')).toBe(false); // User expect(config.get('analytics.tracking.sampleRate')).toBe(1.0); // Default expect(config.get('analytics.tracking.endpoint')).toBe('/api/track'); // Default expect(config.get('analytics.reporting.enabled')).toBe(true); // Default expect(config.get('api.timeout')).toBe(10000); // User expect(config.get('api.retries')).toBe(3); // Default expect(config.get('api.baseUrl')).toBe('https://api.example.com'); // Default expect(config.get('storage.type')).toBe('localStorage'); // Default }); it('should support runtime config updates', () => { config = new Config({ api: { timeout: 5000 } }); // Plugin sets defaults config.defaults({ api: { retries: 3 } }); // Runtime update config.set('api.timeout', 10000); config.set('api.maxRetries', 5); expect(config.get('api.timeout')).toBe(10000); expect(config.get('api.retries')).toBe(3); expect(config.get('api.maxRetries')).toBe(5); }); it('should handle config validation pattern', () => { config = new Config({ api: { key: 'abc123' } }); // Mark API key as required config.markRequired('api'); expect(config.isRequired('api')).toBe(true); expect(config.get('api.key')).toBe('abc123'); }); }); describe('edge cases', () => { it('should handle single character paths', () => { config.set('a', 1); expect(config.get('a')).toBe(1); }); it('should handle paths with spaces in keys', () => { config.set('my key.nested', 'value'); expect(config.get('my key.nested')).toBe('value'); }); it('should handle empty path segments', () => { config.set('a..b', 'value'); const all = config.getAll(); expect(all.a['']).toEqual({ b: 'value' }); }); it('should handle very deep nesting', () => { config.set('a.b.c.d.e.f.g.h.i.j', 'deep'); expect(config.get('a.b.c.d.e.f.g.h.i.j')).toBe('deep'); }); it('should handle numeric values including 0', () => { config.set('count', 0); expect(config.get('count')).toBe(0); }); it('should handle boolean false', () => { config.set('enabled', false); expect(config.get('enabled')).toBe(false); }); it('should handle empty string values', () => { config.set('name', ''); expect(config.get('name')).toBe(''); }); }); });