Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

507 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});