mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 18:18:21 +00:00
fix(cliproxy): preserve user API keys during config regeneration
When CCS updates trigger CLIProxy config regeneration, user-added API keys in ~/.ccs/cliproxy/config.yaml were being overwritten with only the internal CCS key. Changes: - Add parseUserApiKeys() to extract user keys from existing config - Modify regenerateConfig() to preserve user API keys alongside port - Update generateUnifiedConfigContent() to accept userApiKeys param - Add comprehensive test suite (14 tests) for key preservation Fixes #200
This commit is contained in:
1 parent
7dafed663b
commit
2b4d21e8ae
3 files changed
+435
-9
No files matched your search
@@ -166,6 +166,365 @@ describe('Config Generator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// API Key Preservation Tests (Issue #200)
|
||||
// =========================================================================
|
||||
describe('parseUserApiKeys', () => {
|
||||
let parseUserApiKeys;
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear cache and reload module
|
||||
delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
|
||||
const configGenerator = require('../../../dist/cliproxy/config-generator');
|
||||
parseUserApiKeys = configGenerator.parseUserApiKeys;
|
||||
});
|
||||
|
||||
it('extracts user-added API keys from config content', () => {
|
||||
const configContent = `
|
||||
# CLIProxyAPI config generated by CCS v4
|
||||
port: 8317
|
||||
|
||||
api-keys:
|
||||
- "ccs-internal-managed"
|
||||
- "user-custom-key-12345"
|
||||
- "another-user-key-abcde"
|
||||
|
||||
auth-dir: "/home/user/.ccs/cliproxy/auth"
|
||||
`;
|
||||
|
||||
const userKeys = parseUserApiKeys(configContent);
|
||||
|
||||
assert.deepStrictEqual(userKeys, ['user-custom-key-12345', 'another-user-key-abcde']);
|
||||
});
|
||||
|
||||
it('excludes the internal CCS key', () => {
|
||||
const configContent = `
|
||||
api-keys:
|
||||
- "ccs-internal-managed"
|
||||
- "my-custom-key"
|
||||
|
||||
auth-dir: "/test"
|
||||
`;
|
||||
|
||||
const userKeys = parseUserApiKeys(configContent);
|
||||
|
||||
assert.strictEqual(userKeys.length, 1);
|
||||
assert.strictEqual(userKeys[0], 'my-custom-key');
|
||||
assert(!userKeys.includes('ccs-internal-managed'), 'Should not include internal key');
|
||||
});
|
||||
|
||||
it('returns empty array when only internal key exists', () => {
|
||||
const configContent = `
|
||||
api-keys:
|
||||
- "ccs-internal-managed"
|
||||
|
||||
auth-dir: "/test"
|
||||
`;
|
||||
|
||||
const userKeys = parseUserApiKeys(configContent);
|
||||
|
||||
assert.deepStrictEqual(userKeys, []);
|
||||
});
|
||||
|
||||
it('returns empty array when no api-keys section exists', () => {
|
||||
const configContent = `
|
||||
port: 8317
|
||||
debug: false
|
||||
auth-dir: "/test"
|
||||
`;
|
||||
|
||||
const userKeys = parseUserApiKeys(configContent);
|
||||
|
||||
assert.deepStrictEqual(userKeys, []);
|
||||
});
|
||||
|
||||
it('handles single user key', () => {
|
||||
const configContent = `
|
||||
api-keys:
|
||||
- "ccs-internal-managed"
|
||||
- "single-user-key"
|
||||
|
||||
auth-dir: "/test"
|
||||
`;
|
||||
|
||||
const userKeys = parseUserApiKeys(configContent);
|
||||
|
||||
assert.deepStrictEqual(userKeys, ['single-user-key']);
|
||||
});
|
||||
|
||||
it('handles multiple user keys', () => {
|
||||
const configContent = `
|
||||
api-keys:
|
||||
- "ccs-internal-managed"
|
||||
- "key1"
|
||||
- "key2"
|
||||
- "key3"
|
||||
- "key4"
|
||||
- "key5"
|
||||
|
||||
auth-dir: "/test"
|
||||
`;
|
||||
|
||||
const userKeys = parseUserApiKeys(configContent);
|
||||
|
||||
assert.deepStrictEqual(userKeys, ['key1', 'key2', 'key3', 'key4', 'key5']);
|
||||
});
|
||||
|
||||
it('handles keys with special characters', () => {
|
||||
const configContent = `
|
||||
api-keys:
|
||||
- "ccs-internal-managed"
|
||||
- "sk_live_1234567890abcdef"
|
||||
- "api-key-with-dashes"
|
||||
- "key_with_underscores_123"
|
||||
|
||||
auth-dir: "/test"
|
||||
`;
|
||||
|
||||
const userKeys = parseUserApiKeys(configContent);
|
||||
|
||||
assert.deepStrictEqual(userKeys, [
|
||||
'sk_live_1234567890abcdef',
|
||||
'api-key-with-dashes',
|
||||
'key_with_underscores_123',
|
||||
]);
|
||||
});
|
||||
|
||||
it('preserves key order', () => {
|
||||
const configContent = `
|
||||
api-keys:
|
||||
- "ccs-internal-managed"
|
||||
- "zebra-key"
|
||||
- "alpha-key"
|
||||
- "middle-key"
|
||||
|
||||
auth-dir: "/test"
|
||||
`;
|
||||
|
||||
const userKeys = parseUserApiKeys(configContent);
|
||||
|
||||
assert.deepStrictEqual(userKeys, ['zebra-key', 'alpha-key', 'middle-key']);
|
||||
});
|
||||
|
||||
it('handles empty string key (edge case)', () => {
|
||||
const configContent = `
|
||||
api-keys:
|
||||
- "ccs-internal-managed"
|
||||
- ""
|
||||
- "valid-key"
|
||||
|
||||
auth-dir: "/test"
|
||||
`;
|
||||
|
||||
const userKeys = parseUserApiKeys(configContent);
|
||||
|
||||
// Empty strings should be filtered out (only truthy values)
|
||||
assert.deepStrictEqual(userKeys, ['valid-key']);
|
||||
});
|
||||
|
||||
it('handles config with Windows line endings', () => {
|
||||
const configContent =
|
||||
'api-keys:\r\n - "ccs-internal-managed"\r\n - "user-key"\r\n\r\nauth-dir: "/test"\r\n';
|
||||
|
||||
const userKeys = parseUserApiKeys(configContent);
|
||||
|
||||
assert.deepStrictEqual(userKeys, ['user-key']);
|
||||
});
|
||||
|
||||
it('handles different indentation styles', () => {
|
||||
const configContent = `
|
||||
api-keys:
|
||||
- "ccs-internal-managed"
|
||||
- "user-key-with-4-spaces"
|
||||
|
||||
auth-dir: "/test"
|
||||
`;
|
||||
|
||||
const userKeys = parseUserApiKeys(configContent);
|
||||
|
||||
assert.deepStrictEqual(userKeys, ['user-key-with-4-spaces']);
|
||||
});
|
||||
|
||||
it('handles tabs in indentation', () => {
|
||||
const configContent = 'api-keys:\n\t- "ccs-internal-managed"\n\t- "user-key-with-tab"\n\nauth-dir: "/test"';
|
||||
|
||||
const userKeys = parseUserApiKeys(configContent);
|
||||
|
||||
assert.deepStrictEqual(userKeys, ['user-key-with-tab']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('regenerateConfig API key preservation', () => {
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
let testDir;
|
||||
let originalCcsHome;
|
||||
let regenerateConfig;
|
||||
let getConfigPath;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a temporary test directory
|
||||
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-test-'));
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
process.env.CCS_HOME = testDir;
|
||||
|
||||
// Clear cache and reload module
|
||||
delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
|
||||
delete require.cache[require.resolve('../../../dist/utils/config-manager')];
|
||||
const configGenerator = require('../../../dist/cliproxy/config-generator');
|
||||
regenerateConfig = configGenerator.regenerateConfig;
|
||||
getConfigPath = configGenerator.getConfigPath;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore environment
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
|
||||
// Clean up test directory
|
||||
if (testDir && fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('preserves user API keys during regeneration', () => {
|
||||
// Create initial config with user keys
|
||||
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
||||
fs.mkdirSync(cliproxyDir, { recursive: true });
|
||||
|
||||
const initialConfig = `# CLIProxyAPI config generated by CCS v3
|
||||
port: 8317
|
||||
|
||||
api-keys:
|
||||
- "ccs-internal-managed"
|
||||
- "user-custom-key-12345"
|
||||
- "another-user-key"
|
||||
|
||||
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
|
||||
`;
|
||||
fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);
|
||||
|
||||
// Regenerate config (simulating CCS update)
|
||||
regenerateConfig();
|
||||
|
||||
// Read regenerated config
|
||||
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
||||
|
||||
// Verify user keys are preserved
|
||||
assert(newConfig.includes('user-custom-key-12345'), 'Should preserve first user key');
|
||||
assert(newConfig.includes('another-user-key'), 'Should preserve second user key');
|
||||
assert(newConfig.includes('ccs-internal-managed'), 'Should include internal key');
|
||||
});
|
||||
|
||||
it('preserves port setting during regeneration', () => {
|
||||
// Create initial config with custom port
|
||||
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
||||
fs.mkdirSync(cliproxyDir, { recursive: true });
|
||||
|
||||
const initialConfig = `# CLIProxyAPI config generated by CCS v3
|
||||
port: 9999
|
||||
|
||||
api-keys:
|
||||
- "ccs-internal-managed"
|
||||
|
||||
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
|
||||
`;
|
||||
fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);
|
||||
|
||||
// Regenerate config
|
||||
regenerateConfig();
|
||||
|
||||
// Read regenerated config
|
||||
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
||||
|
||||
// Verify port is preserved
|
||||
assert(newConfig.includes('port: 9999'), 'Should preserve custom port');
|
||||
});
|
||||
|
||||
it('creates fresh config when none exists', () => {
|
||||
// Ensure clean state
|
||||
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
||||
const configPath = path.join(cliproxyDir, 'config.yaml');
|
||||
|
||||
assert(!fs.existsSync(configPath), 'Config should not exist initially');
|
||||
|
||||
// Generate config
|
||||
regenerateConfig();
|
||||
|
||||
// Verify config was created
|
||||
assert(fs.existsSync(configPath), 'Config should be created');
|
||||
|
||||
const config = fs.readFileSync(configPath, 'utf-8');
|
||||
assert(config.includes('ccs-internal-managed'), 'Should include internal key');
|
||||
assert(config.includes('port: 8317'), 'Should use default port');
|
||||
});
|
||||
|
||||
it('handles corrupted config gracefully', () => {
|
||||
// Create corrupted config
|
||||
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
||||
fs.mkdirSync(cliproxyDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), 'invalid yaml content: [[[');
|
||||
|
||||
// Should not throw
|
||||
assert.doesNotThrow(() => regenerateConfig());
|
||||
|
||||
// Should create valid config
|
||||
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
||||
assert(newConfig.includes('ccs-internal-managed'), 'Should create valid config');
|
||||
});
|
||||
|
||||
it('handles empty config file gracefully', () => {
|
||||
// Create empty config
|
||||
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
||||
fs.mkdirSync(cliproxyDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), '');
|
||||
|
||||
// Should not throw
|
||||
assert.doesNotThrow(() => regenerateConfig());
|
||||
|
||||
// Should create valid config with defaults
|
||||
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
||||
assert(newConfig.includes('ccs-internal-managed'), 'Should create valid config');
|
||||
assert(newConfig.includes('port: 8317'), 'Should use default port');
|
||||
});
|
||||
|
||||
it('preserves multiple user keys in correct order', () => {
|
||||
// Create config with multiple user keys
|
||||
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
||||
fs.mkdirSync(cliproxyDir, { recursive: true });
|
||||
|
||||
const initialConfig = `# CLIProxyAPI config generated by CCS v3
|
||||
port: 8317
|
||||
|
||||
api-keys:
|
||||
- "ccs-internal-managed"
|
||||
- "key-first"
|
||||
- "key-second"
|
||||
- "key-third"
|
||||
|
||||
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
|
||||
`;
|
||||
fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);
|
||||
|
||||
// Regenerate config
|
||||
regenerateConfig();
|
||||
|
||||
// Read regenerated config
|
||||
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
||||
|
||||
// Verify order is preserved (first > second > third)
|
||||
const firstPos = newConfig.indexOf('key-first');
|
||||
const secondPos = newConfig.indexOf('key-second');
|
||||
const thirdPos = newConfig.indexOf('key-third');
|
||||
|
||||
assert(firstPos < secondPos, 'First key should come before second');
|
||||
assert(secondPos < thirdPos, 'Second key should come before third');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cross-platform path handling', () => {
|
||||
it('normalizes paths regardless of current platform', () => {
|
||||
// The fix should work consistently on Windows, macOS, and Linux
|
||||
|
||||
Reference in new issue
Block a user