import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Config } from './config'; // Mock the config and server modules const mockConfig: Config = { github: { token: 'ghp_super_secret_token_123', repositories: [ { owner: 'test-owner', name: 'test-repo' }, { owner: 'another-owner', name: 'another-repo' } ] }, server: { port: 3001, host: '0.0.0.0' }, cache: { timeoutSeconds: 300 } }; // Import the getPublicConfig function (we'll need to export it for testing) // For now, let's simulate what the public config should look like function getPublicConfig(config: Config) { return { repositories: config.github.repositories.map((repo) => ({ owner: repo.owner, name: repo.name, full_name: `${repo.owner}/${repo.name}` })), cache: { timeoutSeconds: config.cache?.timeoutSeconds || 300 }, repositoryCount: config.github.repositories.length }; } describe('Security Tests', () => { describe('Config API Security', () => { it('should not expose sensitive information in public config', () => { const publicConfig = getPublicConfig(mockConfig); // Ensure sensitive data is not included expect(publicConfig).not.toHaveProperty('github'); expect(publicConfig).not.toHaveProperty('server'); expect(publicConfig).not.toHaveProperty('token'); // Ensure no token is accidentally included anywhere const configString = JSON.stringify(publicConfig); expect(configString).not.toContain('ghp_'); expect(configString).not.toContain('token'); expect(configString).not.toContain('secret'); }); it('should only expose safe repository information', () => { const publicConfig = getPublicConfig(mockConfig); expect(publicConfig.repositories).toHaveLength(2); expect(publicConfig.repositories[0]).toEqual({ owner: 'test-owner', name: 'test-repo', full_name: 'test-owner/test-repo' }); expect(publicConfig.repositories[1]).toEqual({ owner: 'another-owner', name: 'another-repo', full_name: 'another-owner/another-repo' }); }); it('should expose safe cache configuration', () => { const publicConfig = getPublicConfig(mockConfig); expect(publicConfig.cache).toEqual({ timeoutSeconds: 300 }); }); it('should include repository count for UI', () => { const publicConfig = getPublicConfig(mockConfig); expect(publicConfig.repositoryCount).toBe(2); }); it('should handle missing cache configuration safely', () => { const configWithoutCache = { ...mockConfig, cache: undefined }; const publicConfig = getPublicConfig(configWithoutCache); expect(publicConfig.cache).toEqual({ timeoutSeconds: 300 }); }); it('should never expose server configuration', () => { const publicConfig = getPublicConfig(mockConfig); expect(publicConfig).not.toHaveProperty('port'); expect(publicConfig).not.toHaveProperty('host'); expect(publicConfig).not.toHaveProperty('server'); }); it('should never expose GitHub token', () => { const publicConfig = getPublicConfig(mockConfig); // Check that token is never exposed in any form const configString = JSON.stringify(publicConfig); expect(configString).not.toContain('ghp_super_secret_token_123'); expect(configString).not.toContain('token'); }); it('should handle repository with potential sensitive data', () => { const configWithSensitiveRepo = { ...mockConfig, github: { ...mockConfig.github, repositories: [ { owner: 'test-owner', name: 'test-repo', // @ts-ignore - testing potential sensitive data token: 'per-repo-token-123' } ] } }; const publicConfig = getPublicConfig(configWithSensitiveRepo); expect(publicConfig.repositories[0]).toEqual({ owner: 'test-owner', name: 'test-repo', full_name: 'test-owner/test-repo' }); // Ensure repository-specific sensitive data is not exposed expect(publicConfig.repositories[0]).not.toHaveProperty('token'); }); }); describe('Safe Logging', () => { it('should log config changes without sensitive data', () => { const consoleSpy = vi.spyOn(console, 'log'); // Simulate the logConfigChange function function logConfigChange(config: Config) { console.log(`📊 Configuration loaded: ${config.github.repositories.length} repositories`); console.log(`💾 Cache timeout: ${config.cache?.timeoutSeconds || 300} seconds`); } logConfigChange(mockConfig); const logCalls = consoleSpy.mock.calls.flat(); const allLogs = logCalls.join(' '); // Ensure no sensitive data is logged expect(allLogs).not.toContain('ghp_super_secret_token_123'); expect(allLogs).not.toContain('token'); expect(allLogs).not.toContain('3001'); // port expect(allLogs).not.toContain('0.0.0.0'); // host // Ensure safe data is logged expect(allLogs).toContain('2 repositories'); expect(allLogs).toContain('300 seconds'); consoleSpy.mockRestore(); }); }); describe('Data Sanitization', () => { it('should sanitize any potential sensitive data in repository names', () => { const configWithSensitiveNames = { ...mockConfig, github: { ...mockConfig.github, repositories: [ { owner: 'test-owner', name: 'repo-with-token-ghp123' }, { owner: 'user', name: 'secret-project' } ] } }; const publicConfig = getPublicConfig(configWithSensitiveNames); // Repository names should be preserved as-is (they're not sensitive themselves) // but we should ensure the filtering process doesn't accidentally expose other data expect(publicConfig.repositories[0].name).toBe('repo-with-token-ghp123'); expect(publicConfig.repositories[1].name).toBe('secret-project'); // But the actual token should never be exposed const configString = JSON.stringify(publicConfig); expect(configString).not.toContain('ghp_super_secret_token_123'); }); }); });