backend/test/scenarios/apis/auth.js

644 lines
17 KiB
JavaScript
Raw Permalink Normal View History

2025-11-07 07:44:23 +00:00
/**
* 認證相關 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 - 登入 IDemail 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,
};
}