mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
- Enhanced getClaudeAuthIndicators() to return detailed check information including file paths checked and specific error details for debugging - Added debug logging to server startup credential detection for easier troubleshooting in Docker environments - Show paths that were checked in the warning message to help users debug mount issues - Added support for CLAUDE_CODE_OAUTH_TOKEN environment variable - Return authType in verify-claude-auth response to distinguish between OAuth and CLI authentication methods - Updated UI to show specific success messages for Claude Code subscription vs generic CLI auth - Added Docker troubleshooting tips to sandbox risk dialog - Added comprehensive unit tests for OAuth credential detection scenarios Closes #721 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
737 lines
27 KiB
TypeScript
737 lines
27 KiB
TypeScript
/**
|
|
* Unit tests for OAuth credential detection scenarios
|
|
*
|
|
* Tests the various Claude credential detection formats including:
|
|
* - Claude Code CLI OAuth format (claudeAiOauth)
|
|
* - Legacy OAuth token format (oauth_token, access_token)
|
|
* - API key format (api_key)
|
|
* - Invalid/malformed credential files
|
|
*
|
|
* These tests use real temp directories to avoid complex fs mocking issues.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
import os from 'os';
|
|
|
|
describe('OAuth Credential Detection', () => {
|
|
let tempDir: string;
|
|
let originalHomedir: () => string;
|
|
let mockClaudeDir: string;
|
|
let mockCodexDir: string;
|
|
let mockOpenCodeDir: string;
|
|
|
|
beforeEach(async () => {
|
|
// Reset modules to get fresh state
|
|
vi.resetModules();
|
|
|
|
// Create a temporary directory
|
|
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oauth-detection-test-'));
|
|
|
|
// Create mock home directory structure
|
|
mockClaudeDir = path.join(tempDir, '.claude');
|
|
mockCodexDir = path.join(tempDir, '.codex');
|
|
mockOpenCodeDir = path.join(tempDir, '.local', 'share', 'opencode');
|
|
|
|
await fs.mkdir(mockClaudeDir, { recursive: true });
|
|
await fs.mkdir(mockCodexDir, { recursive: true });
|
|
await fs.mkdir(mockOpenCodeDir, { recursive: true });
|
|
|
|
// Mock os.homedir to return our temp directory
|
|
originalHomedir = os.homedir;
|
|
vi.spyOn(os, 'homedir').mockReturnValue(tempDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
vi.restoreAllMocks();
|
|
// Clean up temp directory
|
|
try {
|
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
describe('getClaudeAuthIndicators', () => {
|
|
it('should detect Claude Code CLI OAuth format (claudeAiOauth)', async () => {
|
|
const credentialsContent = JSON.stringify({
|
|
claudeAiOauth: {
|
|
accessToken: 'oauth-access-token-12345',
|
|
refreshToken: 'oauth-refresh-token-67890',
|
|
expiresAt: Date.now() + 3600000,
|
|
},
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), credentialsContent);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasCredentialsFile).toBe(true);
|
|
expect(indicators.credentials).not.toBeNull();
|
|
expect(indicators.credentials?.hasOAuthToken).toBe(true);
|
|
expect(indicators.credentials?.hasApiKey).toBe(false);
|
|
});
|
|
|
|
it('should detect legacy OAuth token format (oauth_token)', async () => {
|
|
const credentialsContent = JSON.stringify({
|
|
oauth_token: 'legacy-oauth-token-abcdef',
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), credentialsContent);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasCredentialsFile).toBe(true);
|
|
expect(indicators.credentials?.hasOAuthToken).toBe(true);
|
|
expect(indicators.credentials?.hasApiKey).toBe(false);
|
|
});
|
|
|
|
it('should detect legacy access_token format', async () => {
|
|
const credentialsContent = JSON.stringify({
|
|
access_token: 'legacy-access-token-xyz',
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), credentialsContent);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasCredentialsFile).toBe(true);
|
|
expect(indicators.credentials?.hasOAuthToken).toBe(true);
|
|
expect(indicators.credentials?.hasApiKey).toBe(false);
|
|
});
|
|
|
|
it('should detect API key format', async () => {
|
|
const credentialsContent = JSON.stringify({
|
|
api_key: 'sk-ant-api03-xxxxxxxxxxxx',
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), credentialsContent);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasCredentialsFile).toBe(true);
|
|
expect(indicators.credentials?.hasOAuthToken).toBe(false);
|
|
expect(indicators.credentials?.hasApiKey).toBe(true);
|
|
});
|
|
|
|
it('should detect both OAuth and API key when present', async () => {
|
|
const credentialsContent = JSON.stringify({
|
|
claudeAiOauth: {
|
|
accessToken: 'oauth-token',
|
|
refreshToken: 'refresh-token',
|
|
},
|
|
api_key: 'sk-ant-api03-xxxxxxxxxxxx',
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), credentialsContent);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasCredentialsFile).toBe(true);
|
|
expect(indicators.credentials?.hasOAuthToken).toBe(true);
|
|
expect(indicators.credentials?.hasApiKey).toBe(true);
|
|
});
|
|
|
|
it('should handle missing credentials file gracefully', async () => {
|
|
// No credentials file created
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasCredentialsFile).toBe(false);
|
|
expect(indicators.credentials).toBeNull();
|
|
expect(indicators.checks.credentialFiles).toBeDefined();
|
|
expect(indicators.checks.credentialFiles.length).toBeGreaterThan(0);
|
|
expect(indicators.checks.credentialFiles[0].exists).toBe(false);
|
|
});
|
|
|
|
it('should handle malformed JSON in credentials file', async () => {
|
|
const malformedContent = '{ invalid json }';
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), malformedContent);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
// File exists but parsing fails
|
|
expect(indicators.hasCredentialsFile).toBe(false);
|
|
expect(indicators.credentials).toBeNull();
|
|
expect(indicators.checks.credentialFiles[0].exists).toBe(true);
|
|
expect(indicators.checks.credentialFiles[0].error).toContain('JSON parse error');
|
|
});
|
|
|
|
it('should handle empty credentials file', async () => {
|
|
const emptyContent = JSON.stringify({});
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), emptyContent);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasCredentialsFile).toBe(true);
|
|
expect(indicators.credentials).not.toBeNull();
|
|
expect(indicators.credentials?.hasOAuthToken).toBe(false);
|
|
expect(indicators.credentials?.hasApiKey).toBe(false);
|
|
});
|
|
|
|
it('should handle credentials file with null values', async () => {
|
|
const nullContent = JSON.stringify({
|
|
claudeAiOauth: null,
|
|
api_key: null,
|
|
oauth_token: null,
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), nullContent);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasCredentialsFile).toBe(true);
|
|
expect(indicators.credentials?.hasOAuthToken).toBe(false);
|
|
expect(indicators.credentials?.hasApiKey).toBe(false);
|
|
});
|
|
|
|
it('should handle credentials with empty string values', async () => {
|
|
const emptyStrings = JSON.stringify({
|
|
claudeAiOauth: {
|
|
accessToken: '',
|
|
refreshToken: '',
|
|
},
|
|
api_key: '',
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), emptyStrings);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasCredentialsFile).toBe(true);
|
|
// Empty strings should not be treated as valid credentials
|
|
expect(indicators.credentials?.hasOAuthToken).toBe(false);
|
|
expect(indicators.credentials?.hasApiKey).toBe(false);
|
|
});
|
|
|
|
it('should detect settings file presence', async () => {
|
|
await fs.writeFile(
|
|
path.join(mockClaudeDir, 'settings.json'),
|
|
JSON.stringify({ theme: 'dark' })
|
|
);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasSettingsFile).toBe(true);
|
|
expect(indicators.checks.settingsFile.exists).toBe(true);
|
|
expect(indicators.checks.settingsFile.readable).toBe(true);
|
|
});
|
|
|
|
it('should detect stats cache with activity', async () => {
|
|
const statsContent = JSON.stringify({
|
|
dailyActivity: [
|
|
{ date: '2025-01-15', messagesCount: 10 },
|
|
{ date: '2025-01-16', messagesCount: 5 },
|
|
],
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, 'stats-cache.json'), statsContent);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasStatsCacheWithActivity).toBe(true);
|
|
expect(indicators.checks.statsCache.exists).toBe(true);
|
|
expect(indicators.checks.statsCache.hasDailyActivity).toBe(true);
|
|
});
|
|
|
|
it('should detect stats cache without activity', async () => {
|
|
const statsContent = JSON.stringify({
|
|
dailyActivity: [],
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, 'stats-cache.json'), statsContent);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasStatsCacheWithActivity).toBe(false);
|
|
expect(indicators.checks.statsCache.exists).toBe(true);
|
|
expect(indicators.checks.statsCache.hasDailyActivity).toBe(false);
|
|
});
|
|
|
|
it('should detect project sessions', async () => {
|
|
const projectsDir = path.join(mockClaudeDir, 'projects');
|
|
await fs.mkdir(projectsDir, { recursive: true });
|
|
await fs.mkdir(path.join(projectsDir, 'session-1'));
|
|
await fs.mkdir(path.join(projectsDir, 'session-2'));
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasProjectsSessions).toBe(true);
|
|
expect(indicators.checks.projectsDir.exists).toBe(true);
|
|
expect(indicators.checks.projectsDir.entryCount).toBe(2);
|
|
});
|
|
|
|
it('should return comprehensive check details', async () => {
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
// Verify all check detail objects are present
|
|
expect(indicators.checks).toBeDefined();
|
|
expect(indicators.checks.settingsFile).toBeDefined();
|
|
expect(indicators.checks.settingsFile.path).toContain('settings.json');
|
|
expect(indicators.checks.statsCache).toBeDefined();
|
|
expect(indicators.checks.statsCache.path).toContain('stats-cache.json');
|
|
expect(indicators.checks.projectsDir).toBeDefined();
|
|
expect(indicators.checks.projectsDir.path).toContain('projects');
|
|
expect(indicators.checks.credentialFiles).toBeDefined();
|
|
expect(Array.isArray(indicators.checks.credentialFiles)).toBe(true);
|
|
});
|
|
|
|
it('should try both .credentials.json and credentials.json paths', async () => {
|
|
// Write to credentials.json (without leading dot)
|
|
const credentialsContent = JSON.stringify({
|
|
api_key: 'sk-test-key',
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, 'credentials.json'), credentialsContent);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
// Should find credentials in the second path
|
|
expect(indicators.hasCredentialsFile).toBe(true);
|
|
expect(indicators.credentials?.hasApiKey).toBe(true);
|
|
});
|
|
|
|
it('should prefer first credentials file if both exist', async () => {
|
|
// Write OAuth to .credentials.json (first path checked)
|
|
await fs.writeFile(
|
|
path.join(mockClaudeDir, '.credentials.json'),
|
|
JSON.stringify({
|
|
claudeAiOauth: {
|
|
accessToken: 'oauth-token',
|
|
refreshToken: 'refresh-token',
|
|
},
|
|
})
|
|
);
|
|
|
|
// Write API key to credentials.json (second path)
|
|
await fs.writeFile(
|
|
path.join(mockClaudeDir, 'credentials.json'),
|
|
JSON.stringify({
|
|
api_key: 'sk-test-key',
|
|
})
|
|
);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
// Should use first file (.credentials.json) which has OAuth
|
|
expect(indicators.hasCredentialsFile).toBe(true);
|
|
expect(indicators.credentials?.hasOAuthToken).toBe(true);
|
|
expect(indicators.credentials?.hasApiKey).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getCodexAuthIndicators', () => {
|
|
it('should detect OAuth token in Codex auth file', async () => {
|
|
const authContent = JSON.stringify({
|
|
access_token: 'codex-oauth-token-12345',
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockCodexDir, 'auth.json'), authContent);
|
|
|
|
const { getCodexAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getCodexAuthIndicators();
|
|
|
|
expect(indicators.hasAuthFile).toBe(true);
|
|
expect(indicators.hasOAuthToken).toBe(true);
|
|
expect(indicators.hasApiKey).toBe(false);
|
|
});
|
|
|
|
it('should detect API key in Codex auth file', async () => {
|
|
const authContent = JSON.stringify({
|
|
OPENAI_API_KEY: 'sk-xxxxxxxxxxxxxxxx',
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockCodexDir, 'auth.json'), authContent);
|
|
|
|
const { getCodexAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getCodexAuthIndicators();
|
|
|
|
expect(indicators.hasAuthFile).toBe(true);
|
|
expect(indicators.hasOAuthToken).toBe(false);
|
|
expect(indicators.hasApiKey).toBe(true);
|
|
});
|
|
|
|
it('should detect nested tokens in Codex auth file', async () => {
|
|
const authContent = JSON.stringify({
|
|
tokens: {
|
|
oauth_token: 'nested-oauth-token',
|
|
},
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockCodexDir, 'auth.json'), authContent);
|
|
|
|
const { getCodexAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getCodexAuthIndicators();
|
|
|
|
expect(indicators.hasAuthFile).toBe(true);
|
|
expect(indicators.hasOAuthToken).toBe(true);
|
|
});
|
|
|
|
it('should handle missing Codex auth file', async () => {
|
|
// No auth file created
|
|
const { getCodexAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getCodexAuthIndicators();
|
|
|
|
expect(indicators.hasAuthFile).toBe(false);
|
|
expect(indicators.hasOAuthToken).toBe(false);
|
|
expect(indicators.hasApiKey).toBe(false);
|
|
});
|
|
|
|
it('should detect api_key field in Codex auth', async () => {
|
|
const authContent = JSON.stringify({
|
|
api_key: 'sk-api-key-value',
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockCodexDir, 'auth.json'), authContent);
|
|
|
|
const { getCodexAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getCodexAuthIndicators();
|
|
|
|
expect(indicators.hasAuthFile).toBe(true);
|
|
expect(indicators.hasApiKey).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getOpenCodeAuthIndicators', () => {
|
|
it('should detect provider-specific OAuth credentials', async () => {
|
|
const authContent = JSON.stringify({
|
|
anthropic: {
|
|
type: 'oauth',
|
|
access: 'oauth-access-token',
|
|
refresh: 'oauth-refresh-token',
|
|
},
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockOpenCodeDir, 'auth.json'), authContent);
|
|
|
|
const { getOpenCodeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getOpenCodeAuthIndicators();
|
|
|
|
expect(indicators.hasAuthFile).toBe(true);
|
|
expect(indicators.hasOAuthToken).toBe(true);
|
|
expect(indicators.hasApiKey).toBe(false);
|
|
});
|
|
|
|
it('should detect GitHub Copilot refresh token as OAuth', async () => {
|
|
const authContent = JSON.stringify({
|
|
'github-copilot': {
|
|
type: 'oauth',
|
|
access: '', // Empty access token
|
|
refresh: 'gh-refresh-token', // But has refresh token
|
|
},
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockOpenCodeDir, 'auth.json'), authContent);
|
|
|
|
const { getOpenCodeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getOpenCodeAuthIndicators();
|
|
|
|
expect(indicators.hasAuthFile).toBe(true);
|
|
expect(indicators.hasOAuthToken).toBe(true);
|
|
});
|
|
|
|
it('should detect provider-specific API key credentials', async () => {
|
|
const authContent = JSON.stringify({
|
|
openai: {
|
|
type: 'api_key',
|
|
key: 'sk-xxxxxxxxxxxx',
|
|
},
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockOpenCodeDir, 'auth.json'), authContent);
|
|
|
|
const { getOpenCodeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getOpenCodeAuthIndicators();
|
|
|
|
expect(indicators.hasAuthFile).toBe(true);
|
|
expect(indicators.hasOAuthToken).toBe(false);
|
|
expect(indicators.hasApiKey).toBe(true);
|
|
});
|
|
|
|
it('should detect multiple providers', async () => {
|
|
const authContent = JSON.stringify({
|
|
anthropic: {
|
|
type: 'oauth',
|
|
access: 'anthropic-token',
|
|
refresh: 'refresh-token',
|
|
},
|
|
openai: {
|
|
type: 'api_key',
|
|
key: 'sk-xxxxxxxxxxxx',
|
|
},
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockOpenCodeDir, 'auth.json'), authContent);
|
|
|
|
const { getOpenCodeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getOpenCodeAuthIndicators();
|
|
|
|
expect(indicators.hasAuthFile).toBe(true);
|
|
expect(indicators.hasOAuthToken).toBe(true);
|
|
expect(indicators.hasApiKey).toBe(true);
|
|
});
|
|
|
|
it('should handle missing OpenCode auth file', async () => {
|
|
// No auth file created
|
|
const { getOpenCodeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getOpenCodeAuthIndicators();
|
|
|
|
expect(indicators.hasAuthFile).toBe(false);
|
|
expect(indicators.hasOAuthToken).toBe(false);
|
|
expect(indicators.hasApiKey).toBe(false);
|
|
});
|
|
|
|
it('should handle legacy top-level OAuth keys', async () => {
|
|
const authContent = JSON.stringify({
|
|
access_token: 'legacy-access-token',
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockOpenCodeDir, 'auth.json'), authContent);
|
|
|
|
const { getOpenCodeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getOpenCodeAuthIndicators();
|
|
|
|
expect(indicators.hasAuthFile).toBe(true);
|
|
expect(indicators.hasOAuthToken).toBe(true);
|
|
});
|
|
|
|
it('should detect copilot provider OAuth', async () => {
|
|
const authContent = JSON.stringify({
|
|
copilot: {
|
|
type: 'oauth',
|
|
access: 'copilot-access-token',
|
|
refresh: 'copilot-refresh-token',
|
|
},
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockOpenCodeDir, 'auth.json'), authContent);
|
|
|
|
const { getOpenCodeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getOpenCodeAuthIndicators();
|
|
|
|
expect(indicators.hasAuthFile).toBe(true);
|
|
expect(indicators.hasOAuthToken).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Credential path helpers', () => {
|
|
it('should return correct Claude credential paths', async () => {
|
|
const { getClaudeCredentialPaths, getClaudeConfigDir } = await import('../src/system-paths');
|
|
|
|
const configDir = getClaudeConfigDir();
|
|
expect(configDir).toContain('.claude');
|
|
|
|
const credPaths = getClaudeCredentialPaths();
|
|
expect(credPaths.length).toBeGreaterThan(0);
|
|
expect(credPaths.some((p) => p.includes('.credentials.json'))).toBe(true);
|
|
expect(credPaths.some((p) => p.includes('credentials.json'))).toBe(true);
|
|
});
|
|
|
|
it('should return correct Codex auth path', async () => {
|
|
const { getCodexAuthPath, getCodexConfigDir } = await import('../src/system-paths');
|
|
|
|
const configDir = getCodexConfigDir();
|
|
expect(configDir).toContain('.codex');
|
|
|
|
const authPath = getCodexAuthPath();
|
|
expect(authPath).toContain('.codex');
|
|
expect(authPath).toContain('auth.json');
|
|
});
|
|
|
|
it('should return correct OpenCode auth path', async () => {
|
|
const { getOpenCodeAuthPath, getOpenCodeConfigDir } = await import('../src/system-paths');
|
|
|
|
const configDir = getOpenCodeConfigDir();
|
|
expect(configDir).toContain('opencode');
|
|
|
|
const authPath = getOpenCodeAuthPath();
|
|
expect(authPath).toContain('opencode');
|
|
expect(authPath).toContain('auth.json');
|
|
});
|
|
});
|
|
|
|
describe('Edge cases for credential detection', () => {
|
|
it('should handle credentials file with unexpected structure', async () => {
|
|
const unexpectedContent = JSON.stringify({
|
|
someUnexpectedKey: 'value',
|
|
nested: {
|
|
deeply: {
|
|
unexpected: true,
|
|
},
|
|
},
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), unexpectedContent);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasCredentialsFile).toBe(true);
|
|
expect(indicators.credentials?.hasOAuthToken).toBe(false);
|
|
expect(indicators.credentials?.hasApiKey).toBe(false);
|
|
});
|
|
|
|
it('should handle array instead of object in credentials', async () => {
|
|
const arrayContent = JSON.stringify(['token1', 'token2']);
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), arrayContent);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
// Array is valid JSON but wrong structure - should handle gracefully
|
|
expect(indicators.hasCredentialsFile).toBe(true);
|
|
expect(indicators.credentials?.hasOAuthToken).toBe(false);
|
|
expect(indicators.credentials?.hasApiKey).toBe(false);
|
|
});
|
|
|
|
it('should handle numeric values in credential fields', async () => {
|
|
const numericContent = JSON.stringify({
|
|
api_key: 12345,
|
|
oauth_token: 67890,
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), numericContent);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
// Note: Current implementation uses JavaScript truthiness which accepts numbers
|
|
// This documents the actual behavior - ideally would validate string type
|
|
expect(indicators.hasCredentialsFile).toBe(true);
|
|
// The implementation checks truthiness, not strict string type
|
|
expect(indicators.credentials?.hasOAuthToken).toBe(true);
|
|
expect(indicators.credentials?.hasApiKey).toBe(true);
|
|
});
|
|
|
|
it('should handle boolean values in credential fields', async () => {
|
|
const booleanContent = JSON.stringify({
|
|
api_key: true,
|
|
oauth_token: false,
|
|
});
|
|
|
|
await fs.writeFile(path.join(mockClaudeDir, '.credentials.json'), booleanContent);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
// Note: Current implementation uses JavaScript truthiness
|
|
// api_key: true is truthy, oauth_token: false is falsy
|
|
expect(indicators.hasCredentialsFile).toBe(true);
|
|
expect(indicators.credentials?.hasOAuthToken).toBe(false); // false is falsy
|
|
expect(indicators.credentials?.hasApiKey).toBe(true); // true is truthy
|
|
});
|
|
|
|
it('should handle malformed stats-cache.json gracefully', async () => {
|
|
await fs.writeFile(path.join(mockClaudeDir, 'stats-cache.json'), '{ invalid json }');
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasStatsCacheWithActivity).toBe(false);
|
|
expect(indicators.checks.statsCache.exists).toBe(true);
|
|
expect(indicators.checks.statsCache.error).toBeDefined();
|
|
});
|
|
|
|
it('should handle empty projects directory', async () => {
|
|
const projectsDir = path.join(mockClaudeDir, 'projects');
|
|
await fs.mkdir(projectsDir, { recursive: true });
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasProjectsSessions).toBe(false);
|
|
expect(indicators.checks.projectsDir.exists).toBe(true);
|
|
expect(indicators.checks.projectsDir.entryCount).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Combined authentication scenarios', () => {
|
|
it('should detect CLI authenticated state with settings + sessions', async () => {
|
|
// Create settings file
|
|
await fs.writeFile(
|
|
path.join(mockClaudeDir, 'settings.json'),
|
|
JSON.stringify({ theme: 'dark' })
|
|
);
|
|
|
|
// Create projects directory with sessions
|
|
const projectsDir = path.join(mockClaudeDir, 'projects');
|
|
await fs.mkdir(projectsDir, { recursive: true });
|
|
await fs.mkdir(path.join(projectsDir, 'session-1'));
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasSettingsFile).toBe(true);
|
|
expect(indicators.hasProjectsSessions).toBe(true);
|
|
});
|
|
|
|
it('should detect recent activity indicating working auth', async () => {
|
|
// Create stats cache with recent activity
|
|
await fs.writeFile(
|
|
path.join(mockClaudeDir, 'stats-cache.json'),
|
|
JSON.stringify({
|
|
dailyActivity: [{ date: new Date().toISOString().split('T')[0], messagesCount: 10 }],
|
|
})
|
|
);
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasStatsCacheWithActivity).toBe(true);
|
|
});
|
|
|
|
it('should handle complete auth setup', async () => {
|
|
// Create all auth indicators
|
|
await fs.writeFile(
|
|
path.join(mockClaudeDir, '.credentials.json'),
|
|
JSON.stringify({
|
|
claudeAiOauth: {
|
|
accessToken: 'token',
|
|
refreshToken: 'refresh',
|
|
},
|
|
})
|
|
);
|
|
await fs.writeFile(
|
|
path.join(mockClaudeDir, 'settings.json'),
|
|
JSON.stringify({ theme: 'dark' })
|
|
);
|
|
await fs.writeFile(
|
|
path.join(mockClaudeDir, 'stats-cache.json'),
|
|
JSON.stringify({ dailyActivity: [{ date: '2025-01-15', messagesCount: 5 }] })
|
|
);
|
|
const projectsDir = path.join(mockClaudeDir, 'projects');
|
|
await fs.mkdir(projectsDir, { recursive: true });
|
|
await fs.mkdir(path.join(projectsDir, 'session-1'));
|
|
|
|
const { getClaudeAuthIndicators } = await import('../src/system-paths');
|
|
const indicators = await getClaudeAuthIndicators();
|
|
|
|
expect(indicators.hasCredentialsFile).toBe(true);
|
|
expect(indicators.hasSettingsFile).toBe(true);
|
|
expect(indicators.hasStatsCacheWithActivity).toBe(true);
|
|
expect(indicators.hasProjectsSessions).toBe(true);
|
|
expect(indicators.credentials?.hasOAuthToken).toBe(true);
|
|
});
|
|
});
|
|
});
|