/** * 認證相關 API 場景模組 * * 此模組提供可重複使用的認證相關場景,包括: * - 註冊(帳號密碼、第三方平台) * - 登入(帳號密碼、第三方平台) * - Token 刷新 * - 密碼重設流程 * * 使用方式: * import { registerWithCredentials, loginWithCredentials } from './scenarios/apis/auth.js'; */ import http from 'k6/http'; import { check, sleep } from 'k6'; import { Rate, Trend } from 'k6/metrics'; // 可選的自定義指標 const registerSuccessRate = new Rate('auth_register_success'); const loginSuccessRate = new Rate('auth_login_success'); const registerDuration = new Trend('auth_register_duration'); const loginDuration = new Trend('auth_login_duration'); /** * 使用帳號密碼註冊 * @param {Object} options - 配置選項 * @param {string} options.baseUrl - API 基礎 URL * @param {string} options.loginId - 登入 ID(email 或 phone) * @param {string} options.password - 密碼 * @param {string} options.accountType - 帳號類型(email/phone/any) * @param {Object} options.customMetrics - 自定義指標對象(可選) * @returns {Object} 註冊結果,包含 tokens 和響應 */ export function registerWithCredentials(options = {}) { const { baseUrl = __ENV.BASE_URL || 'http://localhost:8888', loginId = `test_${Date.now()}@example.com`, password = 'Test123456!', accountType = 'email', customMetrics = null, } = options; const url = `${baseUrl}/api/v1/auth/register`; const payload = JSON.stringify({ auth_method: 'credentials', login_id: loginId, credentials: { password: password, password_confirm: password, account_type: accountType, }, }); const params = { headers: { 'Content-Type': 'application/json', }, tags: { name: 'auth_register_credentials', api: 'auth', method: 'register', auth_type: 'credentials', }, }; const startTime = Date.now(); const res = http.post(url, payload, params); const duration = Date.now() - startTime; // 解析響應 let result = null; let responseData = null; if (res.status === 200) { try { result = JSON.parse(res.body); // 支持兩種響應格式: // 1. 直接返回 LoginResp: { access_token, refresh_token, uid, ... } // 2. 包裝在 Resp 中: { code, message, data: { access_token, ... } } if (result.data && typeof result.data === 'object') { // 格式 2: 包裝在 Resp 中 responseData = result.data; } else if (result.access_token) { // 格式 1: 直接返回 LoginResp responseData = result; } else { // 無法識別的格式,記錄響應以便調試 console.warn('Unexpected response format. Full response:', JSON.stringify(result)); console.warn('Response keys:', Object.keys(result)); responseData = result; } } catch (e) { console.error('Failed to parse register response:', e); console.error('Response body:', res.body); } } // 檢查響應結果 const success = check(res, { 'register status is 200': (r) => r.status === 200, 'register has access_token': () => { return responseData && responseData.access_token && responseData.access_token.length > 0; }, 'register has refresh_token': () => { return responseData && responseData.refresh_token && responseData.refresh_token.length > 0; }, 'register has uid': () => { return responseData && responseData.uid && responseData.uid.length > 0; }, }, { name: 'register_checks' }); // 使用自定義指標(如果提供) if (customMetrics) { customMetrics.registerSuccessRate.add(success); customMetrics.registerDuration.add(duration); } else { // 使用預設指標 registerSuccessRate.add(success); registerDuration.add(duration); } return { success, status: res.status, response: result, responseData: responseData, tokens: responseData && responseData.access_token ? { accessToken: responseData.access_token, refreshToken: responseData.refresh_token, uid: responseData.uid, } : null, }; } /** * 使用第三方平台註冊 * @param {Object} options - 配置選項 * @param {string} options.baseUrl - API 基礎 URL * @param {string} options.loginId - 登入 ID * @param {string} options.provider - 平台名稱(google/line/apple) * @param {string} options.token - 平台提供的 Token * @param {Object} options.customMetrics - 自定義指標對象(可選) * @returns {Object} 註冊結果 */ export function registerWithPlatform(options = {}) { const { baseUrl = __ENV.BASE_URL || 'https://localhost:8888', loginId = `platform_${Date.now()}@example.com`, provider = 'google', token = 'mock_platform_token', customMetrics = null, } = options; const url = `${baseUrl}/api/v1/auth/register`; const payload = JSON.stringify({ auth_method: 'platform', login_id: loginId, platform: { provider: provider, token: token, }, }); const params = { headers: { 'Content-Type': 'application/json', }, tags: { name: 'auth_register_platform', api: 'auth', method: 'register', auth_type: 'platform', provider: provider, }, }; const startTime = Date.now(); const res = http.post(url, payload, params); const duration = Date.now() - startTime; const success = check(res, { 'register platform status is 200': (r) => r.status === 200, 'register platform has tokens': (r) => { try { const body = JSON.parse(r.body); return body.access_token && body.refresh_token; } catch { return false; } }, }, { name: 'register_platform_checks' }); if (customMetrics) { customMetrics.registerSuccessRate?.add(success); customMetrics.registerDuration?.add(duration); } else { registerSuccessRate.add(success); registerDuration.add(duration); } let result = null; if (res.status === 200) { try { result = JSON.parse(res.body); } catch (e) { console.error('Failed to parse register platform response:', e); } } return { success, status: res.status, response: result, tokens: result ? { accessToken: result.access_token, refreshToken: result.refresh_token, uid: result.uid, } : null, }; } /** * 使用帳號密碼登入 * @param {Object} options - 配置選項 * @param {string} options.baseUrl - API 基礎 URL * @param {string} options.loginId - 登入 ID * @param {string} options.password - 密碼 * @param {Object} options.customMetrics - 自定義指標對象(可選) * @returns {Object} 登入結果 */ export function loginWithCredentials(options = {}) { const { baseUrl = __ENV.BASE_URL || 'http://localhost:8888', loginId, password, customMetrics = null, } = options; if (!loginId || !password) { throw new Error('loginId and password are required for credentials login'); } const url = `${baseUrl}/api/v1/auth/sessions`; const payload = JSON.stringify({ auth_method: 'credentials', login_id: loginId, credentials: { password: password, password_confirm: password, account_type: 'email', }, }); const params = { headers: { 'Content-Type': 'application/json', }, tags: { name: 'auth_login_credentials', api: 'auth', method: 'login', auth_type: 'credentials', }, }; const startTime = Date.now(); const res = http.post(url, payload, params); const duration = Date.now() - startTime; // 解析響應 let result = null; let responseData = null; if (res.status === 200) { try { result = JSON.parse(res.body); // 支持兩種響應格式: // 1. 直接返回 LoginResp: { access_token, refresh_token, uid, ... } // 2. 包裝在 Resp 中: { code, message, data: { access_token, ... } } if (result.data && typeof result.data === 'object') { // 格式 2: 包裝在 Resp 中 responseData = result.data; } else if (result.access_token) { // 格式 1: 直接返回 LoginResp responseData = result; } else { // 無法識別的格式,記錄響應以便調試 console.warn('Unexpected login response format. Full response:', JSON.stringify(result)); console.warn('Response keys:', Object.keys(result)); responseData = result; } } catch (e) { console.error('Failed to parse login response:', e); console.error('Response body:', res.body); } } // 檢查響應結果 const success = check(res, { 'login status is 200': (r) => r.status === 200, 'login has access_token': () => { return responseData && responseData.access_token && responseData.access_token.length > 0; }, 'login has refresh_token': () => { return responseData && responseData.refresh_token && responseData.refresh_token.length > 0; }, }, { name: 'login_checks' }); if (customMetrics) { customMetrics.loginSuccessRate?.add(success); customMetrics.loginDuration?.add(duration); } else { loginSuccessRate.add(success); loginDuration.add(duration); } return { success, status: res.status, response: result, responseData: responseData, tokens: responseData && responseData.access_token ? { accessToken: responseData.access_token, refreshToken: responseData.refresh_token, uid: responseData.uid, } : null, }; } /** * 使用第三方平台登入 * @param {Object} options - 配置選項 * @param {string} options.baseUrl - API 基礎 URL * @param {string} options.loginId - 登入 ID * @param {string} options.provider - 平台名稱 * @param {string} options.token - 平台 Token * @param {Object} options.customMetrics - 自定義指標對象(可選) * @returns {Object} 登入結果 */ export function loginWithPlatform(options = {}) { const { baseUrl = __ENV.BASE_URL || 'https://localhost:8888', loginId, provider = 'google', token = 'mock_platform_token', customMetrics = null, } = options; if (!loginId) { throw new Error('loginId is required for platform login'); } const url = `${baseUrl}/api/v1/auth/sessions`; const payload = JSON.stringify({ auth_method: 'platform', login_id: loginId, platform: { provider: provider, token: token, }, }); const params = { headers: { 'Content-Type': 'application/json', }, tags: { name: 'auth_login_platform', api: 'auth', method: 'login', auth_type: 'platform', provider: provider, }, }; const startTime = Date.now(); const res = http.post(url, payload, params); const duration = Date.now() - startTime; const success = check(res, { 'login platform status is 200': (r) => r.status === 200, 'login platform has tokens': (r) => { try { const body = JSON.parse(r.body); return body.access_token && body.refresh_token; } catch { return false; } }, }, { name: 'login_platform_checks' }); if (customMetrics) { customMetrics.loginSuccessRate?.add(success); customMetrics.loginDuration?.add(duration); } else { loginSuccessRate.add(success); loginDuration.add(duration); } let result = null; if (res.status === 200) { try { result = JSON.parse(res.body); } catch (e) { console.error('Failed to parse login platform response:', e); } } return { success, status: res.status, response: result, tokens: result ? { accessToken: result.access_token, refreshToken: result.refresh_token, uid: result.uid, } : null, }; } /** * 刷新 Access Token * @param {Object} options - 配置選項 * @param {string} options.baseUrl - API 基礎 URL * @param {string} options.accessToken - 當前的 Access Token * @param {string} options.refreshToken - Refresh Token * @param {Object} options.customMetrics - 自定義指標對象(可選) * @returns {Object} 刷新結果 */ export function refreshToken(options = {}) { const { baseUrl = __ENV.BASE_URL || 'https://localhost:8888', accessToken, refreshToken, customMetrics = null, } = options; if (!accessToken || !refreshToken) { throw new Error('accessToken and refreshToken are required'); } const url = `${baseUrl}/api/v1/auth/sessions/refresh`; const payload = JSON.stringify({ access_token: accessToken, refresh_token: refreshToken, }); const params = { headers: { 'Content-Type': 'application/json', }, tags: { name: 'auth_refresh_token', api: 'auth', method: 'refresh_token', }, }; const res = http.post(url, payload, params); const success = check(res, { 'refresh token status is 200': (r) => r.status === 200, 'refresh token has new access_token': (r) => { try { const body = JSON.parse(r.body); console.log('refresh token response:', body.data); return body.data.access_token && body.data.access_token.length > 0; } catch { return false; } }, }, { name: 'refresh_token_checks' }); let result = null; if (res.status === 200) { try { result = JSON.parse(res.body.data); } catch (e) { console.error('Failed to parse refresh token response:', e); } } return { success, status: res.status, response: result, tokens: result ? { accessToken: result.access_token, refreshToken: result.refresh_token, } : null, }; } /** * 請求密碼重設驗證碼 * @param {Object} options - 配置選項 * @param {string} options.baseUrl - API 基礎 URL * @param {string} options.identifier - 使用者帳號(email 或 phone) * @param {string} options.accountType - 帳號類型(email/phone) * @returns {Object} 請求結果 */ export function requestPasswordReset(options = {}) { const { baseUrl = __ENV.BASE_URL || 'http://localhost:8888', identifier, accountType = 'email', } = options; if (!identifier) { throw new Error('identifier is required'); } const url = `${baseUrl}/api/v1/auth/password-resets/request`; const payload = JSON.stringify({ identifier: identifier, account_type: accountType, }); const params = { headers: { 'Content-Type': 'application/json', }, tags: { name: 'auth_request_password_reset', api: 'auth', method: 'request_password_reset', }, }; const res = http.post(url, payload, params); const success = check(res, { 'request password reset status is 200': (r) => r.status === 200, }, { name: 'request_password_reset_checks' }); return { success, status: res.status, }; } /** * 驗證密碼重設驗證碼 * @param {Object} options - 配置選項 * @param {string} options.baseUrl - API 基礎 URL * @param {string} options.identifier - 使用者帳號 * @param {string} options.verifyCode - 驗證碼 * @returns {Object} 驗證結果 */ export function verifyPasswordResetCode(options = {}) { const { baseUrl = __ENV.BASE_URL || 'http://localhost:8888', identifier, verifyCode, } = options; if (!identifier || !verifyCode) { throw new Error('identifier and verifyCode are required'); } const url = `${baseUrl}/api/v1/auth/password-resets/verify`; const payload = JSON.stringify({ identifier: identifier, verify_code: verifyCode, }); const params = { headers: { 'Content-Type': 'application/json', }, tags: { name: 'auth_verify_password_reset_code', api: 'auth', method: 'verify_password_reset_code', }, }; const res = http.post(url, payload, params); const success = check(res, { 'verify password reset code status is 200': (r) => r.status === 200, }, { name: 'verify_password_reset_code_checks' }); return { success, status: res.status, }; } /** * 執行密碼重設 * @param {Object} options - 配置選項 * @param {string} options.baseUrl - API 基礎 URL * @param {string} options.identifier - 使用者帳號 * @param {string} options.verifyCode - 驗證碼 * @param {string} options.newPassword - 新密碼 * @returns {Object} 重設結果 */ export function resetPassword(options = {}) { const { baseUrl = __ENV.BASE_URL || 'https://localhost:8888', identifier, verifyCode, newPassword, } = options; if (!identifier || !verifyCode || !newPassword) { throw new Error('identifier, verifyCode, and newPassword are required'); } const url = `${baseUrl}/api/v1/auth/password-resets`; const payload = JSON.stringify({ identifier: identifier, verify_code: verifyCode, password: newPassword, password_confirm: newPassword, }); const params = { headers: { 'Content-Type': 'application/json', }, tags: { name: 'auth_reset_password', api: 'auth', method: 'reset_password', }, }; const res = http.put(url, payload, params); const success = check(res, { 'reset password status is 200': (r) => r.status === 200, }, { name: 'reset_password_checks' }); return { success, status: res.status, }; }