/** * 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);