eigent/test/unit/electron/main/subscriptionAuth.test.ts
Tong Chen 6300702844
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Pre-commit / pre-commit (push) Has been cancelled
Test / Run Web + Local Brain Smoke (push) Has been cancelled
Test / Run Frontend Guardrails (push) Has been cancelled
Test / Run Python Tests (push) Has been cancelled
release: v1.0.1 (#1710)
Co-authored-by: Douglas <douglas.ym.lai@gmail.com>
Co-authored-by: Douglas Lai <115660088+Douglasymlai@users.noreply.github.com>
Co-authored-by: Tao Sun <168447269+fengju0213@users.noreply.github.com>
Co-authored-by: Weijie Bai <happy.regina.bai@gmail.com>
2026-06-27 18:14:29 +08:00

337 lines
11 KiB
TypeScript

// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Reversible fake for Electron safeStorage so the store can be exercised in a
// plain Node test environment.
let encryptionAvailable = true;
vi.mock('electron', () => ({
safeStorage: {
isEncryptionAvailable: () => encryptionAvailable,
encryptString: (s: string) => Buffer.from('enc:' + s, 'utf-8'),
decryptString: (b: Buffer) => b.toString('utf-8').replace(/^enc:/, ''),
},
}));
vi.mock('electron-log', () => ({
default: {
debug: vi.fn(),
error: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
},
}));
import {
DEFAULT_CODEX_OAUTH_AUTH_URL,
DEFAULT_CODEX_OAUTH_CLIENT_ID,
DEFAULT_CODEX_OAUTH_REDIRECT_URI,
DEFAULT_CODEX_OAUTH_SCOPES,
DEFAULT_CODEX_OAUTH_TOKEN_URL,
codexOAuthAuthUrl,
codexOAuthClientId,
codexOAuthRedirectUri,
codexOAuthScopes,
codexOAuthTokenUrl,
} from '../../../../electron/main/subscriptionAuth/codexOAuth';
import {
clearCodexCredential,
getCodexAccountStatus,
getCodexAuthFilePath,
loadCodexCredential,
saveCodexCredential,
} from '../../../../electron/main/subscriptionAuth/credentialStore';
import {
createPkcePair,
deriveCodeChallenge,
generateCodeVerifier,
} from '../../../../electron/main/subscriptionAuth/pkce';
import { ensureCodexResolverServer } from '../../../../electron/main/subscriptionAuth/resolverServer';
import type {
CodexCredential,
CodexResolverRuntime,
} from '../../../../electron/main/subscriptionAuth/types';
const BASE64URL = /^[A-Za-z0-9_-]+$/;
describe('pkce', () => {
it('generates a base64url verifier of >= 43 chars', () => {
const v = generateCodeVerifier();
expect(v).toMatch(BASE64URL);
expect(v.length).toBeGreaterThanOrEqual(43);
});
it('derives a stable S256 challenge for a known verifier', () => {
// RFC 7636 Appendix B test vector.
const challenge = deriveCodeChallenge(
'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'
);
expect(challenge).toBe('E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM');
});
it('produces unique verifiers and states across calls', () => {
const a = createPkcePair();
const b = createPkcePair();
expect(a.codeChallengeMethod).toBe('S256');
expect(a.codeVerifier).not.toBe(b.codeVerifier);
expect(a.state).not.toBe(b.state);
expect(a.codeChallenge).toBe(deriveCodeChallenge(a.codeVerifier));
});
});
describe('codexOAuth', () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it('uses OpenAI Codex OAuth defaults without app configuration', () => {
vi.stubEnv('CODEX_OAUTH_AUTH_URL', '');
vi.stubEnv('CODEX_OAUTH_TOKEN_URL', '');
vi.stubEnv('CODEX_OAUTH_CLIENT_ID', '');
vi.stubEnv('CODEX_OAUTH_SCOPES', '');
vi.stubEnv('CODEX_OAUTH_REDIRECT_URI', '');
expect(codexOAuthAuthUrl()).toBe(DEFAULT_CODEX_OAUTH_AUTH_URL);
expect(codexOAuthTokenUrl()).toBe(DEFAULT_CODEX_OAUTH_TOKEN_URL);
expect(codexOAuthClientId()).toBe(DEFAULT_CODEX_OAUTH_CLIENT_ID);
expect(codexOAuthScopes()).toBe(DEFAULT_CODEX_OAUTH_SCOPES);
expect(codexOAuthRedirectUri()).toBe(DEFAULT_CODEX_OAUTH_REDIRECT_URI);
});
});
describe('credentialStore', () => {
const email = 'User.Name@example.com';
let homeDir: string;
beforeEach(() => {
encryptionAvailable = true;
homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-auth-test-'));
vi.spyOn(os, 'homedir').mockReturnValue(homeDir);
});
afterEach(() => {
vi.restoreAllMocks();
fs.rmSync(homeDir, { recursive: true, force: true });
});
const sample: CodexCredential = {
access_token: 'access-123',
refresh_token: 'refresh-456',
token_type: 'Bearer',
expires_at: '2026-06-24T12:00:00Z',
account_label: 'user@example.com',
scopes: ['openid', 'profile'],
status: 'connected',
};
it('round-trips a credential through encrypted storage', () => {
saveCodexCredential(email, sample);
const loaded = loadCodexCredential(email);
expect(loaded?.access_token).toBe('access-123');
expect(loaded?.refresh_token).toBe('refresh-456');
expect(loaded?.account_label).toBe('user@example.com');
expect(loaded?.status).toBe('connected');
});
it('does not write the raw token to disk in plaintext', () => {
saveCodexCredential(email, sample);
const raw = fs.readFileSync(getCodexAuthFilePath(email), 'utf-8');
expect(raw).not.toContain('access-123');
expect(raw).not.toContain('refresh-456');
// Non-sensitive metadata is allowed in plaintext.
expect(raw).toContain('connected');
});
it('exposes a non-sensitive status view without token material', () => {
saveCodexCredential(email, sample);
const view = getCodexAccountStatus(email);
expect(view).toEqual({
connected: true,
status: 'connected',
account_label: 'user@example.com',
expires_at: '2026-06-24T12:00:00Z',
last_error_code: null,
});
expect(JSON.stringify(view)).not.toContain('access-123');
});
it('reports not_connected before anything is stored', () => {
expect(getCodexAccountStatus(email)).toEqual({
connected: false,
status: 'not_connected',
});
expect(loadCodexCredential(email)).toBeNull();
});
it('clears the credential on disconnect (idempotent)', () => {
saveCodexCredential(email, sample);
clearCodexCredential(email);
expect(loadCodexCredential(email)).toBeNull();
expect(getCodexAccountStatus(email).connected).toBe(false);
// second clear must not throw
expect(() => clearCodexCredential(email)).not.toThrow();
});
it('refuses to persist when OS encryption is unavailable', () => {
encryptionAvailable = false;
expect(() => saveCodexCredential(email, sample)).toThrow(/safeStorage/);
});
});
describe('resolverServer', () => {
const email = 'resolver@example.com';
let homeDir: string;
let resolver: CodexResolverRuntime | null = null;
beforeEach(() => {
encryptionAvailable = true;
homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-resolver-test-'));
vi.spyOn(os, 'homedir').mockReturnValue(homeDir);
});
afterEach(async () => {
if (resolver) {
await resolver.close();
resolver = null;
}
vi.unstubAllEnvs();
vi.restoreAllMocks();
fs.rmSync(homeDir, { recursive: true, force: true });
});
it('requires the shared secret', async () => {
resolver = await ensureCodexResolverServer();
const response = await fetch(`${resolver.url}/codex/token`, {
method: 'POST',
body: JSON.stringify({ email }),
});
expect(response.status).toBe(401);
const body = await response.json();
expect(body).toEqual({ error_code: 'unauthorized' });
});
it('returns a local token only to authorized backend callers', async () => {
saveCodexCredential(email, {
access_token: 'resolver-token',
refresh_token: null,
token_type: 'Bearer',
expires_at: null,
status: 'connected_non_refreshable',
});
resolver = await ensureCodexResolverServer();
const response = await fetch(`${resolver.url}/codex/token`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-eigent-resolver-secret': resolver.secret,
},
body: JSON.stringify({ email }),
});
expect(response.status).toBe(200);
const body = await response.json();
expect(body.access_token).toBe('resolver-token');
expect(body.token_type).toBe('Bearer');
expect(body.status).toBe('connected_non_refreshable');
});
it('refuses an expired non-refreshable token', async () => {
saveCodexCredential(email, {
access_token: 'expired-token',
refresh_token: null,
token_type: 'Bearer',
expires_at: '2000-01-01T00:00:00Z',
status: 'connected_non_refreshable',
});
resolver = await ensureCodexResolverServer();
const response = await fetch(`${resolver.url}/codex/token`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-eigent-resolver-secret': resolver.secret,
},
body: JSON.stringify({ email }),
});
expect(response.status).toBe(401);
const body = await response.json();
expect(body).toMatchObject({
error_code: 'token_expired',
status: 'expired',
});
expect(JSON.stringify(body)).not.toContain('expired-token');
});
it('refreshes an expired token when a refresh token is available', async () => {
vi.stubEnv('CODEX_OAUTH_TOKEN_URL', 'https://codex.example/token');
vi.stubEnv('CODEX_OAUTH_CLIENT_ID', 'codex-client');
const realFetch = globalThis.fetch.bind(globalThis);
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
if (String(input) === 'https://codex.example/token') {
return Promise.resolve(
new Response(
JSON.stringify({
access_token: 'fresh-token',
refresh_token: 'fresh-refresh',
token_type: 'Bearer',
expires_in: 3600,
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
}
)
);
}
return realFetch(input, init);
});
saveCodexCredential(email, {
access_token: 'expired-token',
refresh_token: 'old-refresh',
token_type: 'Bearer',
expires_at: '2000-01-01T00:00:00Z',
status: 'connected',
});
resolver = await ensureCodexResolverServer();
const response = await fetch(`${resolver.url}/codex/token`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-eigent-resolver-secret': resolver.secret,
},
body: JSON.stringify({ email }),
});
expect(response.status).toBe(200);
const body = await response.json();
expect(body.access_token).toBe('fresh-token');
expect(fetchMock).toHaveBeenCalledWith(
'https://codex.example/token',
expect.objectContaining({ method: 'POST' })
);
const saved = loadCodexCredential(email);
expect(saved?.access_token).toBe('fresh-token');
expect(saved?.refresh_token).toBe('fresh-refresh');
});
});