255 lines
9.0 KiB
TypeScript
255 lines
9.0 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 AionUi (aionui.com)
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
parseApiKeys,
|
|
maskApiKey,
|
|
normalizeBaseUrl,
|
|
removeApiPathSuffix,
|
|
guessProtocolFromUrl,
|
|
guessProtocolFromKey,
|
|
identifyProviderFromKey,
|
|
getProtocolDisplayName,
|
|
getRecommendedPlatform,
|
|
} from '@/common/utils/protocolDetector';
|
|
|
|
describe('protocolDetector', () => {
|
|
describe('parseApiKeys', () => {
|
|
it('parses comma-separated keys', () => {
|
|
expect(parseApiKeys('key1,key2,key3')).toEqual(['key1', 'key2', 'key3']);
|
|
});
|
|
|
|
it('parses newline-separated keys', () => {
|
|
expect(parseApiKeys('key1\nkey2\nkey3')).toEqual(['key1', 'key2', 'key3']);
|
|
});
|
|
|
|
it('parses mixed separators', () => {
|
|
expect(parseApiKeys('key1,key2\nkey3')).toEqual(['key1', 'key2', 'key3']);
|
|
});
|
|
|
|
it('trims whitespace from keys', () => {
|
|
expect(parseApiKeys(' key1 , key2 \n key3 ')).toEqual(['key1', 'key2', 'key3']);
|
|
});
|
|
|
|
it('filters empty strings', () => {
|
|
expect(parseApiKeys('key1,,key2,,')).toEqual(['key1', 'key2']);
|
|
expect(parseApiKeys('key1\n\nkey2')).toEqual(['key1', 'key2']);
|
|
});
|
|
|
|
it('returns empty array for empty input', () => {
|
|
expect(parseApiKeys('')).toEqual([]);
|
|
expect(parseApiKeys(null as any)).toEqual([]);
|
|
expect(parseApiKeys(undefined as any)).toEqual([]);
|
|
});
|
|
|
|
it('handles single key', () => {
|
|
expect(parseApiKeys('single-key')).toEqual(['single-key']);
|
|
});
|
|
});
|
|
|
|
describe('maskApiKey', () => {
|
|
it('masks long API keys showing first and last 4 characters', () => {
|
|
expect(maskApiKey('sk-abc123def456ghi789')).toBe('sk-a...i789');
|
|
const geminiKey = 'AIza' + 'a'.repeat(35);
|
|
expect(maskApiKey(geminiKey)).toBe('AIza...aaaa');
|
|
});
|
|
|
|
it('returns *** for short keys (8 chars or less)', () => {
|
|
expect(maskApiKey('short')).toBe('***');
|
|
expect(maskApiKey('12345678')).toBe('***');
|
|
});
|
|
|
|
it('handles exact 9 character key', () => {
|
|
expect(maskApiKey('123456789')).toBe('1234...6789');
|
|
});
|
|
|
|
it('handles empty string', () => {
|
|
expect(maskApiKey('')).toBe('***');
|
|
});
|
|
});
|
|
|
|
describe('normalizeBaseUrl', () => {
|
|
it('removes trailing slashes', () => {
|
|
expect(normalizeBaseUrl('https://api.example.com/')).toBe('https://api.example.com');
|
|
expect(normalizeBaseUrl('https://api.example.com///')).toBe('https://api.example.com');
|
|
});
|
|
|
|
it('trims whitespace', () => {
|
|
expect(normalizeBaseUrl(' https://api.example.com ')).toBe('https://api.example.com');
|
|
});
|
|
|
|
it('preserves path', () => {
|
|
expect(normalizeBaseUrl('https://api.example.com/v1')).toBe('https://api.example.com/v1');
|
|
expect(normalizeBaseUrl('https://api.example.com/v1/')).toBe('https://api.example.com/v1');
|
|
});
|
|
|
|
it('returns empty string for empty input', () => {
|
|
expect(normalizeBaseUrl('')).toBe('');
|
|
expect(normalizeBaseUrl(null as any)).toBe('');
|
|
expect(normalizeBaseUrl(undefined as any)).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('removeApiPathSuffix', () => {
|
|
it('removes OpenAI path suffixes', () => {
|
|
expect(removeApiPathSuffix('https://api.example.com/v1/chat/completions')).toBe('https://api.example.com');
|
|
expect(removeApiPathSuffix('https://api.example.com/v1/models')).toBe('https://api.example.com');
|
|
});
|
|
|
|
it('removes Gemini path suffixes', () => {
|
|
expect(removeApiPathSuffix('https://generativelanguage.googleapis.com/v1beta/models')).toBe(
|
|
'https://generativelanguage.googleapis.com'
|
|
);
|
|
});
|
|
|
|
it('removes Anthropic path suffixes', () => {
|
|
expect(removeApiPathSuffix('https://api.anthropic.com/v1/messages')).toBe('https://api.anthropic.com');
|
|
});
|
|
|
|
it('matches longest suffix first', () => {
|
|
expect(removeApiPathSuffix('https://api.example.com/v1/chat/completions')).toBe('https://api.example.com');
|
|
});
|
|
|
|
it('returns null when no suffix matches', () => {
|
|
expect(removeApiPathSuffix('https://api.example.com/custom/endpoint')).toBeNull();
|
|
expect(removeApiPathSuffix('https://api.example.com')).toBeNull();
|
|
});
|
|
|
|
it('handles case-insensitive matching', () => {
|
|
expect(removeApiPathSuffix('https://api.example.com/V1/MODELS')).toBe('https://api.example.com');
|
|
});
|
|
|
|
it('returns null for empty input', () => {
|
|
expect(removeApiPathSuffix('')).toBeNull();
|
|
expect(removeApiPathSuffix(null as any)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('guessProtocolFromUrl', () => {
|
|
it('detects Gemini URLs', () => {
|
|
expect(guessProtocolFromUrl('https://generativelanguage.googleapis.com/v1')).toBe('gemini');
|
|
expect(guessProtocolFromUrl('https://aiplatform.googleapis.com/v1')).toBe('gemini');
|
|
});
|
|
|
|
it('detects OpenAI URLs', () => {
|
|
expect(guessProtocolFromUrl('https://api.openai.com/v1')).toBe('openai');
|
|
expect(guessProtocolFromUrl('https://api.deepseek.com/v1')).toBe('openai');
|
|
expect(guessProtocolFromUrl('https://openrouter.ai/v1')).toBe('openai');
|
|
});
|
|
|
|
it('detects Anthropic URLs', () => {
|
|
expect(guessProtocolFromUrl('https://api.anthropic.com/v1')).toBe('anthropic');
|
|
});
|
|
|
|
it('returns null for unknown URLs', () => {
|
|
expect(guessProtocolFromUrl('https://unknown.example.com/v1')).toBeNull();
|
|
});
|
|
|
|
it('handles case-insensitive matching', () => {
|
|
expect(guessProtocolFromUrl('HTTPS://API.OPENAI.COM/V1')).toBe('openai');
|
|
});
|
|
|
|
it('detects localhost URLs as OpenAI', () => {
|
|
expect(guessProtocolFromUrl('http://localhost:8000/v1')).toBe('openai');
|
|
expect(guessProtocolFromUrl('http://127.0.0.1:8000/v1')).toBe('openai');
|
|
});
|
|
});
|
|
|
|
describe('guessProtocolFromKey', () => {
|
|
it('detects OpenAI key format', () => {
|
|
expect(guessProtocolFromKey('sk-abc123def456ghi789jkl012')).toBe('openai');
|
|
expect(guessProtocolFromKey('sk-proj-abc123def456ghi789jkl012')).toBe('openai');
|
|
});
|
|
|
|
it('detects Gemini key format', () => {
|
|
const geminiKey = 'AIza' + 'a'.repeat(35);
|
|
expect(guessProtocolFromKey(geminiKey)).toBe('gemini');
|
|
});
|
|
|
|
it('detects Anthropic key format (matched as openai due to pattern order)', () => {
|
|
const anthropicKey = 'sk-ant-' + 'a'.repeat(80);
|
|
// Current implementation: OpenAI pattern matches first (sk-[A-Za-z0-9-_]{20,})
|
|
expect(guessProtocolFromKey(anthropicKey)).toBe('openai');
|
|
});
|
|
|
|
it('detects Groq key format', () => {
|
|
const groqKey = 'gsk_' + 'a'.repeat(52);
|
|
expect(guessProtocolFromKey(groqKey)).toBe('openai');
|
|
});
|
|
|
|
it('detects Perplexity key format', () => {
|
|
const pplxKey = 'pplx-' + 'a'.repeat(48);
|
|
expect(guessProtocolFromKey(pplxKey)).toBe('openai');
|
|
});
|
|
|
|
it('returns null for unknown key format', () => {
|
|
expect(guessProtocolFromKey('unknown-key-format')).toBeNull();
|
|
expect(guessProtocolFromKey('abc123')).toBeNull();
|
|
});
|
|
|
|
it('returns null for empty input', () => {
|
|
expect(guessProtocolFromKey('')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('identifyProviderFromKey', () => {
|
|
it('identifies OpenAI/Compatible provider', () => {
|
|
expect(identifyProviderFromKey('sk-abc123def456ghi789jkl012')).toBe('OpenAI/Compatible');
|
|
});
|
|
|
|
it('identifies Google/Gemini provider', () => {
|
|
const geminiKey = 'AIza' + 'a'.repeat(35);
|
|
expect(identifyProviderFromKey(geminiKey)).toBe('Google/Gemini');
|
|
});
|
|
|
|
it('identifies Anthropic provider (matched as OpenAI/Compatible due to pattern order)', () => {
|
|
const anthropicKey = 'sk-ant-' + 'a'.repeat(80);
|
|
// Current implementation: OpenAI pattern matches first in THIRD_PARTY_KEY_PATTERNS
|
|
expect(identifyProviderFromKey(anthropicKey)).toBe('OpenAI/Compatible');
|
|
});
|
|
|
|
it('identifies Groq provider', () => {
|
|
const groqKey = 'gsk_' + 'a'.repeat(52);
|
|
expect(identifyProviderFromKey(groqKey)).toBe('Groq');
|
|
});
|
|
|
|
it('identifies Perplexity provider', () => {
|
|
const pplxKey = 'pplx-' + 'a'.repeat(48);
|
|
expect(identifyProviderFromKey(pplxKey)).toBe('Perplexity');
|
|
});
|
|
|
|
it('returns null for unknown key', () => {
|
|
expect(identifyProviderFromKey('unknown-key')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('getProtocolDisplayName', () => {
|
|
it('returns display names for known protocols', () => {
|
|
expect(getProtocolDisplayName('openai')).toBe('OpenAI');
|
|
expect(getProtocolDisplayName('gemini')).toBe('Gemini');
|
|
expect(getProtocolDisplayName('anthropic')).toBe('Anthropic');
|
|
expect(getProtocolDisplayName('unknown')).toBe('Unknown');
|
|
});
|
|
|
|
it('returns protocol string for unmapped values', () => {
|
|
expect(getProtocolDisplayName('custom' as any)).toBe('custom');
|
|
});
|
|
});
|
|
|
|
describe('getRecommendedPlatform', () => {
|
|
it('returns recommended platforms', () => {
|
|
expect(getRecommendedPlatform('gemini')).toBe('gemini');
|
|
expect(getRecommendedPlatform('anthropic')).toBe('Anthropic');
|
|
});
|
|
|
|
it('returns null for protocols without recommendation', () => {
|
|
expect(getRecommendedPlatform('openai')).toBeNull();
|
|
expect(getRecommendedPlatform('unknown')).toBeNull();
|
|
});
|
|
});
|
|
});
|