644 lines
17 KiB
JavaScript
644 lines
17 KiB
JavaScript
/**
|
||
* 認證相關 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,
|
||
};
|
||
}
|
||
|