7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
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
507 lines
20 KiB
JavaScript
507 lines
20 KiB
JavaScript
const puppeteer = require('puppeteer');
|
||
const AuthHelper = require('./auth_helper');
|
||
const { getPuppeteerLaunchOptions } = require('./puppeteer_config');
|
||
|
||
// Color codes for terminal output
|
||
const colors = {
|
||
reset: '\x1b[0m',
|
||
bright: '\x1b[1m',
|
||
red: '\x1b[31m',
|
||
green: '\x1b[32m',
|
||
yellow: '\x1b[33m',
|
||
blue: '\x1b[34m',
|
||
magenta: '\x1b[35m'
|
||
};
|
||
|
||
function log(message, type = 'info') {
|
||
const timestamp = new Date().toISOString();
|
||
const prefix = {
|
||
info: `${colors.blue}ℹ`,
|
||
success: `${colors.green}✓`,
|
||
error: `${colors.red}✗`,
|
||
warn: `${colors.yellow}⚠`,
|
||
test: `${colors.magenta}▶`
|
||
}[type] || '';
|
||
|
||
console.log(`${prefix} [${timestamp}] ${message}${colors.reset}`);
|
||
}
|
||
|
||
// Test configuration
|
||
const TEST_CONFIG = {
|
||
baseUrl: 'http://localhost:5000',
|
||
apiKeys: {
|
||
initial: 'sk-test-initial-key-123456789',
|
||
changed: 'sk-test-changed-key-987654321',
|
||
final: 'sk-test-final-key-555555555'
|
||
},
|
||
timeout: 30000,
|
||
headless: process.env.SHOW_BROWSER !== '1',
|
||
slowMo: process.env.SHOW_BROWSER === '1' ? 100 : 0
|
||
};
|
||
|
||
// Main test function
|
||
async function runComprehensiveApiKeyTest() {
|
||
let browser;
|
||
let page;
|
||
let authHelper;
|
||
const timestamp = Date.now();
|
||
const testUsername = `api_key_test_${timestamp}`;
|
||
const testPassword = 'TestP@ssw0rd123';
|
||
const testResults = {
|
||
authentication: false,
|
||
settingsPageAccess: false,
|
||
apiKeyFieldFound: false,
|
||
enterKeySave: false,
|
||
apiSave: false,
|
||
persistence: false,
|
||
multipleChanges: false
|
||
};
|
||
const apiCalls = [];
|
||
|
||
try {
|
||
log(`${colors.bright}=== Starting Comprehensive API Key Settings Test ===${colors.reset}`, 'info');
|
||
log(`Test user: ${testUsername}`, 'info');
|
||
|
||
// Launch browser
|
||
browser = await puppeteer.launch(getPuppeteerLaunchOptions());
|
||
|
||
page = await browser.newPage();
|
||
await page.setViewport({ width: 1920, height: 1080 });
|
||
|
||
// Log browser console errors
|
||
page.on('console', msg => {
|
||
if (msg.type() === 'error') {
|
||
log(`Browser console: ${msg.text()}`, 'error');
|
||
}
|
||
});
|
||
|
||
// Set up request interception to monitor API calls
|
||
await page.setRequestInterception(true);
|
||
|
||
page.on('request', request => {
|
||
const url = request.url();
|
||
if (url.includes('/api/settings') || url.includes('/auth/')) {
|
||
apiCalls.push({
|
||
method: request.method(),
|
||
url,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
}
|
||
request.continue();
|
||
});
|
||
|
||
// Initialize auth helper
|
||
authHelper = new AuthHelper(page, TEST_CONFIG.baseUrl);
|
||
|
||
// Test 1: Authentication
|
||
log('Test 1: User Registration and Authentication', 'test');
|
||
try {
|
||
await authHelper.register(testUsername, testPassword);
|
||
const isAuthenticated = await authHelper.isLoggedIn();
|
||
if (isAuthenticated) {
|
||
testResults.authentication = true;
|
||
log('Authentication successful', 'success');
|
||
} else {
|
||
throw new Error('Failed to verify authentication');
|
||
}
|
||
} catch (error) {
|
||
log(`Authentication failed: ${error.message}`, 'error');
|
||
throw error;
|
||
}
|
||
|
||
// Test 2: Navigate to Settings Page
|
||
log('Test 2: Accessing Settings Page', 'test');
|
||
try {
|
||
await page.goto(`${TEST_CONFIG.baseUrl}/settings`, {
|
||
waitUntil: 'domcontentloaded',
|
||
timeout: TEST_CONFIG.timeout
|
||
});
|
||
|
||
// Verify we're on the settings page
|
||
const currentUrl = page.url();
|
||
if (!currentUrl.includes('/settings')) {
|
||
throw new Error(`Not on settings page. Current URL: ${currentUrl}`);
|
||
}
|
||
|
||
// Wait for settings to load
|
||
await page.waitForSelector('input, select, textarea', { timeout: 10000 });
|
||
testResults.settingsPageAccess = true;
|
||
log('Settings page loaded successfully', 'success');
|
||
|
||
// Take screenshot
|
||
await page.screenshot({
|
||
path: `/tmp/api_key_test_settings_${timestamp}.png`,
|
||
fullPage: true
|
||
});
|
||
log(`Screenshot saved: /tmp/api_key_test_settings_${timestamp}.png`, 'info');
|
||
} catch (error) {
|
||
log(`Failed to access settings page: ${error.message}`, 'error');
|
||
throw error;
|
||
}
|
||
|
||
// Test 3: Find API Key Field
|
||
log('Test 3: Locating OpenAI Endpoint API Key Field', 'test');
|
||
let apiKeyInput = null;
|
||
const apiKeySelectors = [
|
||
'input[name="llm.openai_endpoint.api_key"]',
|
||
'input[id="llm.openai_endpoint.api_key"]',
|
||
'input[data-setting="llm.openai_endpoint.api_key"]',
|
||
'#llm\\.openai_endpoint\\.api_key',
|
||
'input[type="password"][name*="openai"][name*="api_key"]',
|
||
'input[type="text"][name*="openai"][name*="api_key"]'
|
||
];
|
||
|
||
// First, check if we need to navigate to a specific section
|
||
const sections = await page.evaluate(() => {
|
||
const links = Array.from(document.querySelectorAll('a[href*="settings"], .nav-link, [role="tab"]'));
|
||
return links.map(link => ({
|
||
text: link.textContent.trim(),
|
||
href: link.href,
|
||
isActive: link.classList.contains('active')
|
||
}));
|
||
});
|
||
|
||
log(`Found ${sections.length} settings sections`, 'info');
|
||
|
||
// Look for LLM or API section
|
||
const llmSection = sections.find(s =>
|
||
s.text.toLowerCase().includes('llm') ||
|
||
s.text.toLowerCase().includes('api') ||
|
||
s.text.toLowerCase().includes('model')
|
||
);
|
||
|
||
if (llmSection && !llmSection.isActive) {
|
||
log(`Clicking on section: ${llmSection.text}`, 'info');
|
||
await page.evaluate((href) => {
|
||
const link = document.querySelector(`a[href="${href}"]`);
|
||
if (link) link.click();
|
||
}, llmSection.href);
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
}
|
||
|
||
// Try to find the API key field
|
||
for (const selector of apiKeySelectors) {
|
||
try {
|
||
apiKeyInput = await page.$(selector);
|
||
if (apiKeyInput) {
|
||
const isVisible = await apiKeyInput.evaluate(el => {
|
||
const rect = el.getBoundingClientRect();
|
||
return rect.width > 0 && rect.height > 0;
|
||
});
|
||
|
||
if (isVisible) {
|
||
log(`Found API key field with selector: ${selector}`, 'success');
|
||
testResults.apiKeyFieldFound = true;
|
||
break;
|
||
}
|
||
}
|
||
} catch {
|
||
// Continue with next selector
|
||
}
|
||
}
|
||
|
||
if (!apiKeyInput || !testResults.apiKeyFieldFound) {
|
||
// Try expanding collapsible sections
|
||
log('API key field not immediately visible, looking for expandable sections', 'info');
|
||
|
||
const expandables = await page.$$('[data-toggle="collapse"], .accordion-button, details summary');
|
||
for (const expandable of expandables) {
|
||
const text = await expandable.evaluate(el => el.textContent);
|
||
if (text.toLowerCase().includes('llm') || text.toLowerCase().includes('openai')) {
|
||
log(`Expanding section: ${text.trim()}`, 'info');
|
||
await expandable.click();
|
||
await new Promise(resolve => setTimeout(resolve, 500));
|
||
}
|
||
}
|
||
|
||
// Try finding the field again
|
||
for (const selector of apiKeySelectors) {
|
||
apiKeyInput = await page.$(selector);
|
||
if (apiKeyInput) {
|
||
const isVisible = await apiKeyInput.evaluate(el => el.offsetParent !== null);
|
||
if (isVisible) {
|
||
log(`Found API key field after expanding sections: ${selector}`, 'success');
|
||
testResults.apiKeyFieldFound = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!testResults.apiKeyFieldFound) {
|
||
throw new Error('Could not find OpenAI Endpoint API key field');
|
||
}
|
||
|
||
// Test 4: Enter Key Save Functionality
|
||
log('Test 4: Testing Enter Key Save (Individual Field)', 'test');
|
||
try {
|
||
// Wait for event handlers to be attached
|
||
log('Waiting for event handlers to be attached...', 'info');
|
||
await page.waitForFunction(() => {
|
||
const input = document.querySelector('input[name="llm.openai_endpoint.api_key"]');
|
||
if (!input) return false;
|
||
|
||
// Check if the input has data indicating handlers are attached
|
||
// The settings.js adds event listeners dynamically
|
||
return true; // For now, just wait a bit
|
||
}, { timeout: 5000 }).catch(() => {
|
||
log('Handler wait timed out', 'warn');
|
||
});
|
||
|
||
// Additional wait to ensure handlers are ready
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
|
||
// Focus the input first
|
||
await apiKeyInput.focus();
|
||
|
||
// Clear the field completely
|
||
await page.keyboard.down('Control');
|
||
await page.keyboard.press('a');
|
||
await page.keyboard.up('Control');
|
||
await page.keyboard.press('Backspace');
|
||
|
||
// Type the new value
|
||
await apiKeyInput.type(TEST_CONFIG.apiKeys.initial);
|
||
|
||
// Small delay to ensure typing is complete
|
||
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
||
// Get initial value
|
||
const valueBefore = await apiKeyInput.evaluate(el => el.value);
|
||
log(`Typed value: ${valueBefore}`, 'info');
|
||
|
||
// Press Enter to save
|
||
log('Pressing Enter to save...', 'info');
|
||
await page.keyboard.press('Enter');
|
||
|
||
// Wait for save operation to start and complete
|
||
// First wait for the saving class to appear
|
||
await page.waitForFunction(
|
||
() => {
|
||
const input = document.querySelector('input[name="llm.openai_endpoint.api_key"]');
|
||
const container = input?.closest('.form-group, .settings-item') || input;
|
||
return container?.classList.contains('saving');
|
||
},
|
||
{ timeout: 5000 }
|
||
).catch(() => log('No saving indicator detected', 'warn'));
|
||
|
||
// Then wait for save-success class or toast message
|
||
const successIndicators = await page.waitForFunction(
|
||
() => {
|
||
const indicators = [];
|
||
|
||
// Check for save-success class on input
|
||
const input = document.querySelector('input[name="llm.openai_endpoint.api_key"]');
|
||
if (input && input.classList.contains('save-success')) {
|
||
indicators.push('Input has save-success class');
|
||
}
|
||
|
||
// Check for toast notifications
|
||
const toastMessage = document.querySelector('.toast-message, .alert-success');
|
||
if (toastMessage && toastMessage.textContent.toLowerCase().includes('saved')) {
|
||
indicators.push(toastMessage.textContent.trim());
|
||
}
|
||
|
||
// Check if ui.showMessage was called (look for any success toast)
|
||
const toasts = document.querySelectorAll('[class*="toast"][class*="success"], .notification.success');
|
||
toasts.forEach(toast => {
|
||
if (toast.offsetParent !== null) { // Is visible
|
||
indicators.push('Success toast visible');
|
||
}
|
||
});
|
||
|
||
return indicators.length > 0 ? indicators : null;
|
||
},
|
||
{ timeout: 5000 }
|
||
).then(result => result.jsonValue())
|
||
.catch(() => []);
|
||
|
||
if (successIndicators.length > 0) {
|
||
log(`Success indicators found: ${successIndicators.join(', ')}`, 'success');
|
||
testResults.enterKeySave = true;
|
||
} else {
|
||
log('No immediate success indicators found, checking persistence...', 'warn');
|
||
}
|
||
|
||
// Verify by reloading the page
|
||
log('Reloading page to check persistence...', 'info');
|
||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||
|
||
// Find the field again
|
||
apiKeyInput = null;
|
||
for (const selector of apiKeySelectors) {
|
||
apiKeyInput = await page.$(selector);
|
||
if (apiKeyInput) break;
|
||
}
|
||
|
||
if (apiKeyInput) {
|
||
const valueAfterReload = await apiKeyInput.evaluate(el => el.value);
|
||
if (valueAfterReload && valueAfterReload.includes(TEST_CONFIG.apiKeys.initial.slice(-5))) {
|
||
log('Value persisted after reload!', 'success');
|
||
testResults.enterKeySave = true;
|
||
testResults.persistence = true;
|
||
} else {
|
||
log(`Value after reload: ${valueAfterReload || 'empty'}`, 'warn');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
log(`Enter key save test failed: ${error.message}`, 'error');
|
||
}
|
||
|
||
// Test 5: API-based Save
|
||
log('Test 5: Testing API-based Settings Save', 'test');
|
||
try {
|
||
const apiResponse = await page.evaluate(async (apiKey) => {
|
||
const response = await fetch('/settings/api/llm.openai_endpoint.api_key', {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
credentials: 'same-origin',
|
||
body: JSON.stringify({
|
||
value: apiKey
|
||
})
|
||
});
|
||
|
||
return {
|
||
ok: response.ok,
|
||
status: response.status,
|
||
data: await response.json()
|
||
};
|
||
}, TEST_CONFIG.apiKeys.changed);
|
||
|
||
if (apiResponse.ok) {
|
||
log(`API save successful: ${apiResponse.data.message || 'OK'}`, 'success');
|
||
testResults.apiSave = true;
|
||
|
||
// Verify the change
|
||
const verifyResponse = await page.evaluate(async () => {
|
||
const response = await fetch('/settings/api/llm.openai_endpoint.api_key', {
|
||
credentials: 'same-origin'
|
||
});
|
||
return await response.json();
|
||
});
|
||
|
||
const apiKeySetting = verifyResponse;
|
||
|
||
if (apiKeySetting && apiKeySetting.value) {
|
||
log(`API key verified via API: ***${apiKeySetting.value.slice(-5)}`, 'success');
|
||
}
|
||
} else {
|
||
log(`API save failed: ${apiResponse.status} - ${JSON.stringify(apiResponse.data)}`, 'error');
|
||
}
|
||
} catch (error) {
|
||
log(`API save test failed: ${error.message}`, 'error');
|
||
}
|
||
|
||
// Test 6: Multiple Changes and Final Verification
|
||
log('Test 6: Testing Multiple Changes', 'test');
|
||
try {
|
||
// Reload to get fresh state
|
||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||
|
||
// Find field again
|
||
apiKeyInput = null;
|
||
for (const selector of apiKeySelectors) {
|
||
apiKeyInput = await page.$(selector);
|
||
if (apiKeyInput) break;
|
||
}
|
||
|
||
if (apiKeyInput) {
|
||
// Make final change
|
||
await apiKeyInput.click({ clickCount: 3 });
|
||
await page.keyboard.press('Backspace');
|
||
await apiKeyInput.type(TEST_CONFIG.apiKeys.final);
|
||
|
||
// Try different save methods
|
||
log('Trying blur event (Tab)', 'info');
|
||
await page.keyboard.press('Tab');
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
|
||
log('Clicking elsewhere on page', 'info');
|
||
await page.click('body');
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
|
||
// Final verification
|
||
const finalCheck = await page.evaluate(async () => {
|
||
const response = await fetch('/settings/api/llm.openai_endpoint.api_key', {
|
||
credentials: 'same-origin'
|
||
});
|
||
return await response.json();
|
||
});
|
||
|
||
const finalApiKeySetting = finalCheck;
|
||
|
||
if (finalApiKeySetting && finalApiKeySetting.value) {
|
||
log(`Final API key value: ***${finalApiKeySetting.value.slice(-5)}`, 'info');
|
||
if (finalApiKeySetting.value.includes(TEST_CONFIG.apiKeys.final.slice(-5)) ||
|
||
finalApiKeySetting.value.includes(TEST_CONFIG.apiKeys.changed.slice(-5))) {
|
||
testResults.multipleChanges = true;
|
||
log('Multiple changes test passed', 'success');
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
log(`Multiple changes test failed: ${error.message}`, 'error');
|
||
}
|
||
|
||
// Take final screenshot
|
||
await page.screenshot({
|
||
path: `/tmp/api_key_test_final_${timestamp}.png`,
|
||
fullPage: false
|
||
});
|
||
log(`Final screenshot saved: /tmp/api_key_test_final_${timestamp}.png`, 'info');
|
||
|
||
} catch (error) {
|
||
log(`Test suite failed: ${error.message}`, 'error');
|
||
console.error(error);
|
||
|
||
// Take error screenshot
|
||
if (page) {
|
||
await page.screenshot({
|
||
path: `/tmp/api_key_test_error_${timestamp}.png`,
|
||
fullPage: true
|
||
});
|
||
log(`Error screenshot saved: /tmp/api_key_test_error_${timestamp}.png`, 'info');
|
||
}
|
||
} finally {
|
||
// Generate test report
|
||
log('\n' + colors.bright + '=== Test Results Summary ===' + colors.reset, 'info');
|
||
|
||
const totalTests = Object.keys(testResults).length;
|
||
const passedTests = Object.values(testResults).filter(v => v).length;
|
||
const failedTests = totalTests - passedTests;
|
||
|
||
for (const [test, result] of Object.entries(testResults)) {
|
||
const status = result ? `${colors.green}PASS${colors.reset}` : `${colors.red}FAIL${colors.reset}`;
|
||
const testName = test.replace(/([A-Z])/g, ' $1').trim();
|
||
log(`${testName}: ${status}`, result ? 'success' : 'error');
|
||
}
|
||
|
||
log(`\nTotal: ${totalTests} | Passed: ${passedTests} | Failed: ${failedTests}`, 'info');
|
||
|
||
// Log API calls made during test
|
||
if (apiCalls.length > 0) {
|
||
log('\n' + colors.bright + '=== API Calls Made ===' + colors.reset, 'info');
|
||
apiCalls.forEach(call => {
|
||
log(`${call.method} ${call.url}`, 'info');
|
||
});
|
||
}
|
||
|
||
// Clean up
|
||
if (browser) {
|
||
await browser.close();
|
||
log('Browser closed', 'info');
|
||
}
|
||
|
||
// Exit with appropriate code
|
||
process.exit(failedTests > 0 ? 1 : 0);
|
||
}
|
||
}
|
||
|
||
// Run the test
|
||
runComprehensiveApiKeyTest().catch(error => {
|
||
console.error('Unhandled error:', error);
|
||
process.exit(1);
|
||
});
|