Files
openchamber/packages/web/server/lib/quota/providers/claude/auth.test.js
T

118 lines
4.1 KiB
JavaScript
Raw Normal View History

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const execFileSync = vi.fn();
const files = new Map();
const openCodeAuth = vi.fn(() => ({}));
vi.mock('child_process', () => ({ execFileSync: (...args) => execFileSync(...args) }));
vi.mock('fs', () => {
const fs = {
existsSync: (filePath) => files.has(filePath),
readFileSync: (filePath) => {
if (!files.has(filePath)) throw new Error('ENOENT');
return files.get(filePath);
},
};
return { ...fs, default: fs };
});
vi.mock('../../../opencode/auth.js', () => ({ readAuthFile: () => openCodeAuth() }));
import { loadClaudeCredential } from './auth.js';
const claudeCodeBlob = (accessToken) => JSON.stringify({
mcpOAuth: { 'linear|abc': { accessToken: 'unrelated-mcp-token' } },
claudeAiOauth: {
accessToken,
refreshToken: `${accessToken}-refresh`,
expiresAt: 1786735755912,
subscriptionType: 'max',
},
});
const withPlatform = (platform, run) => {
const original = Object.getOwnPropertyDescriptor(process, 'platform');
Object.defineProperty(process, 'platform', { value: platform, configurable: true });
try {
return run();
} finally {
Object.defineProperty(process, 'platform', original);
}
};
beforeEach(() => {
files.clear();
execFileSync.mockReset();
execFileSync.mockImplementation(() => { throw new Error('no keychain entry'); });
openCodeAuth.mockReturnValue({});
delete process.env.CLAUDE_CONFIG_DIR;
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
});
afterEach(() => {
delete process.env.CLAUDE_CONFIG_DIR;
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
});
describe('Claude credential discovery', () => {
it('prefers the macOS Keychain over a stale credentials file', () => {
execFileSync.mockReturnValue(claudeCodeBlob('keychain-token'));
files.set(`${process.env.HOME}/.claude/.credentials.json`, claudeCodeBlob('file-token'));
const credential = withPlatform('darwin', loadClaudeCredential);
expect(credential.accessToken).toBe('keychain-token');
expect(credential.refreshToken).toBe('keychain-token-refresh');
expect(credential.planLabel).toBe('max');
expect(credential.source).toBe('keychain');
});
it('reads the credentials file on Linux, where there is no Keychain', () => {
files.set(`${process.env.HOME}/.claude/.credentials.json`, claudeCodeBlob('file-token'));
const credential = withPlatform('linux', loadClaudeCredential);
expect(execFileSync).not.toHaveBeenCalled();
expect(credential.accessToken).toBe('file-token');
expect(credential.source).toBe('credentials-file');
});
it('honours CLAUDE_CONFIG_DIR when locating the credentials file', () => {
process.env.CLAUDE_CONFIG_DIR = '/tmp/claude-home';
files.set('/tmp/claude-home/.credentials.json', claudeCodeBlob('custom-dir-token'));
expect(withPlatform('linux', loadClaudeCredential).accessToken).toBe('custom-dir-token');
});
it('falls back to the OpenCode auth entry when Claude Code is not signed in', () => {
openCodeAuth.mockReturnValue({ anthropic: { access: 'opencode-token', refresh: 'opencode-refresh', expires: 1786735755912 } });
const credential = withPlatform('linux', loadClaudeCredential);
expect(credential.accessToken).toBe('opencode-token');
expect(credential.source).toBe('opencode-auth');
expect(credential.planLabel).toBeNull();
});
it('falls back to CLAUDE_CODE_OAUTH_TOKEN last, without a refresh token', () => {
process.env.CLAUDE_CODE_OAUTH_TOKEN = 'env-token';
const credential = withPlatform('linux', loadClaudeCredential);
expect(credential.accessToken).toBe('env-token');
expect(credential.refreshToken).toBeNull();
expect(credential.source).toBe('env');
});
it('ignores a Keychain blob that only holds unrelated MCP tokens', () => {
execFileSync.mockReturnValue(JSON.stringify({ mcpOAuth: { 'linear|abc': { accessToken: 'unrelated' } } }));
expect(withPlatform('darwin', loadClaudeCredential)).toBeNull();
});
it('returns null when every source is empty', () => {
expect(withPlatform('darwin', loadClaudeCredential)).toBeNull();
});
});