mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 18:16:08 +00:00
445 lines
15 KiB
JavaScript
445 lines
15 KiB
JavaScript
/**
|
|
* 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();
|
|
});
|
|
});
|
|
});
|
|
});
|