diff --git a/apps/frontend/src/main/claude-profile/profile-utils.test.ts b/apps/frontend/src/main/claude-profile/profile-utils.test.ts new file mode 100644 index 00000000..2c090d83 --- /dev/null +++ b/apps/frontend/src/main/claude-profile/profile-utils.test.ts @@ -0,0 +1,108 @@ +/** + * Tests for profile-utils module + */ + +import { describe, it, expect } from 'vitest'; +import { isAPIProfileAuthenticated } from './profile-utils'; +import type { APIProfile } from '../../shared/types'; + +describe('isAPIProfileAuthenticated', () => { + it('should return true when both apiKey and baseUrl are present and non-empty', () => { + const validProfile: APIProfile = { + id: 'test-1', + name: 'Test Profile', + baseUrl: 'https://api.anthropic.com', + apiKey: 'sk-ant-api03-test', + createdAt: Date.now(), + updatedAt: Date.now(), + }; + + expect(isAPIProfileAuthenticated(validProfile)).toBe(true); + }); + + it('should return false when apiKey is missing', () => { + const profileWithoutApiKey: APIProfile = { + id: 'test-2', + name: 'Test Profile', + baseUrl: 'https://api.anthropic.com', + apiKey: '', + createdAt: Date.now(), + updatedAt: Date.now(), + }; + + expect(isAPIProfileAuthenticated(profileWithoutApiKey)).toBe(false); + }); + + it('should return false when baseUrl is missing', () => { + const profileWithoutBaseUrl: APIProfile = { + id: 'test-3', + name: 'Test Profile', + baseUrl: '', + apiKey: 'sk-ant-api03-test', + createdAt: Date.now(), + updatedAt: Date.now(), + }; + + expect(isAPIProfileAuthenticated(profileWithoutBaseUrl)).toBe(false); + }); + + it('should return false when apiKey is only whitespace', () => { + const profileWithWhitespaceApiKey: APIProfile = { + id: 'test-4', + name: 'Test Profile', + baseUrl: 'https://api.anthropic.com', + apiKey: ' ', + createdAt: Date.now(), + updatedAt: Date.now(), + }; + + expect(isAPIProfileAuthenticated(profileWithWhitespaceApiKey)).toBe(false); + }); + + it('should return false when baseUrl is only whitespace', () => { + const profileWithWhitespaceBaseUrl: APIProfile = { + id: 'test-5', + name: 'Test Profile', + baseUrl: ' ', + apiKey: 'sk-ant-api03-test', + createdAt: Date.now(), + updatedAt: Date.now(), + }; + + expect(isAPIProfileAuthenticated(profileWithWhitespaceBaseUrl)).toBe(false); + }); + + it('should return false when both apiKey and baseUrl are missing', () => { + const profileWithoutCredentials: APIProfile = { + id: 'test-6', + name: 'Test Profile', + baseUrl: '', + apiKey: '', + createdAt: Date.now(), + updatedAt: Date.now(), + }; + + expect(isAPIProfileAuthenticated(profileWithoutCredentials)).toBe(false); + }); + + it('should return false when profile is undefined', () => { + expect(isAPIProfileAuthenticated(undefined as any)).toBe(false); + }); + + it('should return false when profile is null', () => { + expect(isAPIProfileAuthenticated(null as any)).toBe(false); + }); + + it('should handle profiles with apiKey and baseUrl containing leading/trailing whitespace', () => { + const profileWithWhitespace: APIProfile = { + id: 'test-7', + name: 'Test Profile', + baseUrl: ' https://api.anthropic.com ', + apiKey: ' sk-ant-api03-test ', + createdAt: Date.now(), + updatedAt: Date.now(), + }; + + expect(isAPIProfileAuthenticated(profileWithWhitespace)).toBe(true); + }); +}); diff --git a/apps/frontend/src/main/claude-profile/profile-utils.ts b/apps/frontend/src/main/claude-profile/profile-utils.ts index de32b1d5..c6799a6e 100644 --- a/apps/frontend/src/main/claude-profile/profile-utils.ts +++ b/apps/frontend/src/main/claude-profile/profile-utils.ts @@ -6,7 +6,7 @@ import { homedir } from 'os'; import { join } from 'path'; import { existsSync, readFileSync, readdirSync, mkdirSync } from 'fs'; -import type { ClaudeProfile } from '../../shared/types'; +import type { ClaudeProfile, APIProfile } from '../../shared/types'; import { getCredentialsFromKeychain } from './credential-utils'; /** @@ -203,6 +203,26 @@ export function hasValidToken(profile: ClaudeProfile): boolean { return true; } +/** + * Check if an API profile has valid authentication credentials. + * Validates that both apiKey and baseUrl are present and non-empty. + * + * @param profile - The API profile to check + * @returns true if the profile has both apiKey and baseUrl, false otherwise + */ +export function isAPIProfileAuthenticated(profile: APIProfile): boolean { + // Check for presence of required fields + if (!profile?.apiKey || !profile?.baseUrl) { + return false; + } + + // Validate that the fields are non-empty strings (after trimming whitespace) + const hasValidApiKey = typeof profile.apiKey === 'string' && profile.apiKey.trim().length > 0; + const hasValidBaseUrl = typeof profile.baseUrl === 'string' && profile.baseUrl.trim().length > 0; + + return hasValidApiKey && hasValidBaseUrl; +} + /** * Expand ~ in path to home directory */ diff --git a/apps/frontend/src/shared/types/index.ts b/apps/frontend/src/shared/types/index.ts index 9ac42f76..bb45441c 100644 --- a/apps/frontend/src/shared/types/index.ts +++ b/apps/frontend/src/shared/types/index.ts @@ -11,6 +11,7 @@ export * from './task'; export * from './kanban'; export * from './terminal'; export * from './agent'; +export * from './profile'; export * from './settings'; export * from './changelog'; export * from './insights'; diff --git a/apps/frontend/src/shared/types/profile.ts b/apps/frontend/src/shared/types/profile.ts index 44454143..6abe8799 100644 --- a/apps/frontend/src/shared/types/profile.ts +++ b/apps/frontend/src/shared/types/profile.ts @@ -1,3 +1,5 @@ +import type { ClaudeUsageData, ClaudeRateLimitEvent } from './agent'; + /** * API Profile Management Types * @@ -27,6 +29,10 @@ export interface APIProfile { }; createdAt: number; // Unix timestamp (ms) updatedAt: number; // Unix timestamp (ms) + /** Current usage data from API */ + usage?: ClaudeUsageData; + /** Recent rate limit events for this profile */ + rateLimitEvents?: ClaudeRateLimitEvent[]; } /**