Fix provider API key validation trimming (#810)
This commit is contained in:
committed by
GitHub
Unverified
parent
66b2ddb2dc
commit
49518300dc
@@ -62,4 +62,83 @@ test.describe('ClawX provider lifecycle', () => {
|
||||
await relaunchedApp.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('trims whitespace before validating and saving a custom provider key', async ({ electronApp, page }) => {
|
||||
await completeSetup(page);
|
||||
|
||||
await electronApp.evaluate(async ({ app: _app }) => {
|
||||
const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron');
|
||||
|
||||
let accounts: Array<Record<string, unknown>> = [];
|
||||
let statuses: Array<Record<string, unknown>> = [];
|
||||
let defaultAccountId: string | null = null;
|
||||
|
||||
const respond = (json: unknown, status = 200) => ({
|
||||
ok: true,
|
||||
data: {
|
||||
status,
|
||||
ok: status >= 200 && status < 300,
|
||||
json,
|
||||
},
|
||||
});
|
||||
|
||||
ipcMain.removeHandler('hostapi:fetch');
|
||||
ipcMain.handle('hostapi:fetch', async (_event: unknown, request: { path?: string; method?: string; body?: string | null }) => {
|
||||
const path = request?.path ?? '';
|
||||
const method = request?.method ?? 'GET';
|
||||
const body = request?.body ? JSON.parse(request.body) : null;
|
||||
|
||||
if (path === '/api/provider-accounts' && method === 'GET') return respond(accounts);
|
||||
if (path === '/api/providers' && method === 'GET') return respond(statuses);
|
||||
if (path === '/api/provider-vendors' && method === 'GET') return respond([]);
|
||||
if (path === '/api/provider-accounts/default' && method === 'GET') return respond({ accountId: defaultAccountId });
|
||||
|
||||
if (path === '/api/providers/validate' && method === 'POST') {
|
||||
if (body?.apiKey !== 'sk-lm-test') {
|
||||
return respond({ valid: false, error: `unexpected key: ${String(body?.apiKey)}` }, 400);
|
||||
}
|
||||
return respond({ valid: true });
|
||||
}
|
||||
|
||||
if (path === '/api/provider-accounts' && method === 'POST') {
|
||||
accounts = [body.account];
|
||||
statuses = [{
|
||||
id: body.account.id,
|
||||
name: body.account.label,
|
||||
type: body.account.vendorId,
|
||||
baseUrl: body.account.baseUrl,
|
||||
model: body.account.model,
|
||||
enabled: body.account.enabled,
|
||||
createdAt: body.account.createdAt,
|
||||
updatedAt: body.account.updatedAt,
|
||||
hasKey: Boolean(body.apiKey),
|
||||
keyMasked: body.apiKey ? 'sk-***' : null,
|
||||
}];
|
||||
return respond({ success: true });
|
||||
}
|
||||
|
||||
if (path === '/api/provider-accounts/default' && method === 'PUT') {
|
||||
defaultAccountId = body?.accountId ?? null;
|
||||
return respond({ success: true });
|
||||
}
|
||||
|
||||
return respond({});
|
||||
});
|
||||
});
|
||||
|
||||
await page.getByTestId('sidebar-nav-models').click();
|
||||
await expect(page.getByTestId('providers-settings')).toBeVisible();
|
||||
|
||||
await page.getByTestId('providers-add-button').click();
|
||||
await expect(page.getByTestId('add-provider-dialog')).toBeVisible();
|
||||
|
||||
await page.getByTestId('add-provider-type-custom').click();
|
||||
await page.getByTestId('add-provider-name-input').fill('LM Studio Local');
|
||||
await page.getByTestId('add-provider-api-key-input').fill(' sk-lm-test \n');
|
||||
await page.getByTestId('add-provider-base-url-input').fill('http://127.0.0.1:1234/v1');
|
||||
await page.getByTestId('add-provider-model-id-input').fill('local-model');
|
||||
await page.getByTestId('add-provider-submit-button').click();
|
||||
|
||||
await expect(page.getByTestId('provider-card-custom')).toContainText('LM Studio Local');
|
||||
});
|
||||
});
|
||||
|
||||
42
tests/unit/provider-store-validation.test.ts
Normal file
42
tests/unit/provider-store-validation.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mockFetchProviderSnapshot = vi.fn();
|
||||
const mockHostApiFetch = vi.fn();
|
||||
|
||||
vi.mock('@/lib/provider-accounts', () => ({
|
||||
fetchProviderSnapshot: (...args: unknown[]) => mockFetchProviderSnapshot(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/host-api', () => ({
|
||||
hostApiFetch: (...args: unknown[]) => mockHostApiFetch(...args),
|
||||
}));
|
||||
|
||||
import { useProviderStore } from '@/stores/providers';
|
||||
|
||||
describe('useProviderStore – validateAccountApiKey()', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('trims API keys before sending provider validation requests', async () => {
|
||||
mockHostApiFetch.mockResolvedValueOnce({ valid: true });
|
||||
|
||||
const result = await useProviderStore.getState().validateAccountApiKey('custom', ' sk-lm-test \n', {
|
||||
baseUrl: 'http://127.0.0.1:1234/v1',
|
||||
apiProtocol: 'openai-completions',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ valid: true });
|
||||
expect(mockHostApiFetch).toHaveBeenCalledWith('/api/providers/validate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
providerId: 'custom',
|
||||
apiKey: 'sk-lm-test',
|
||||
options: {
|
||||
baseUrl: 'http://127.0.0.1:1234/v1',
|
||||
apiProtocol: 'openai-completions',
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
normalizeProviderApiKeyInput,
|
||||
PROVIDER_TYPES,
|
||||
PROVIDER_TYPE_INFO,
|
||||
getProviderDocsUrl,
|
||||
@@ -171,6 +172,7 @@ describe('provider metadata', () => {
|
||||
});
|
||||
|
||||
it('normalizes provider API keys for save flow', () => {
|
||||
expect(normalizeProviderApiKeyInput(' sk-test \n')).toBe('sk-test');
|
||||
expect(resolveProviderApiKeyForSave('ollama', '')).toBe('ollama-local');
|
||||
expect(resolveProviderApiKeyForSave('ollama', ' ')).toBe('ollama-local');
|
||||
expect(resolveProviderApiKeyForSave('ollama', 'real-key')).toBe('real-key');
|
||||
|
||||
Reference in New Issue
Block a user