/** * ================================================================ * E2E Test Authentication Helper * ================================================================ * Provides unified login for 8 roles: parent, child, teacher, * nutritionist, butler, assessor, admin, vendor * * Usage: * const { ROLES, loginAs } = require('./helpers/auth'); * const auth = await loginAs(browser, 'parent'); * console.log(auth.token, auth.userId, auth.role); * ================================================================ */ var BASE_URL = process.env.PW_BASE_URL || 'http://cfc.iwintrue.com'; // ================================================================ // DB helpers - insert verification codes directly // (Bypasses send-code endpoint which throws when testMode=false) // ================================================================ var child_process = require('child_process'); function insertVerificationCode(phone) { var mysqlHost = process.env.MYSQL_HOST || '192.168.16.251'; var mysqlUser = process.env.MYSQL_USER || 'zxyj'; var mysqlPass = process.env.MYSQL_PASS || 'zxyj@123'; var mysqlDb = process.env.MYSQL_DB || 'zxyj'; var type = phone === '13800138000' ? 'admin_login' : 'login'; try { child_process.execSync( 'mysql -h ' + mysqlHost + ' -u ' + mysqlUser + ' -p"' + mysqlPass + '" ' + mysqlDb + ' -e "INSERT INTO verification_codes (phone, code, type, expires_in, created_at, expires_at, used) VALUES (\'' + phone + '\', \'123456\', \'' + type + '\', 300, NOW(), \'2026-07-28 23:59:59\', 0);"', { stdio: 'ignore', shell: true, timeout: 10000 } ); } catch (e) { // ignore insert failures - the API call will fail with a clear error } } // ================================================================ // ROLES - Test user credentials mapping // ================================================================ var ROLES = { parent: { phone: '13701366188', userId: 1002, loginType: 'code', familyId: 2001 }, child: { phone: '13701366189', userId: 1003, loginType: 'code', familyId: 2001 }, teacher: { phone: '13800000001', userId: 1005, loginType: 'code', familyId: 0 }, nutritionist: { phone: '13800000010', userId: 1006, loginType: 'code', familyId: 0 }, butler: { phone: '13800000020', userId: 1007, loginType: 'code', familyId: 0 }, assessor: { phone: '13800000030', userId: 1008, loginType: 'code', familyId: 0 }, admin: { phone: '13800138000', userId: 1001, loginType: 'password', familyId: 0 }, vendor: { phone: '13800138001', userId: 1009, loginType: 'code', familyId: 0 } }; // ================================================================ // loginAs - Unified authentication for Playwright E2E tests // ================================================================ /** * Login as a specific role and return auth information * @param {Browser} browser - Playwright Browser instance * @param {string} role - Role name (parent, child, teacher, etc.) * @returns {Promise<{token: string, userId: number, role: string, familyId: number}>} */ async function loginAs(browser, role) { var context = null; var page = null; try { var roleConfig = ROLES[role]; if (!roleConfig) { throw new Error('Unknown role: ' + role + '. Available roles: ' + Object.keys(ROLES).join(', ')); } context = await browser.newContext(); page = await context.newPage(); var authResult = { token: null, userId: roleConfig.userId, role: role, familyId: roleConfig.familyId }; if (role === 'admin') { // Admin login via password-based API // The /api/admin-auth/login-by-password endpoint is public and accepts phone+password. // The admin user's password hash was pre-set in the DB. try { var apiResponse = await page.request.post(BASE_URL + '/api/admin-auth/login-by-password', { data: { phone: roleConfig.phone, password: 'admin123' }, headers: { 'Content-Type': 'application/json' } }); if (apiResponse.ok()) { var apiData = await apiResponse.json(); if (apiData && apiData.code === 200 && apiData.data && apiData.data.token) { authResult.token = apiData.data.token; } } } catch (apiErr) { // API login failed } } else { // Non-admin roles: try API login first, fallback to UI // Insert a fresh verification code directly into DB so the API login succeeds. // The backend's send-code endpoint throws UnsupportedOperationException when // testMode=false (SMS provider not configured), so we bypass it. insertVerificationCode(roleConfig.phone); // Attempt 1: API login try { var apiResponse = await page.request.post(BASE_URL + '/api/admin-auth/login', { data: { phone: roleConfig.phone, code: '123456' }, headers: { 'Content-Type': 'application/json' } }); if (apiResponse.ok()) { var apiData = await apiResponse.json(); // Backend returns HTTP 200 even on business errors; check body.code === 200 if (apiData && apiData.code === 200 && apiData.data && apiData.data.token) { authResult.token = apiData.data.token; // Store token in localStorage for UI consistency await page.evaluate(function(token) { localStorage.setItem('token', token); localStorage.setItem('access_token', token); }, authResult.token); } } } catch (apiErr) { // API login failed, will try UI fallback } // Fallback: UI login if API didn't work if (!authResult.token) { await page.goto(BASE_URL, { waitUntil: 'networkidle', timeout: 30000 }); await page.waitForTimeout(2000); // Try to find and click login button or go to login page var loginBtnSelectors = [ 'button:has-text("登录")', 'a:has-text("登录")', '.login-btn', '[class*="login"]' ]; for (var k = 0; k < loginBtnSelectors.length; k++) { try { var loginBtn = page.locator(loginBtnSelectors[k]).first(); if (await loginBtn.isVisible({ timeout: 2000 })) { await loginBtn.click(); await page.waitForTimeout(1000); break; } } catch (e) { // continue } } // Fill phone and code var phoneInputSelectors = [ 'input[type="tel"]', 'input[placeholder*="手机"]', 'input[name="phone"]', '.el-input__inner' ]; for (var m = 0; m < phoneInputSelectors.length; m++) { try { var phoneInput = page.locator(phoneInputSelectors[m]).first(); if (await phoneInput.isVisible({ timeout: 2000 })) { await phoneInput.fill(roleConfig.phone); break; } } catch (e) { // continue } } // Fill verification code (123456 for test) var codeInputSelectors = [ 'input[placeholder*="验证码"]', 'input[placeholder*="code"]', 'input[name="code"]' ]; for (var n = 0; n < codeInputSelectors.length; n++) { try { var codeInput = page.locator(codeInputSelectors[n]).first(); if (await codeInput.isVisible({ timeout: 2000 })) { await codeInput.fill('123456'); break; } } catch (e) { // continue } } // Click submit button var submitSelectors = [ 'button[type="submit"]', '.el-button--primary', 'button:has-text("登录")', 'button:has-text("提交")' ]; for (var p = 0; p < submitSelectors.length; p++) { try { var submitBtn = page.locator(submitSelectors[p]).first(); if (await submitBtn.isVisible({ timeout: 2000 })) { await submitBtn.click(); break; } } catch (e) { // continue } } // Wait for navigation or token storage await page.waitForTimeout(3000); // Try to read token from localStorage authResult.token = await page.evaluate(function() { return localStorage.getItem('token') || localStorage.getItem('access_token') || ''; }); } } return authResult; } catch (error) { console.error('Login failed for role ' + role + ':', error.message); // Return partial result even on error return { token: null, userId: roleConfig ? roleConfig.userId : null, role: role, familyId: roleConfig ? roleConfig.familyId : null, error: error.message }; } finally { // Cleanup: close the browser context if (context) { try { await context.close(); } catch (closeErr) { // Ignore close errors } } } } // ================================================================ // Module exports (CommonJS) // ================================================================ module.exports = { ROLES: ROLES, loginAs: loginAs, BASE_URL: BASE_URL };