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

375 lines
16 KiB
JavaScript

/**
* Change Password Form Validation Test
* Tests the change password form validation including:
* - CSS class consistency
* - Required field validation
* - Minlength validation
* - Password mismatch detection
* - Password strength indicator
* CI-compatible: Works in both local and CI environments
*/
const puppeteer = require('puppeteer');
const AuthHelper = require('./auth_helper');
const { Timer, CI_TEST_USER } = require('./auth_helper');
const { getPuppeteerLaunchOptions } = require('./puppeteer_config');
const fs = require('fs');
const path = require('path');
async function testChangePasswordValidation() {
const isCI = !!process.env.CI;
const testTimer = new Timer('test_change_password_validation');
console.log(`🧪 Running change password validation test (CI mode: ${isCI})`);
// 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;
// Test user credentials - ensureAuthenticated will use CI_TEST_USER or register with DEFAULT_TEST_USER password
const testUser = {
password: isCI ? CI_TEST_USER.password : 'T3st!Secure#2024$LDR' // DEFAULT_TEST_USER.password
};
console.log('🧪 Starting change password validation tests...\n');
try {
// IMPORTANT: This test must NOT use the shared CI_TEST_USER because it tests
// password change functionality. Even though we're only testing validation
// (mismatched passwords, same-as-current detection), using a dedicated user
// ensures we don't accidentally break other tests if validation logic changes.
// See AI Code Review concern about "Test Isolation Failure".
console.log('📋 Setup: Registering dedicated test user...');
const authHelper = new AuthHelper(page, baseUrl);
const dedicatedUsername = `changepwd_test_${Date.now()}`;
const dedicatedPassword = 'T3st!Secure#2024$LDR';
try {
// Always register a new user for this test to ensure isolation
await authHelper.register(dedicatedUsername, dedicatedPassword);
// Update testUser.password to match the registered user
testUser.password = dedicatedPassword;
console.log(`✅ Registered dedicated user: ${dedicatedUsername}\n`);
} catch (authError) {
console.log(`⚠️ Could not register test user: ${authError.message}`);
console.log(' Skipping change password tests - requires authenticated user\n');
await browser.close();
testTimer.summary();
console.log('❌ Test skipped - no test user available');
process.exit(1); // Auth failure should not be silent
}
// Navigate to change password page
// NOTE: Using 'domcontentloaded' instead of 'networkidle2' to avoid hangs
// caused by WebSocket/polling keeping the network active.
console.log('📄 Navigating to change password page...');
await page.goto(`${baseUrl}/auth/change-password`, {
waitUntil: 'domcontentloaded',
timeout: 30000
});
// Check if we were redirected (not authenticated)
const currentUrl = page.url();
if (currentUrl.includes('/auth/login')) {
console.log('⚠️ Redirected to login - user not authenticated');
console.log(' Attempting to login...');
await authHelper.login(dedicatedUsername, dedicatedPassword);
await page.goto(`${baseUrl}/auth/change-password`, {
waitUntil: 'domcontentloaded',
timeout: 30000
});
}
// Wait for the form - try multiple selectors
try {
// First wait for the page to have any form elements
await page.waitForFunction(() => {
const form = document.querySelector('form');
const input = document.querySelector('input[name="current_password"]');
return form && input;
}, { timeout: 15000 });
console.log('✅ Change password page loaded\n');
} catch {
console.log('⚠️ Change password page not accessible');
console.log(` Current URL: ${page.url()}`);
// Log page content for debugging
const pageContent = await page.evaluate(() => document.body.innerHTML.substring(0, 500));
console.log(` Page content preview: ${pageContent.substring(0, 200)}...`);
// Check if this is a server error (CI environment issue)
if (pageContent.includes('Server error') || pageContent.includes('error')) {
console.log('❌ Server error detected - cannot run change password tests');
await browser.close();
process.exit(1); // Infrastructure error should not be silent
}
throw new Error('Cannot access change password page');
}
// Test 1: Check CSS class consistency
console.log('📋 Test 1: CSS class consistency');
const currentPasswordInput = await page.$('input[name="current_password"]');
const currentPasswordClass = await page.evaluate(el => el.className, currentPasswordInput);
if (currentPasswordClass.includes('ldr-form-control')) {
console.log('✅ Current password input uses ldr-form-control class');
testsPassed++;
} else {
console.log(`❌ Current password input has incorrect class: "${currentPasswordClass}"`);
testsFailed++;
}
const newPasswordInput = await page.$('input[name="new_password"]');
const newPasswordClass = await page.evaluate(el => el.className, newPasswordInput);
if (newPasswordClass.includes('ldr-form-control')) {
console.log('✅ New password input uses ldr-form-control class');
testsPassed++;
} else {
console.log(`❌ New password input has incorrect class: "${newPasswordClass}"`);
testsFailed++;
}
const confirmPasswordInput = await page.$('input[name="confirm_password"]');
const confirmPasswordClass = await page.evaluate(el => el.className, confirmPasswordInput);
if (confirmPasswordClass.includes('ldr-form-control')) {
console.log('✅ Confirm password input uses ldr-form-control class');
testsPassed++;
} else {
console.log(`❌ Confirm password input has incorrect class: "${confirmPasswordClass}"`);
testsFailed++;
}
// Test 2: Required field validation - current password
console.log('\n📋 Test 2: Current password required validation');
let validity = await page.evaluate(el => ({
valid: el.validity.valid,
valueMissing: el.validity.valueMissing,
required: el.required
}), currentPasswordInput);
console.log(` Current password - required: ${validity.required}, valueMissing: ${validity.valueMissing}`);
if (validity.required && validity.valueMissing) {
console.log('✅ Current password is required and empty triggers valueMissing');
testsPassed++;
} else {
console.log('❌ Current password should be required');
testsFailed++;
}
// Test 3: New password minlength validation
console.log('\n📋 Test 3: New password minlength validation');
await page.type('input[name="new_password"]', 'short', { delay: 50 });
validity = await page.evaluate(el => ({
valid: el.validity.valid,
tooShort: el.validity.tooShort,
minLength: el.minLength
}), newPasswordInput);
console.log(` Value: "short" (5 chars), minLength: ${validity.minLength}, tooShort: ${validity.tooShort}`);
if (validity.tooShort) {
console.log('✅ New password too short correctly triggers tooShort validity');
testsPassed++;
} else {
console.log('❌ New password too short should trigger tooShort validity');
testsFailed++;
}
// Test 4: Password strength indicator
console.log('\n📋 Test 4: Password strength indicator');
// Clear and type a weak password (short, lowercase-only, no digit)
// Strength algorithm: +1 for length>=8, +1 for length>=12, +1 for lowercase, +1 for digit
// "abcdefg" scores 1 (lowercase only) → strength<=1 → weak
await page.evaluate(() => { document.querySelector('input[name="new_password"]').value = ''; });
await page.type('input[name="new_password"]', 'abcdefg', { delay: 50 });
await new Promise(resolve => setTimeout(resolve, 200));
const strengthBar = await page.$('#password-strength');
const strengthVisible = await page.evaluate(el => el.style.display !== 'none', strengthBar);
let strengthClasses = await page.evaluate(el => el.className, strengthBar);
console.log(` Weak password "abcdefg" - visible: ${strengthVisible}, classes: "${strengthClasses}"`);
if (strengthVisible) {
console.log('✅ Password strength indicator is visible');
testsPassed++;
} else {
console.log('❌ Password strength indicator should be visible');
testsFailed++;
}
if (strengthClasses.includes('ldr-strength-weak')) {
console.log('✅ Weak password shows ldr-strength-weak class');
testsPassed++;
} else {
console.log(`❌ Expected ldr-strength-weak class, got: "${strengthClasses}"`);
testsFailed++;
}
// Test strong password
await page.evaluate(() => { document.querySelector('input[name="new_password"]').value = ''; });
await page.type('input[name="new_password"]', 'StrongPass123!', { delay: 50 });
await new Promise(resolve => setTimeout(resolve, 200));
strengthClasses = await page.evaluate(el => el.className, strengthBar);
console.log(` Strong password - classes: "${strengthClasses}"`);
if (strengthClasses.includes('ldr-strength-strong')) {
console.log('✅ Strong password shows ldr-strength-strong class');
testsPassed++;
} else {
console.log(`❌ Expected ldr-strength-strong class, got: "${strengthClasses}"`);
testsFailed++;
}
// Test 5: Password mismatch detection
console.log('\n📋 Test 5: Password mismatch detection');
// Fill form with mismatched passwords
await page.evaluate(() => {
document.querySelector('input[name="current_password"]').value = '';
document.querySelector('input[name="new_password"]').value = '';
document.querySelector('input[name="confirm_password"]').value = '';
});
await page.type('input[name="current_password"]', testUser.password, { delay: 30 });
await page.type('input[name="new_password"]', 'NewPassword123!', { delay: 30 });
await page.type('input[name="confirm_password"]', 'DifferentPassword!', { delay: 30 });
// Set up dialog handler to catch the alert
let alertMessage = null;
page.once('dialog', async dialog => {
alertMessage = dialog.message();
await dialog.accept();
});
// Try to submit the form
await page.click('button[type="submit"]');
await new Promise(resolve => setTimeout(resolve, 1000));
if (alertMessage && alertMessage.toLowerCase().includes('match')) {
console.log(`✅ Password mismatch shows alert: "${alertMessage}"`);
testsPassed++;
} else {
console.log('❌ Password mismatch should show alert about non-matching passwords');
console.log(` Alert received: ${alertMessage}`);
testsFailed++;
}
// Test 6: Same password detection (new = current)
console.log('\n📋 Test 6: New password same as current detection');
// Clear and fill with same password
await page.evaluate(() => {
document.querySelector('input[name="current_password"]').value = '';
document.querySelector('input[name="new_password"]').value = '';
document.querySelector('input[name="confirm_password"]').value = '';
});
await page.type('input[name="current_password"]', testUser.password, { delay: 30 });
await page.type('input[name="new_password"]', testUser.password, { delay: 30 });
await page.type('input[name="confirm_password"]', testUser.password, { delay: 30 });
// Set up dialog handler
// Single test sequence; no concurrent writers to alertMessage.
// eslint-disable-next-line require-atomic-updates
alertMessage = null;
page.once('dialog', async dialog => {
alertMessage = dialog.message();
await dialog.accept();
});
// Try to submit the form
await page.click('button[type="submit"]');
await new Promise(resolve => setTimeout(resolve, 1000));
if (alertMessage && (alertMessage.toLowerCase().includes('different') || alertMessage.toLowerCase().includes('same'))) {
console.log(`✅ Same password shows alert: "${alertMessage}"`);
testsPassed++;
} else {
// Check if server rejected it
const afterUrl = page.url();
const errorAlert = await page.$('.alert');
if (afterUrl.includes('/auth/change-password') && errorAlert) {
const errorText = await page.evaluate(el => el.textContent, errorAlert);
if (errorText.toLowerCase().includes('different')) {
console.log(`✅ Server rejected same password: "${errorText.trim().substring(0, 50)}"`);
testsPassed++;
} else {
console.log(`⚠️ Alert shown but unexpected message: "${errorText.trim().substring(0, 50)}"`);
}
} else {
console.log('⚠️ Same password validation might be server-side only');
// Don't fail - this might be expected behavior
}
}
// Take a screenshot of the final state (skip in CI)
if (!isCI) {
await page.screenshot({
path: path.join(screenshotsDir, 'change_password_validation_test.png'),
fullPage: true
});
console.log('\n📸 Screenshot saved to screenshots/change_password_validation_test.png');
}
// Summary
console.log('\n' + '='.repeat(50));
console.log(`📊 Test Summary: ${testsPassed} passed, ${testsFailed} failed`);
console.log('='.repeat(50));
if (testsFailed > 0) {
throw new Error(`${testsFailed} test(s) failed`);
}
console.log('\n🎉 All change password validation tests passed!');
} catch (error) {
console.error('\n❌ Test failed:', error.message);
// Take error screenshot (skip in CI)
if (!isCI) {
try {
await page.screenshot({
path: path.join(screenshotsDir, 'change_password_validation_error.png'),
fullPage: true
});
console.log('📸 Error screenshot saved');
} catch (screenshotError) {
console.log('⚠️ Could not take error screenshot:', screenshotError.message);
}
}
await browser.close();
testTimer.summary();
process.exit(1);
}
await browser.close();
testTimer.summary();
console.log('\n✅ Test completed successfully');
}
// Run the test
testChangePasswordValidation().catch(console.error);