7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
326 lines
13 KiB
JavaScript
326 lines
13 KiB
JavaScript
/**
|
|
* API Key Inputs Test
|
|
* Tests the API key input fields on the research form for cloud providers
|
|
* CI-compatible: Works in both local and CI environments
|
|
*/
|
|
|
|
const puppeteer = require('puppeteer');
|
|
const AuthHelper = require('./auth_helper');
|
|
const { getPuppeteerLaunchOptions } = require('./puppeteer_config');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// API key provider configurations
|
|
const API_KEY_PROVIDERS = [
|
|
{ provider: 'OPENAI', containerId: 'openai_api_key_container', inputId: 'openai_api_key', settingKey: 'llm.openai.api_key' },
|
|
{ provider: 'ANTHROPIC', containerId: 'anthropic_api_key_container', inputId: 'anthropic_api_key', settingKey: 'llm.anthropic.api_key' },
|
|
{ provider: 'GOOGLE', containerId: 'google_api_key_container', inputId: 'google_api_key', settingKey: 'llm.google.api_key' },
|
|
{ provider: 'OPENROUTER', containerId: 'openrouter_api_key_container', inputId: 'openrouter_api_key', settingKey: 'llm.openrouter.api_key' },
|
|
{ provider: 'XAI', containerId: 'xai_api_key_container', inputId: 'xai_api_key', settingKey: 'llm.xai.api_key' },
|
|
{ provider: 'IONOS', containerId: 'ionos_api_key_container', inputId: 'ionos_api_key', settingKey: 'llm.ionos.api_key' },
|
|
{ provider: 'OPENAI_ENDPOINT', containerId: 'openai_endpoint_api_key_container', inputId: 'openai_endpoint_api_key', settingKey: 'llm.openai_endpoint.api_key' },
|
|
{ provider: 'OLLAMA', containerId: 'ollama_api_key_container', inputId: 'ollama_api_key', settingKey: 'llm.ollama.api_key' },
|
|
];
|
|
|
|
// Local providers that should NOT show API key fields (except Ollama which has optional key)
|
|
const LOCAL_PROVIDERS_NO_KEY = ['LMSTUDIO', 'LLAMACPP'];
|
|
|
|
async function testApiKeyInputs() {
|
|
const isCI = !!process.env.CI;
|
|
console.log(`\n🧪 Running API Key Inputs test (CI mode: ${isCI})\n`);
|
|
|
|
// Create screenshots directory if it doesn't exist
|
|
const screenshotsDir = path.join(__dirname, 'screenshots');
|
|
if (!fs.existsSync(screenshotsDir)) {
|
|
fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
}
|
|
|
|
const browser = await puppeteer.launch(getPuppeteerLaunchOptions());
|
|
const page = await browser.newPage();
|
|
const baseUrl = 'http://127.0.0.1:5000';
|
|
|
|
// Increase default timeout in CI
|
|
if (isCI) {
|
|
page.setDefaultTimeout(60000);
|
|
page.setDefaultNavigationTimeout(60000);
|
|
}
|
|
|
|
let testsPassed = 0;
|
|
let testsFailed = 0;
|
|
|
|
function logTest(name, passed, message = '') {
|
|
if (passed) {
|
|
console.log(` ✅ ${name}${message ? ': ' + message : ''}`);
|
|
testsPassed++;
|
|
} else {
|
|
console.log(` ❌ ${name}${message ? ': ' + message : ''}`);
|
|
testsFailed++;
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Authenticate
|
|
console.log('📋 Setup: Authenticating...');
|
|
const authHelper = new AuthHelper(page, baseUrl);
|
|
const testUser = {
|
|
username: `apikey_test_${Date.now()}`,
|
|
password: 'TestPass123!'
|
|
};
|
|
|
|
await authHelper.ensureAuthenticatedWithTimeout();
|
|
console.log('✅ Authenticated\n');
|
|
|
|
// Navigate to research page
|
|
console.log('📄 Navigating to research page...');
|
|
await page.goto(`${baseUrl}/`, {
|
|
waitUntil: 'networkidle2',
|
|
timeout: 30000
|
|
});
|
|
|
|
const currentUrl = page.url();
|
|
if (currentUrl.includes('/auth/login')) {
|
|
throw new Error('Redirected to login - not authenticated');
|
|
}
|
|
console.log('✅ Research page loaded\n');
|
|
|
|
// ===== TEST GROUP 1: API Key HTML Elements Exist =====
|
|
console.log('📋 Test Group 1: API Key HTML Elements Exist');
|
|
|
|
for (const config of API_KEY_PROVIDERS) {
|
|
const containerExists = await page.$(`#${config.containerId}`) !== null;
|
|
const inputExists = await page.$(`#${config.inputId}`) !== null;
|
|
logTest(
|
|
`${config.provider} API key elements`,
|
|
containerExists && inputExists,
|
|
containerExists && inputExists ? 'container and input found' : 'missing elements'
|
|
);
|
|
}
|
|
|
|
// ===== TEST GROUP 2: API Key Inputs Are Password Type =====
|
|
console.log('\n📋 Test Group 2: API Key Inputs Are Password Type');
|
|
|
|
for (const config of API_KEY_PROVIDERS) {
|
|
const inputType = await page.$eval(`#${config.inputId}`, el => el.type).catch(() => null);
|
|
logTest(
|
|
`${config.provider} input is password type`,
|
|
inputType === 'password',
|
|
inputType === 'password' ? 'correctly masked' : `got type: ${inputType}`
|
|
);
|
|
}
|
|
|
|
// ===== TEST GROUP 3: All API Key Containers Initially Hidden =====
|
|
console.log('\n📋 Test Group 3: API Key Containers Initially Hidden');
|
|
|
|
for (const config of API_KEY_PROVIDERS) {
|
|
const displayStyle = await page.$eval(`#${config.containerId}`, el => {
|
|
return window.getComputedStyle(el).display;
|
|
}).catch(() => 'error');
|
|
|
|
// Initially all should be hidden (display: none)
|
|
logTest(
|
|
`${config.provider} container initially hidden`,
|
|
displayStyle === 'none',
|
|
displayStyle === 'none' ? 'correctly hidden' : `display: ${displayStyle}`
|
|
);
|
|
}
|
|
|
|
// ===== TEST GROUP 4: Ensure Advanced Options Expanded =====
|
|
console.log('\n📋 Test Group 4: Ensure Advanced Options Expanded');
|
|
|
|
const advancedToggle = await page.$('.ldr-advanced-options-toggle');
|
|
if (advancedToggle) {
|
|
// Check if panel is already expanded (default open)
|
|
const panel = await page.$('.ldr-advanced-options-panel');
|
|
const isAlreadyExpanded = panel ? await page.evaluate(el => {
|
|
const style = window.getComputedStyle(el);
|
|
return style.visibility !== 'hidden' && style.opacity !== '0';
|
|
}, panel) : false;
|
|
if (!isAlreadyExpanded) {
|
|
await advancedToggle.click();
|
|
await page.waitForTimeout(500); // Wait for animation
|
|
}
|
|
logTest('Advanced options expanded', true, isAlreadyExpanded ? 'already open by default' : 'expanded via click');
|
|
} else {
|
|
logTest('Advanced options toggle', false, 'toggle not found');
|
|
}
|
|
|
|
// ===== TEST GROUP 5: Provider Selection Shows Correct API Key Field =====
|
|
console.log('\n📋 Test Group 5: Provider Selection Shows Correct API Key Field');
|
|
|
|
const providerSelect = await page.$('#model_provider');
|
|
if (!providerSelect) {
|
|
logTest('Provider select', false, 'provider dropdown not found');
|
|
} else {
|
|
for (const config of API_KEY_PROVIDERS) {
|
|
// Select the provider
|
|
await page.select('#model_provider', config.provider);
|
|
await page.waitForTimeout(300); // Wait for visibility toggle
|
|
|
|
// Check that this provider's API key container is visible
|
|
const isVisible = await page.$eval(`#${config.containerId}`, el => {
|
|
return window.getComputedStyle(el).display !== 'none';
|
|
}).catch(() => false);
|
|
|
|
logTest(
|
|
`${config.provider} shows API key field when selected`,
|
|
isVisible,
|
|
isVisible ? 'field visible' : 'field still hidden'
|
|
);
|
|
|
|
// Check that other API key containers are hidden
|
|
for (const otherConfig of API_KEY_PROVIDERS) {
|
|
if (otherConfig.provider !== config.provider) {
|
|
const otherHidden = await page.$eval(`#${otherConfig.containerId}`, el => {
|
|
return window.getComputedStyle(el).display === 'none';
|
|
}).catch(() => true);
|
|
|
|
if (!otherHidden) {
|
|
logTest(
|
|
`${otherConfig.provider} hidden when ${config.provider} selected`,
|
|
false,
|
|
'should be hidden but is visible'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===== TEST GROUP 6: Local Providers Don't Show API Key (except Ollama) =====
|
|
console.log('\n📋 Test Group 6: Local Providers Without API Key Fields');
|
|
|
|
for (const localProvider of LOCAL_PROVIDERS_NO_KEY) {
|
|
// Try to select the provider (may not exist in dropdown)
|
|
try {
|
|
await page.select('#model_provider', localProvider);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Check that no cloud API key containers are visible
|
|
let anyVisible = false;
|
|
for (const config of API_KEY_PROVIDERS) {
|
|
// Skip Ollama since it has optional API key
|
|
if (config.provider === 'OLLAMA') continue;
|
|
|
|
const isVisible = await page.$eval(`#${config.containerId}`, el => {
|
|
return window.getComputedStyle(el).display !== 'none';
|
|
}).catch(() => false);
|
|
|
|
if (isVisible) {
|
|
anyVisible = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
logTest(
|
|
`${localProvider} hides cloud API key fields`,
|
|
!anyVisible,
|
|
anyVisible ? 'some API key fields visible' : 'all hidden correctly'
|
|
);
|
|
} catch {
|
|
// Provider may not be in the dropdown, that's OK
|
|
console.log(` ⏭️ ${localProvider} provider not in dropdown, skipping`);
|
|
}
|
|
}
|
|
|
|
// ===== TEST GROUP 7: API Key Input Accepts Text =====
|
|
console.log('\n📋 Test Group 7: API Key Input Accepts Text');
|
|
|
|
// Select OpenAI provider
|
|
await page.select('#model_provider', 'OPENAI');
|
|
await page.waitForTimeout(300);
|
|
|
|
const testApiKey = 'sk-test-key-12345';
|
|
await page.type('#openai_api_key', testApiKey);
|
|
|
|
const inputValue = await page.$eval('#openai_api_key', el => el.value);
|
|
logTest(
|
|
'API key input accepts text',
|
|
inputValue === testApiKey,
|
|
inputValue === testApiKey ? 'value stored correctly' : `got: ${inputValue}`
|
|
);
|
|
|
|
// ===== TEST GROUP 8: API Key Persists After Provider Switch and Back =====
|
|
console.log('\n📋 Test Group 8: API Key Value Persistence');
|
|
|
|
// Switch to another provider
|
|
await page.select('#model_provider', 'ANTHROPIC');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Switch back to OpenAI
|
|
await page.select('#model_provider', 'OPENAI');
|
|
await page.waitForTimeout(300);
|
|
|
|
const persistedValue = await page.$eval('#openai_api_key', el => el.value);
|
|
logTest(
|
|
'API key persists after provider switch',
|
|
persistedValue === testApiKey,
|
|
persistedValue === testApiKey ? 'value preserved' : `got: ${persistedValue}`
|
|
);
|
|
|
|
// ===== TEST GROUP 9: API Key Save Handler Fires =====
|
|
console.log('\n📋 Test Group 9: API Key Save Handler');
|
|
|
|
// Listen for network requests to verify save
|
|
let saveRequestFired = false;
|
|
page.on('request', request => {
|
|
if (request.url().includes('/settings/api/') && request.method() === 'PUT') {
|
|
saveRequestFired = true;
|
|
}
|
|
});
|
|
|
|
// Clear and type a new value, then blur to trigger save
|
|
await page.$eval('#openai_api_key', el => { el.value = ''; });
|
|
await page.type('#openai_api_key', 'sk-new-test-key');
|
|
await page.click('#model_provider'); // Click elsewhere to trigger blur/change
|
|
await page.waitForTimeout(500);
|
|
|
|
logTest(
|
|
'API key save request fires on change',
|
|
saveRequestFired,
|
|
saveRequestFired ? 'save request detected' : 'no save request detected'
|
|
);
|
|
|
|
// Take a screenshot of the final state
|
|
await page.screenshot({
|
|
path: path.join(screenshotsDir, 'api_key_inputs_final.png'),
|
|
fullPage: true
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error(`\n❌ Test error: ${error.message}`);
|
|
|
|
// Take error screenshot
|
|
try {
|
|
await page.screenshot({
|
|
path: path.join(screenshotsDir, 'api_key_inputs_error.png'),
|
|
fullPage: true
|
|
});
|
|
} catch {
|
|
// Ignore screenshot errors
|
|
}
|
|
|
|
testsFailed++;
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
|
|
// Summary
|
|
console.log('\n' + '='.repeat(50));
|
|
console.log(`📊 Test Summary: ${testsPassed} passed, ${testsFailed} failed`);
|
|
console.log('='.repeat(50));
|
|
|
|
if (testsFailed > 0) {
|
|
console.log('\n❌ Some tests failed');
|
|
process.exit(1);
|
|
} else {
|
|
console.log('\n✅ All tests passed!');
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
// Run the tests
|
|
testApiKeyInputs().catch(error => {
|
|
console.error('Fatal error:', error);
|
|
process.exit(1);
|
|
});
|