Some checks are pending
HVAC Plugin CI/CD Pipeline / Security Analysis (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Code Quality & Standards (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Unit Tests (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Integration Tests (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Deploy to Staging (push) Blocked by required conditions
HVAC Plugin CI/CD Pipeline / Deploy to Production (push) Blocked by required conditions
HVAC Plugin CI/CD Pipeline / Notification (push) Blocked by required conditions
Security Monitoring & Compliance / Dependency Vulnerability Scan (push) Waiting to run
Security Monitoring & Compliance / Secrets & Credential Scan (push) Waiting to run
Security Monitoring & Compliance / WordPress Security Analysis (push) Waiting to run
Security Monitoring & Compliance / Static Code Security Analysis (push) Waiting to run
Security Monitoring & Compliance / Security Compliance Validation (push) Waiting to run
Security Monitoring & Compliance / Security Summary Report (push) Blocked by required conditions
Security Monitoring & Compliance / Security Team Notification (push) Blocked by required conditions
- Add 90+ test files including E2E, unit, and integration tests - Implement Page Object Model (POM) architecture - Add Docker testing environment with comprehensive services - Include modernized test framework with error recovery - Add specialized test suites for master trainer and trainer workflows - Update .gitignore to properly track test infrastructure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
517 lines
No EOL
18 KiB
JavaScript
517 lines
No EOL
18 KiB
JavaScript
/**
|
|
* 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(); |