/** * Account Manager Discovery Tests * * Tests for discoverExistingAccounts() function including: * - Kiro/GHCP provider mapping * - Email extraction from filename * - Multiple accounts per provider */ const assert = require('assert'); const fs = require('fs'); const path = require('path'); const os = require('os'); describe('Account Manager - discoverExistingAccounts', () => { let testDir; let originalCcsHome; let accountManager; beforeEach(() => { // Create temp directory for test testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-test-')); const ccsDir = path.join(testDir, '.ccs', 'cliproxy'); const authDir = path.join(ccsDir, 'auth'); fs.mkdirSync(authDir, { recursive: true }); // Mock CCS_HOME to use temp directory originalCcsHome = process.env.CCS_HOME; process.env.CCS_HOME = testDir; // Clear cache and reload - must clear all modular submodules too delete require.cache[require.resolve('../../../dist/cliproxy/account-manager')]; delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')]; delete require.cache[require.resolve('../../../dist/utils/config-manager')]; // Clear new modular structure (accounts/ and config/ submodules) delete require.cache[require.resolve('../../../dist/cliproxy/accounts/index')]; delete require.cache[require.resolve('../../../dist/cliproxy/accounts/registry')]; delete require.cache[require.resolve('../../../dist/cliproxy/accounts/token-file-ops')]; delete require.cache[require.resolve('../../../dist/cliproxy/accounts/query')]; delete require.cache[require.resolve('../../../dist/cliproxy/accounts/types')]; delete require.cache[require.resolve('../../../dist/cliproxy/config/index')]; delete require.cache[require.resolve('../../../dist/cliproxy/config/path-resolver')]; accountManager = require('../../../dist/cliproxy/account-manager'); }); afterEach(() => { // Restore CCS_HOME if (originalCcsHome !== undefined) { process.env.CCS_HOME = originalCcsHome; } else { delete process.env.CCS_HOME; } // Cleanup temp directory fs.rmSync(testDir, { recursive: true, force: true }); }); function createAuthFile(filename, content) { const authDir = path.join(testDir, '.ccs', 'cliproxy', 'auth'); fs.writeFileSync(path.join(authDir, filename), JSON.stringify(content)); } function getAccountsFile() { const accountsPath = path.join(testDir, '.ccs', 'cliproxy', 'accounts.json'); if (fs.existsSync(accountsPath)) { return JSON.parse(fs.readFileSync(accountsPath, 'utf-8')); } return null; } // ========================================================================= // Provider Type Mapping Tests // ========================================================================= describe('Provider Type Mapping', () => { it('discovers kiro accounts with type="kiro"', () => { createAuthFile('kiro-google-user@example.com.json', { type: 'kiro', email: '', provider: 'Google', profile_arn: 'arn:aws:codewhisperer:us-east-1::profile/123', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); assert(accounts, 'accounts.json should be created'); assert(accounts.providers.kiro, 'kiro provider should exist'); assert.strictEqual( Object.keys(accounts.providers.kiro.accounts).length, 1, 'Should have 1 kiro account' ); }); it('discovers kiro accounts with type="codewhisperer"', () => { createAuthFile('codewhisperer-user@example.com.json', { type: 'codewhisperer', email: 'user@example.com', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); assert(accounts.providers.kiro, 'codewhisperer type should map to kiro provider'); }); it('discovers ghcp accounts with type="github-copilot"', () => { createAuthFile('ghcp-user@example.com.json', { type: 'github-copilot', email: 'user@example.com', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); assert(accounts.providers.ghcp, 'github-copilot type should map to ghcp provider'); }); it('discovers ghcp accounts with type="copilot"', () => { createAuthFile('copilot-user@example.com.json', { type: 'copilot', email: 'user@example.com', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); assert(accounts.providers.ghcp, 'copilot type should map to ghcp provider'); }); it('handles case-insensitive type values', () => { createAuthFile('kiro-test.json', { type: 'KIRO', email: 'test@example.com', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); assert(accounts.providers.kiro, 'KIRO (uppercase) should map to kiro provider'); }); }); // ========================================================================= // Email Extraction Tests // ========================================================================= describe('Email Extraction from Filename', () => { it('extracts email from filename when data.email is empty', () => { createAuthFile('kiro-google-user@example.com.json', { type: 'kiro', email: '', provider: 'Google', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); const accountIds = Object.keys(accounts.providers.kiro.accounts); assert.strictEqual(accountIds[0], 'user@example.com', 'Should extract email from filename'); }); it('uses data.email when available', () => { createAuthFile('kiro-test.json', { type: 'kiro', email: 'actual@email.com', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); const accountIds = Object.keys(accounts.providers.kiro.accounts); assert.strictEqual(accountIds[0], 'actual@email.com', 'Should use data.email when available'); }); it('generates unique ID for kiro/ghcp when no email in file or filename', () => { createAuthFile('kiro-nomail.json', { type: 'kiro', email: '', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); const accountIds = Object.keys(accounts.providers.kiro.accounts); // For kiro/ghcp without email, generates unique ID like "kiro-1" assert.strictEqual(accountIds[0], 'kiro-1', 'Should generate unique ID for kiro without email'); }); it('handles dots in email local part', () => { createAuthFile('kiro-google-first.last@example.com.json', { type: 'kiro', email: '', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); const accountIds = Object.keys(accounts.providers.kiro.accounts); assert.strictEqual( accountIds[0], 'first.last@example.com', 'Should handle dots in local part' ); }); it('handles plus addressing in email', () => { createAuthFile('kiro-google-user+tag@example.com.json', { type: 'kiro', email: '', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); const accountIds = Object.keys(accounts.providers.kiro.accounts); assert.strictEqual(accountIds[0], 'user+tag@example.com', 'Should handle plus addressing'); }); it('handles subdomain email addresses', () => { createAuthFile('codex-user@mail.example.com.json', { type: 'codex', email: '', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); const accountIds = Object.keys(accounts.providers.codex.accounts); assert.strictEqual(accountIds[0], 'user@mail.example.com', 'Should handle subdomains'); }); }); // ========================================================================= // Multiple Accounts Tests // ========================================================================= describe('Multiple Accounts', () => { it('discovers multiple kiro accounts', () => { createAuthFile('kiro-google-user1@example.com.json', { type: 'kiro', email: '', }); createAuthFile('kiro-google-user2@example.com.json', { type: 'kiro', email: '', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); assert.strictEqual( Object.keys(accounts.providers.kiro.accounts).length, 2, 'Should discover 2 kiro accounts' ); }); it('does not overwrite existing accounts', () => { // Create first account createAuthFile('kiro-google-user1@example.com.json', { type: 'kiro', email: '', }); accountManager.discoverExistingAccounts(); // Add another file and run again createAuthFile('kiro-google-user2@example.com.json', { type: 'kiro', email: '', }); // Reload module to clear in-memory cache (including all submodules) delete require.cache[require.resolve('../../../dist/cliproxy/account-manager')]; delete require.cache[require.resolve('../../../dist/cliproxy/accounts/index')]; delete require.cache[require.resolve('../../../dist/cliproxy/accounts/registry')]; accountManager = require('../../../dist/cliproxy/account-manager'); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); assert.strictEqual( Object.keys(accounts.providers.kiro.accounts).length, 2, 'Should have 2 accounts without overwriting' ); }); it('sets first discovered account as default', () => { createAuthFile('kiro-google-user@example.com.json', { type: 'kiro', email: '', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); assert.strictEqual( accounts.providers.kiro.default, 'user@example.com', 'First account should be default' ); }); it('generates sequential IDs for multiple kiro files without email', () => { // Create multiple kiro files without email in filename // Files like "kiro-nomail.json" don't match oauth pattern (need 2+ hyphens) createAuthFile('kiro-account1.json', { type: 'kiro', email: '', }); createAuthFile('kiro-account2.json', { type: 'kiro', email: '', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); const accountIds = Object.keys(accounts.providers.kiro.accounts); assert.strictEqual(accountIds.length, 2, 'Should have 2 accounts'); // Both should have kiro-N format since filenames don't match oauth pattern (1 hyphen only) assert(accountIds.includes('kiro-1'), 'Should have kiro-1'); assert(accountIds.includes('kiro-2'), 'Should have kiro-2'); }); it('skips to next ID when collision exists', () => { // Pre-create accounts.json with kiro-1 already registered const accountsPath = path.join(testDir, '.ccs', 'cliproxy', 'accounts.json'); const existingRegistry = { version: 1, providers: { kiro: { default: 'kiro-1', accounts: { 'kiro-1': { tokenFile: 'kiro-existing.json', createdAt: new Date().toISOString(), }, }, }, }, }; fs.writeFileSync(accountsPath, JSON.stringify(existingRegistry)); // Create the existing token file (matches registry) createAuthFile('kiro-existing.json', { type: 'kiro', email: '', }); // Create new file that will need auto-generated ID (single hyphen = no oauth pattern match) createAuthFile('kiro-newaccount.json', { type: 'kiro', email: '', }); // Reload module to pick up pre-existing accounts.json (including all submodules) delete require.cache[require.resolve('../../../dist/cliproxy/account-manager')]; delete require.cache[require.resolve('../../../dist/cliproxy/accounts/index')]; delete require.cache[require.resolve('../../../dist/cliproxy/accounts/registry')]; accountManager = require('../../../dist/cliproxy/account-manager'); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); const accountIds = Object.keys(accounts.providers.kiro.accounts); assert.strictEqual(accountIds.length, 2, 'Should have 2 accounts'); assert(accountIds.includes('kiro-1'), 'Should keep existing kiro-1'); assert(accountIds.includes('kiro-2'), 'New account should be kiro-2 (skipped kiro-1)'); }); }); // ========================================================================= // Edge Cases // ========================================================================= describe('Edge Cases', () => { it('skips files without type field', () => { createAuthFile('invalid.json', { email: 'test@example.com', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); // Should either be null or have no providers if (accounts) { assert.strictEqual(Object.keys(accounts.providers).length, 0, 'Should have no providers'); } }); it('skips files with unknown type', () => { createAuthFile('unknown.json', { type: 'unknown-provider', email: 'test@example.com', }); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); if (accounts) { assert.strictEqual( Object.keys(accounts.providers).length, 0, 'Should skip unknown providers' ); } }); it('skips non-JSON files', () => { fs.writeFileSync( path.join(testDir, '.ccs', 'cliproxy', 'auth', 'readme.txt'), 'This is not JSON' ); accountManager.discoverExistingAccounts(); const accounts = getAccountsFile(); // Should not throw, should skip the file if (accounts) { assert.strictEqual(Object.keys(accounts.providers).length, 0); } }); it('handles malformed JSON gracefully', () => { fs.writeFileSync( path.join(testDir, '.ccs', 'cliproxy', 'auth', 'bad.json'), '{not valid json' ); // Should not throw assert.doesNotThrow(() => { accountManager.discoverExistingAccounts(); }); }); it('handles missing auth directory', () => { // Remove auth directory fs.rmSync(path.join(testDir, '.ccs', 'cliproxy', 'auth'), { recursive: true }); // Should not throw assert.doesNotThrow(() => { accountManager.discoverExistingAccounts(); }); }); }); });