backend/test/scenarios/apis/auth.js

644 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 認證相關 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,
};
}