auth.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. /**
  2. * ================================================================
  3. * E2E Test Authentication Helper
  4. * ================================================================
  5. * Provides unified login for 8 roles: parent, child, teacher,
  6. * nutritionist, butler, assessor, admin, vendor
  7. *
  8. * Usage:
  9. * const { ROLES, loginAs } = require('./helpers/auth');
  10. * const auth = await loginAs(browser, 'parent');
  11. * console.log(auth.token, auth.userId, auth.role);
  12. * ================================================================
  13. */
  14. var BASE_URL = process.env.PW_BASE_URL || 'http://cfc.iwintrue.com';
  15. // ================================================================
  16. // DB helpers - insert verification codes directly
  17. // (Bypasses send-code endpoint which throws when testMode=false)
  18. // ================================================================
  19. var child_process = require('child_process');
  20. function insertVerificationCode(phone) {
  21. var mysqlHost = process.env.MYSQL_HOST || '192.168.16.251';
  22. var mysqlUser = process.env.MYSQL_USER || 'zxyj';
  23. var mysqlPass = process.env.MYSQL_PASS || 'zxyj@123';
  24. var mysqlDb = process.env.MYSQL_DB || 'zxyj';
  25. var type = phone === '13800138000' ? 'admin_login' : 'login';
  26. try {
  27. child_process.execSync(
  28. 'mysql -h ' + mysqlHost + ' -u ' + mysqlUser + ' -p"' + mysqlPass + '" ' + mysqlDb +
  29. ' -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);"',
  30. { stdio: 'ignore', shell: true, timeout: 10000 }
  31. );
  32. } catch (e) {
  33. // ignore insert failures - the API call will fail with a clear error
  34. }
  35. }
  36. // ================================================================
  37. // ROLES - Test user credentials mapping
  38. // ================================================================
  39. var ROLES = {
  40. parent: {
  41. phone: '13701366188',
  42. userId: 1002,
  43. loginType: 'code',
  44. familyId: 2001
  45. },
  46. child: {
  47. phone: '13701366189',
  48. userId: 1003,
  49. loginType: 'code',
  50. familyId: 2001
  51. },
  52. teacher: {
  53. phone: '13800000001',
  54. userId: 1005,
  55. loginType: 'code',
  56. familyId: 0
  57. },
  58. nutritionist: {
  59. phone: '13800000010',
  60. userId: 1006,
  61. loginType: 'code',
  62. familyId: 0
  63. },
  64. butler: {
  65. phone: '13800000020',
  66. userId: 1007,
  67. loginType: 'code',
  68. familyId: 0
  69. },
  70. assessor: {
  71. phone: '13800000030',
  72. userId: 1008,
  73. loginType: 'code',
  74. familyId: 0
  75. },
  76. admin: {
  77. phone: '13800138000',
  78. userId: 1001,
  79. loginType: 'password',
  80. familyId: 0
  81. },
  82. vendor: {
  83. phone: '13800138001',
  84. userId: 1009,
  85. loginType: 'code',
  86. familyId: 0
  87. }
  88. };
  89. // ================================================================
  90. // loginAs - Unified authentication for Playwright E2E tests
  91. // ================================================================
  92. /**
  93. * Login as a specific role and return auth information
  94. * @param {Browser} browser - Playwright Browser instance
  95. * @param {string} role - Role name (parent, child, teacher, etc.)
  96. * @returns {Promise<{token: string, userId: number, role: string, familyId: number}>}
  97. */
  98. async function loginAs(browser, role) {
  99. var context = null;
  100. var page = null;
  101. try {
  102. var roleConfig = ROLES[role];
  103. if (!roleConfig) {
  104. throw new Error('Unknown role: ' + role + '. Available roles: ' + Object.keys(ROLES).join(', '));
  105. }
  106. context = await browser.newContext();
  107. page = await context.newPage();
  108. var authResult = {
  109. token: null,
  110. userId: roleConfig.userId,
  111. role: role,
  112. familyId: roleConfig.familyId
  113. };
  114. if (role === 'admin') {
  115. // Admin login via password-based API
  116. // The /api/admin-auth/login-by-password endpoint is public and accepts phone+password.
  117. // The admin user's password hash was pre-set in the DB.
  118. try {
  119. var apiResponse = await page.request.post(BASE_URL + '/api/admin-auth/login-by-password', {
  120. data: {
  121. phone: roleConfig.phone,
  122. password: 'admin123'
  123. },
  124. headers: {
  125. 'Content-Type': 'application/json'
  126. }
  127. });
  128. if (apiResponse.ok()) {
  129. var apiData = await apiResponse.json();
  130. if (apiData && apiData.code === 200 && apiData.data && apiData.data.token) {
  131. authResult.token = apiData.data.token;
  132. }
  133. }
  134. } catch (apiErr) {
  135. // API login failed
  136. }
  137. } else {
  138. // Non-admin roles: try API login first, fallback to UI
  139. // Insert a fresh verification code directly into DB so the API login succeeds.
  140. // The backend's send-code endpoint throws UnsupportedOperationException when
  141. // testMode=false (SMS provider not configured), so we bypass it.
  142. insertVerificationCode(roleConfig.phone);
  143. // Attempt 1: API login
  144. try {
  145. var apiResponse = await page.request.post(BASE_URL + '/api/admin-auth/login', {
  146. data: {
  147. phone: roleConfig.phone,
  148. code: '123456'
  149. },
  150. headers: {
  151. 'Content-Type': 'application/json'
  152. }
  153. });
  154. if (apiResponse.ok()) {
  155. var apiData = await apiResponse.json();
  156. // Backend returns HTTP 200 even on business errors; check body.code === 200
  157. if (apiData && apiData.code === 200 && apiData.data && apiData.data.token) {
  158. authResult.token = apiData.data.token;
  159. // Store token in localStorage for UI consistency
  160. await page.evaluate(function(token) {
  161. localStorage.setItem('token', token);
  162. localStorage.setItem('access_token', token);
  163. }, authResult.token);
  164. }
  165. }
  166. } catch (apiErr) {
  167. // API login failed, will try UI fallback
  168. }
  169. // Fallback: UI login if API didn't work
  170. if (!authResult.token) {
  171. await page.goto(BASE_URL, { waitUntil: 'networkidle', timeout: 30000 });
  172. await page.waitForTimeout(2000);
  173. // Try to find and click login button or go to login page
  174. var loginBtnSelectors = [
  175. 'button:has-text("登录")',
  176. 'a:has-text("登录")',
  177. '.login-btn',
  178. '[class*="login"]'
  179. ];
  180. for (var k = 0; k < loginBtnSelectors.length; k++) {
  181. try {
  182. var loginBtn = page.locator(loginBtnSelectors[k]).first();
  183. if (await loginBtn.isVisible({ timeout: 2000 })) {
  184. await loginBtn.click();
  185. await page.waitForTimeout(1000);
  186. break;
  187. }
  188. } catch (e) {
  189. // continue
  190. }
  191. }
  192. // Fill phone and code
  193. var phoneInputSelectors = [
  194. 'input[type="tel"]',
  195. 'input[placeholder*="手机"]',
  196. 'input[name="phone"]',
  197. '.el-input__inner'
  198. ];
  199. for (var m = 0; m < phoneInputSelectors.length; m++) {
  200. try {
  201. var phoneInput = page.locator(phoneInputSelectors[m]).first();
  202. if (await phoneInput.isVisible({ timeout: 2000 })) {
  203. await phoneInput.fill(roleConfig.phone);
  204. break;
  205. }
  206. } catch (e) {
  207. // continue
  208. }
  209. }
  210. // Fill verification code (123456 for test)
  211. var codeInputSelectors = [
  212. 'input[placeholder*="验证码"]',
  213. 'input[placeholder*="code"]',
  214. 'input[name="code"]'
  215. ];
  216. for (var n = 0; n < codeInputSelectors.length; n++) {
  217. try {
  218. var codeInput = page.locator(codeInputSelectors[n]).first();
  219. if (await codeInput.isVisible({ timeout: 2000 })) {
  220. await codeInput.fill('123456');
  221. break;
  222. }
  223. } catch (e) {
  224. // continue
  225. }
  226. }
  227. // Click submit button
  228. var submitSelectors = [
  229. 'button[type="submit"]',
  230. '.el-button--primary',
  231. 'button:has-text("登录")',
  232. 'button:has-text("提交")'
  233. ];
  234. for (var p = 0; p < submitSelectors.length; p++) {
  235. try {
  236. var submitBtn = page.locator(submitSelectors[p]).first();
  237. if (await submitBtn.isVisible({ timeout: 2000 })) {
  238. await submitBtn.click();
  239. break;
  240. }
  241. } catch (e) {
  242. // continue
  243. }
  244. }
  245. // Wait for navigation or token storage
  246. await page.waitForTimeout(3000);
  247. // Try to read token from localStorage
  248. authResult.token = await page.evaluate(function() {
  249. return localStorage.getItem('token') || localStorage.getItem('access_token') || '';
  250. });
  251. }
  252. }
  253. return authResult;
  254. } catch (error) {
  255. console.error('Login failed for role ' + role + ':', error.message);
  256. // Return partial result even on error
  257. return {
  258. token: null,
  259. userId: roleConfig ? roleConfig.userId : null,
  260. role: role,
  261. familyId: roleConfig ? roleConfig.familyId : null,
  262. error: error.message
  263. };
  264. } finally {
  265. // Cleanup: close the browser context
  266. if (context) {
  267. try {
  268. await context.close();
  269. } catch (closeErr) {
  270. // Ignore close errors
  271. }
  272. }
  273. }
  274. }
  275. // ================================================================
  276. // Module exports (CommonJS)
  277. // ================================================================
  278. module.exports = {
  279. ROLES: ROLES,
  280. loginAs: loginAs,
  281. BASE_URL: BASE_URL
  282. };