import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { readFileSync } from 'fs'; import { loadConfig, validateConfig, Config } from './config'; // Mock fs module vi.mock('fs'); const mockedReadFileSync = vi.mocked(readFileSync); describe('Configuration Management', () => { beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { // Clear environment variables delete process.env.GITHUB_TOKEN; delete process.env.PORT; delete process.env.HOST; }); describe('loadConfig', () => { it('should load configuration from file', () => { const mockConfig = { github: { token: 'test-token', repositories: [ { owner: 'test-owner', name: 'test-repo' } ] }, server: { port: 3001, host: '0.0.0.0' }, cache: { timeoutSeconds: 300 } }; mockedReadFileSync.mockReturnValueOnce(JSON.stringify(mockConfig)); const result = loadConfig(); expect(result).toEqual(mockConfig); expect(mockedReadFileSync).toHaveBeenCalledWith( expect.stringContaining('config.json'), 'utf8' ); }); it('should use environment variables when config file is not found', () => { process.env.GITHUB_TOKEN = 'env-token'; process.env.PORT = '8080'; process.env.HOST = '127.0.0.1'; mockedReadFileSync.mockImplementationOnce(() => { throw new Error('ENOENT: no such file or directory'); }); const result = loadConfig(); expect(result).toEqual({ github: { token: 'env-token', repositories: [] }, server: { port: 8080, host: '127.0.0.1' }, cache: { timeoutSeconds: 300 } }); }); it('should merge file config with environment variables', () => { process.env.GITHUB_TOKEN = 'env-token'; process.env.PORT = '8080'; const mockConfig = { github: { repositories: [ { owner: 'test-owner', name: 'test-repo' } ] }, server: { host: '0.0.0.0' }, cache: { timeoutSeconds: 600 } }; mockedReadFileSync.mockReturnValueOnce(JSON.stringify(mockConfig)); const result = loadConfig(); expect(result).toEqual({ github: { token: 'env-token', // From environment repositories: [ { owner: 'test-owner', name: 'test-repo' } ] }, server: { port: 8080, // From environment host: '0.0.0.0' // From file }, cache: { timeoutSeconds: 600 // From file } }); }); it('should use default values when neither file nor env vars are provided', () => { mockedReadFileSync.mockImplementationOnce(() => { throw new Error('ENOENT: no such file or directory'); }); const result = loadConfig(); expect(result).toEqual({ github: { token: '', repositories: [] }, server: { port: 3001, host: '0.0.0.0' }, cache: { timeoutSeconds: 300 } }); }); it('should handle invalid JSON in config file', () => { mockedReadFileSync.mockReturnValueOnce('invalid json'); const result = loadConfig(); // Should fallback to defaults expect(result).toEqual({ github: { token: '', repositories: [] }, server: { port: 3001, host: '0.0.0.0' }, cache: { timeoutSeconds: 300 } }); }); }); describe('validateConfig', () => { it('should validate correct configuration', () => { const config: Config = { github: { token: 'test-token', repositories: [ { owner: 'test-owner', name: 'test-repo' } ] }, server: { port: 3001, host: '0.0.0.0' }, cache: { timeoutSeconds: 300 } }; expect(() => validateConfig(config)).not.toThrow(); }); it('should throw error when GitHub token is missing', () => { const config: Config = { github: { token: '', repositories: [ { owner: 'test-owner', name: 'test-repo' } ] }, server: { port: 3001, host: '0.0.0.0' }, cache: { timeoutSeconds: 300 } }; expect(() => validateConfig(config)).toThrow('GitHub token is required'); }); it('should throw error when no repositories are configured', () => { const config: Config = { github: { token: 'test-token', repositories: [] }, server: { port: 3001, host: '0.0.0.0' }, cache: { timeoutSeconds: 300 } }; expect(() => validateConfig(config)).toThrow('At least one repository is required'); }); it('should throw error for invalid repository configuration', () => { const config: Config = { github: { token: 'test-token', repositories: [ { owner: '', name: 'test-repo' } ] }, server: { port: 3001, host: '0.0.0.0' }, cache: { timeoutSeconds: 300 } }; expect(() => validateConfig(config)).toThrow('Invalid repository configuration'); }); it('should throw error for repository missing name', () => { const config: Config = { github: { token: 'test-token', repositories: [ { owner: 'test-owner', name: '' } ] }, server: { port: 3001, host: '0.0.0.0' }, cache: { timeoutSeconds: 300 } }; expect(() => validateConfig(config)).toThrow('Invalid repository configuration'); }); it('should validate multiple repositories', () => { const config: Config = { github: { token: 'test-token', repositories: [ { owner: 'owner1', name: 'repo1' }, { owner: 'owner2', name: 'repo2' }, { owner: 'owner3', name: 'repo3' } ] }, server: { port: 3001, host: '0.0.0.0' }, cache: { timeoutSeconds: 300 } }; expect(() => validateConfig(config)).not.toThrow(); }); }); describe('cache configuration', () => { it('should use default cache timeout when not specified', () => { const mockConfig = { github: { token: 'test-token', repositories: [ { owner: 'test-owner', name: 'test-repo' } ] }, server: { port: 3001, host: '0.0.0.0' } }; mockedReadFileSync.mockReturnValueOnce(JSON.stringify(mockConfig)); const result = loadConfig(); expect(result.cache?.timeoutSeconds).toBe(300); }); it('should use custom cache timeout when specified', () => { const mockConfig = { github: { token: 'test-token', repositories: [ { owner: 'test-owner', name: 'test-repo' } ] }, server: { port: 3001, host: '0.0.0.0' }, cache: { timeoutSeconds: 600 } }; mockedReadFileSync.mockReturnValueOnce(JSON.stringify(mockConfig)); const result = loadConfig(); expect(result.cache?.timeoutSeconds).toBe(600); }); }); });