/** * Enhanced Authentication Manager for HVAC Testing Framework * * Provides comprehensive authentication management with: * - Storage state pre-generation and management * - Role-based authentication with WordPress integration * - Session persistence and validation * - Parallel test execution support with isolated auth states * * @package HVAC_Community_Events * @version 2.0.0 * @created 2025-08-27 */ const fs = require('fs').promises; const path = require('path'); const { chromium } = require('playwright'); const ConfigManager = require('./ConfigManager'); class AuthManager { /** * Authentication manager instance */ static instance = null; /** * Get authentication manager instance (Singleton pattern) */ static getInstance() { if (!AuthManager.instance) { AuthManager.instance = new AuthManager(); } return AuthManager.instance; } constructor() { this.config = ConfigManager; this.storageStatesDir = path.resolve(__dirname, '../../fixtures/storage-states'); this.authValidated = new Map(); // Cache for validated auth states this.initializeStorageDirectory(); } /** * Initialize storage states directory */ async initializeStorageDirectory() { try { await fs.mkdir(this.storageStatesDir, { recursive: true }); } catch (error) { console.warn('Could not create storage states directory:', error.message); } } /** * Pre-generate storage states for all user roles * This method should be called during test setup to create auth states */ async preGenerateStorageStates() { console.log('šŸ” Pre-generating authentication storage states...'); const roles = ['trainer', 'masterTrainer', 'admin']; const results = []; for (const role of roles) { try { console.log(` šŸ“‹ Generating storage state for: ${role}`); const storageState = await this.generateStorageState(role); results.push({ role, success: true, path: storageState }); console.log(` āœ… Generated: ${role}`); } catch (error) { console.error(` āŒ Failed to generate ${role}:`, error.message); results.push({ role, success: false, error: error.message }); } } // Summary const successful = results.filter(r => r.success).length; const failed = results.filter(r => !r.success).length; console.log(`\nšŸ“Š Storage state generation complete:`); console.log(` āœ… Successful: ${successful}`); console.log(` āŒ Failed: ${failed}`); if (failed > 0) { console.warn('Some storage states failed to generate. Tests may fall back to fresh login.'); } return results; } /** * Generate storage state for a specific user role */ async generateStorageState(role) { const browser = await chromium.launch(this.config.getBrowserConfig()); const context = await browser.newContext(); const page = await context.newPage(); try { // Get user configuration const userConfig = this.config.getUserConfig(role); if (!userConfig || !userConfig.username || !userConfig.password) { throw new Error(`Missing user configuration for role: ${role}`); } // Navigate to login page const loginUrl = this.getLoginUrl(role); await page.goto(loginUrl, { waitUntil: 'networkidle' }); // Perform login await this.performLogin(page, userConfig, role); // Verify authentication await this.verifyAuthentication(page, role); // Save storage state const storageStatePath = this.getStorageStatePath(role); await context.storageState({ path: storageStatePath }); console.log(`šŸ’¾ Saved storage state: ${storageStatePath}`); return storageStatePath; } finally { await browser.close(); } } /** * Load authentication storage state for user role */ async loadStorageState(role, context) { const storageStatePath = this.getStorageStatePath(role); try { // Check if storage state file exists and is recent const stats = await fs.stat(storageStatePath); const ageHours = (Date.now() - stats.mtime) / (1000 * 60 * 60); if (ageHours > 24) { console.log(`šŸ”„ Storage state for ${role} is ${ageHours.toFixed(1)} hours old, regenerating...`); return await this.generateStorageState(role); } // Load storage state await context.addInitScript(` // Set storage state metadata window.HVAC_AUTH_ROLE = '${role}'; window.HVAC_AUTH_LOADED = true; `); const storageState = JSON.parse(await fs.readFile(storageStatePath, 'utf8')); await context.addCookies(storageState.cookies || []); console.log(`šŸ“‚ Loaded storage state for: ${role}`); return storageStatePath; } catch (error) { console.log(`āš ļø Could not load storage state for ${role}: ${error.message}`); console.log('Falling back to fresh authentication...'); return null; } } /** * Validate that authentication storage state is still valid */ async validateStorageState(page, role) { // Check cache first const cacheKey = `${role}-${this.getStorageStatePath(role)}`; if (this.authValidated.has(cacheKey)) { const cached = this.authValidated.get(cacheKey); const ageMinutes = (Date.now() - cached.timestamp) / (1000 * 60); if (ageMinutes < 5) { // Cache for 5 minutes return cached.valid; } } try { await this.verifyAuthentication(page, role); this.authValidated.set(cacheKey, { valid: true, timestamp: Date.now() }); return true; } catch (error) { this.authValidated.set(cacheKey, { valid: false, timestamp: Date.now() }); console.log(`āŒ Storage state validation failed for ${role}: ${error.message}`); return false; } } /** * Authenticate user with storage state management */ async authenticate(page, role, options = {}) { const context = page.context(); // Try to load existing storage state if (!options.forceLogin) { const storageStatePath = await this.loadStorageState(role, context); if (storageStatePath) { // Navigate to expected page and validate const defaultPage = this.config.getUserConfig(role).defaultPage; await page.goto(`${this.config.get('app.baseUrl')}${defaultPage}`); if (await this.validateStorageState(page, role)) { console.log(`āœ… Authentication restored for ${role} using storage state`); return { method: 'storage-state', role, valid: true }; } } } // Fall back to fresh login console.log(`šŸ”‘ Performing fresh login for: ${role}`); return await this.freshLogin(page, role, options); } /** * Perform fresh login and optionally save storage state */ async freshLogin(page, role, options = {}) { const userConfig = this.config.getUserConfig(role); if (!userConfig || !userConfig.username || !userConfig.password) { throw new Error(`Missing user configuration for role: ${role}`); } // Navigate to login page const loginUrl = this.getLoginUrl(role); await page.goto(loginUrl, { waitUntil: 'networkidle' }); // Perform login await this.performLogin(page, userConfig, role); // Verify authentication await this.verifyAuthentication(page, role); // Save storage state for future use if (!options.skipStorageStateSave) { try { const storageStatePath = this.getStorageStatePath(role); await page.context().storageState({ path: storageStatePath }); console.log(`šŸ’¾ Updated storage state for: ${role}`); } catch (error) { console.warn('Could not save storage state:', error.message); } } console.log(`āœ… Fresh login successful for: ${role}`); return { method: 'fresh-login', role, valid: true }; } /** * Perform login on page with user configuration */ async performLogin(page, userConfig, role) { // Wait for login form await page.waitForSelector('input[type="email"], input[name="log"], #user_login', { timeout: 10000 }); // Fill email/username const emailSelectors = [ 'input[type="email"]', 'input[name="log"]', '#user_login', 'input[name="username"]' ]; let emailFilled = false; for (const selector of emailSelectors) { try { const field = page.locator(selector); if (await field.isVisible()) { await field.fill(userConfig.username || userConfig.email); emailFilled = true; break; } } catch (error) { // Continue to next selector } } if (!emailFilled) { throw new Error('Could not find email/username field'); } // Fill password const passwordSelectors = [ 'input[type="password"]', 'input[name="pwd"]', '#user_pass', 'input[name="password"]' ]; let passwordFilled = false; for (const selector of passwordSelectors) { try { const field = page.locator(selector); if (await field.isVisible()) { await field.fill(userConfig.password); passwordFilled = true; break; } } catch (error) { // Continue to next selector } } if (!passwordFilled) { throw new Error('Could not find password field'); } // Submit form const submitSelectors = [ 'button[type="submit"]', 'input[type="submit"]', '#wp-submit', '.login-submit button', '.submit input' ]; let submitted = false; for (const selector of submitSelectors) { try { const button = page.locator(selector); if (await button.isVisible()) { await Promise.all([ page.waitForURL(url => !url.includes('/wp-login.php') && !url.includes('/training-login/'), { timeout: this.config.get('framework.timeout') } ), button.click() ]); submitted = true; break; } } catch (error) { // Continue to next selector } } if (!submitted) { throw new Error('Could not find or click submit button'); } // Wait for page to load await page.waitForLoadState('networkidle', { timeout: 10000 }); } /** * Verify authentication for user role */ async verifyAuthentication(page, role) { const userConfig = this.config.getUserConfig(role); const expectedPage = userConfig.defaultPage; // Check if we're on the expected page or similar const currentUrl = page.url(); // Role-specific verification switch (role) { case 'trainer': if (!currentUrl.includes('/trainer/')) { // Navigate to trainer dashboard await page.goto(`${this.config.get('app.baseUrl')}/trainer/dashboard/`); } await page.waitForSelector('.hvac-trainer-nav, .trainer-dashboard, h1:has-text("Dashboard")', { timeout: 10000 }); break; case 'masterTrainer': if (!currentUrl.includes('/master-trainer/')) { // Navigate to master trainer dashboard await page.goto(`${this.config.get('app.baseUrl')}/master-trainer/master-dashboard/`); } await page.waitForSelector('.hvac-master-dashboard, .master-trainer-nav, h1:has-text("Master Dashboard")', { timeout: 10000 }); break; case 'admin': if (!currentUrl.includes('/wp-admin/')) { // Navigate to WordPress admin await page.goto(`${this.config.get('app.baseUrl')}/wp-admin/`); } await page.waitForSelector('#wpadminbar, .wp-admin, #wpbody', { timeout: 10000 }); break; default: throw new Error(`Unknown role: ${role}`); } // Check for login error indicators const errorSelectors = [ '.error', '.login-error', '#login_error', '.error-message', 'text=Invalid username or password' ]; for (const selector of errorSelectors) { if (await page.locator(selector).isVisible().catch(() => false)) { throw new Error('Login failed - error message visible'); } } } /** * Get login URL for role */ getLoginUrl(role) { const baseUrl = this.config.get('app.baseUrl'); if (role === 'admin') { return `${baseUrl}${this.config.get('wordpress.loginPath')}`; } else { return `${baseUrl}${this.config.get('wordpress.customLoginPath')}`; } } /** * Get storage state file path for role */ getStorageStatePath(role) { const environment = this.config.getEnvironment(); return path.join(this.storageStatesDir, `${role}-${environment}.json`); } /** * Clear all storage states */ async clearStorageStates() { try { const files = await fs.readdir(this.storageStatesDir); const jsonFiles = files.filter(f => f.endsWith('.json')); for (const file of jsonFiles) { await fs.unlink(path.join(this.storageStatesDir, file)); } console.log(`šŸ—‘ļø Cleared ${jsonFiles.length} storage state files`); } catch (error) { console.warn('Could not clear storage states:', error.message); } } /** * Clear validation cache */ clearValidationCache() { this.authValidated.clear(); console.log('šŸ—‘ļø Cleared authentication validation cache'); } /** * Get authentication status summary */ async getAuthStatus() { const roles = ['trainer', 'masterTrainer', 'admin']; const status = {}; for (const role of roles) { const storageStatePath = this.getStorageStatePath(role); try { const stats = await fs.stat(storageStatePath); const ageHours = (Date.now() - stats.mtime) / (1000 * 60 * 60); status[role] = { exists: true, path: storageStatePath, ageHours: ageHours.toFixed(2), fresh: ageHours < 1 }; } catch (error) { status[role] = { exists: false, path: storageStatePath, error: error.message }; } } return status; } /** * Logout current user */ async logout(page) { const logoutSelectors = [ 'a[href*="logout"]', 'text=Logout', 'text=Log Out', 'text=Sign Out', '.logout-link' ]; for (const selector of logoutSelectors) { try { const logoutLink = page.locator(selector); if (await logoutLink.isVisible()) { await logoutLink.click(); await page.waitForURL(url => url.includes('/wp-login.php') || url.includes('/training-login/') || url.includes('/logged-out'), { timeout: 5000 } ); console.log('āœ… Successfully logged out'); return; } } catch (error) { // Continue to next selector } } // Fallback: clear cookies and navigate to login await page.context().clearCookies(); await page.goto(`${this.config.get('app.baseUrl')}/wp-login.php`); console.log('āš ļø Forced logout by clearing cookies'); } } // Export singleton instance module.exports = AuthManager.getInstance();