feat: k6 test
This commit is contained in:
parent
d845ef45fd
commit
55e0e5c85b
|
|
@ -1,4 +1,4 @@
|
||||||
export ZITADEL_SERVICE_TOKEN=4ozUZSXQZ2iaObRwejZT95BtuEPI4j_UZoCqUOM4B26KhYAW4k3uSVokd-sat49HrDMSkxM
|
export ZITADEL_SERVICE_TOKEN=KK234msGgYozMn56fEzHAjsf-lVm5qyoHCGR10ay1nGYUtN06RIxHtSq90Wtle9cuIVhXWg
|
||||||
export BASE_URL=http://localhost:8888
|
export BASE_URL=http://localhost:8888
|
||||||
export MAILHOG_URL=http://localhost:8025
|
export MAILHOG_URL=http://localhost:8025
|
||||||
export REDIS_ADDR=localhost:6379
|
export REDIS_ADDR=localhost:6379
|
||||||
|
|
|
||||||
|
|
@ -669,9 +669,6 @@
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -689,9 +686,6 @@
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -709,9 +703,6 @@
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -729,9 +720,6 @@
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -749,9 +737,6 @@
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -769,9 +754,6 @@
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -2149,9 +2131,6 @@
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -2173,9 +2152,6 @@
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -2197,9 +2173,6 @@
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
@ -2221,9 +2194,6 @@
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# k6 API tests
|
# k6 API tests
|
||||||
|
|
||||||
完整的 Gateway API smoke + journey 測試套件。**所有 36 個對外端點**都至少在 `smoke/` 或 `journeys/` 裡有一發。
|
完整的 Gateway API smoke + journey 測試套件。**所有對外端點**都至少在 `smoke/` 或 `journeys/` 裡有一發(含登入 MFA、忘記/改密碼、註冊 resume)。
|
||||||
|
|
||||||
## TL;DR
|
## TL;DR
|
||||||
|
|
||||||
|
|
@ -33,6 +33,7 @@ k6 run test/k6/journeys/email_register_full.js
|
||||||
| `ADMIN_EMAIL` / `ADMIN_PASSWORD` | — | rbac journey seeded admin |
|
| `ADMIN_EMAIL` / `ADMIN_PASSWORD` | — | rbac journey seeded admin |
|
||||||
| `OTP_POLL_INTERVAL_MS` | `300` | OTP poll 頻率 |
|
| `OTP_POLL_INTERVAL_MS` | `300` | OTP poll 頻率 |
|
||||||
| `OTP_POLL_TIMEOUT_MS` | `5000` | OTP poll 超時 |
|
| `OTP_POLL_TIMEOUT_MS` | `5000` | OTP poll 超時 |
|
||||||
|
| `RESEND_COOLDOWN_SECONDS` | `60` | 與 `gateway.k6.yaml` OTP 重送冷卻一致(cooldown smoke 會 sleep) |
|
||||||
|
|
||||||
## 目錄結構
|
## 目錄結構
|
||||||
|
|
||||||
|
|
@ -44,17 +45,25 @@ test/k6/
|
||||||
│ ├── http.js # get/post/...、checkEnvelope、withBearer
|
│ ├── http.js # get/post/...、checkEnvelope、withBearer
|
||||||
│ ├── otp.js # fetchEmailOTP (MailHog) / fetchSMSOTP (Redis)
|
│ ├── otp.js # fetchEmailOTP (MailHog) / fetchSMSOTP (Redis)
|
||||||
│ ├── totp.js # HMAC-SHA1 TOTP(P4)
|
│ ├── totp.js # HMAC-SHA1 TOTP(P4)
|
||||||
│ ├── auth.js # register / confirm / login / refresh helper(P2)
|
│ ├── auth.js # register / confirm / login / MFA / password helper
|
||||||
|
│ ├── member.js # TOTP enroll / change password helper
|
||||||
│ └── seed.js # tenant + invite + admin role bootstrap(P5)
|
│ └── seed.js # tenant + invite + admin role bootstrap(P5)
|
||||||
├── smoke/ # 每個端點至少一發
|
├── smoke/ # 每個端點至少一發
|
||||||
│ ├── health.js # GET /api/v1/health
|
│ ├── health.js # GET /api/v1/health
|
||||||
│ ├── auth_public.js # register / login / refresh / social-start (+negative)
|
│ ├── auth_public.js # register / login / refresh / social-start (+negative)
|
||||||
|
│ ├── auth_register_resume.js # register/resume(happy + 404/409/429/400)
|
||||||
|
│ ├── auth_password.js # password/forgot + reset(happy + 各種 negative)
|
||||||
|
│ ├── auth_login_mfa.js # login/mfa(MFA 前置 + negative)
|
||||||
│ ├── auth_bearer.js # logout
|
│ ├── auth_bearer.js # logout
|
||||||
│ ├── member.js # me / patch / verify start+confirm / TOTP
|
│ ├── member.js # me / patch / verify / TOTP / change-password negative
|
||||||
│ ├── permission_read.js # catalog / me
|
│ ├── permission_read.js # catalog / me
|
||||||
│ └── permission_admin.js # roles CRUD / role-permissions / user-roles / mappings / policy reload
|
│ └── permission_admin.js # roles CRUD / role-permissions / user-roles / mappings / policy reload
|
||||||
└── journeys/ # 完整流程
|
└── journeys/ # 完整流程
|
||||||
├── email_register_full.js # register → confirm OTP(MailHog) → me → patch → logout
|
├── email_register_full.js # register → confirm OTP(MailHog) → me → patch → logout
|
||||||
|
├── register_resume_full.js # register → resume → confirm → me
|
||||||
|
├── password_forgot_reset_full.js # forgot → reset → login(新密碼)
|
||||||
|
├── login_mfa_full.js # enroll TOTP → login MFA → me
|
||||||
|
├── change_password_full.js # change password → login(新密碼)
|
||||||
├── login_refresh.js # login → refresh → me → logout
|
├── login_refresh.js # login → refresh → me → logout
|
||||||
├── email_verify.js # register → confirm → email verify start → confirm
|
├── email_verify.js # register → confirm → email verify start → confirm
|
||||||
├── phone_verify.js # register → confirm → phone verify (Redis OTP)
|
├── phone_verify.js # register → confirm → phone verify (Redis OTP)
|
||||||
|
|
@ -78,6 +87,10 @@ test/k6/
|
||||||
| Auth 公開 | `POST /register` | journeys/email_register_full + smoke/auth_public |
|
| Auth 公開 | `POST /register` | journeys/email_register_full + smoke/auth_public |
|
||||||
| Auth 公開 | `POST /register/confirm` | journeys/email_register_full |
|
| Auth 公開 | `POST /register/confirm` | journeys/email_register_full |
|
||||||
| Auth 公開 | `POST /register/resend` | smoke/auth_public |
|
| Auth 公開 | `POST /register/resend` | smoke/auth_public |
|
||||||
|
| Auth 公開 | `POST /register/resume` | smoke/auth_register_resume + journeys/register_resume_full |
|
||||||
|
| Auth 公開 | `POST /password/forgot` | smoke/auth_password + journeys/password_forgot_reset_full |
|
||||||
|
| Auth 公開 | `POST /password/reset` | smoke/auth_password + journeys/password_forgot_reset_full |
|
||||||
|
| Auth 公開 | `POST /login/mfa` | smoke/auth_login_mfa + journeys/login_mfa_full |
|
||||||
| Auth 公開 | `POST /register/social/start` | smoke/auth_public (happy) |
|
| Auth 公開 | `POST /register/social/start` | smoke/auth_public (happy) |
|
||||||
| Auth 公開 | `GET /register/social/callback` | smoke/auth_public (negative — TODO happy) |
|
| Auth 公開 | `GET /register/social/callback` | smoke/auth_public (negative — TODO happy) |
|
||||||
| Auth 公開 | `POST /login` | journeys/login_refresh + smoke/auth_public (negative) |
|
| Auth 公開 | `POST /login` | journeys/login_refresh + smoke/auth_public (negative) |
|
||||||
|
|
@ -98,6 +111,7 @@ test/k6/
|
||||||
| Member | `POST /me/totp/verify` | smoke/member (negative) + journeys/totp_full (happy) |
|
| Member | `POST /me/totp/verify` | smoke/member (negative) + journeys/totp_full (happy) |
|
||||||
| Member | `POST /me/totp/backup-codes` | smoke/member (negative) + journeys/totp_full (happy) |
|
| Member | `POST /me/totp/backup-codes` | smoke/member (negative) + journeys/totp_full (happy) |
|
||||||
| Member | `DELETE /me/totp` | smoke/member + journeys/totp_full |
|
| Member | `DELETE /me/totp` | smoke/member + journeys/totp_full |
|
||||||
|
| Member | `POST /me/password` | smoke/member (negative) + journeys/change_password_full |
|
||||||
| Perm 讀 | `GET /permissions/catalog` | smoke/permission_read + journeys/rbac_admin |
|
| Perm 讀 | `GET /permissions/catalog` | smoke/permission_read + journeys/rbac_admin |
|
||||||
| Perm 讀 | `GET /permissions/me` | smoke/permission_read + journeys/rbac_admin |
|
| Perm 讀 | `GET /permissions/me` | smoke/permission_read + journeys/rbac_admin |
|
||||||
| Perm 管理 | `GET /roles` | smoke/permission_admin + journeys/rbac_admin |
|
| Perm 管理 | `GET /roles` | smoke/permission_admin + journeys/rbac_admin |
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
// Journey: change password while logged in → login with new password
|
||||||
|
//
|
||||||
|
// Endpoints:
|
||||||
|
// POST /api/v1/auth/register/confirm path (via registerAndConfirm)
|
||||||
|
// POST /api/v1/members/me/password
|
||||||
|
// POST /api/v1/auth/login
|
||||||
|
import { post, checkError } from '../lib/http.js';
|
||||||
|
import { cfg } from '../lib/config.js';
|
||||||
|
import { registerAndConfirm, loginStep } from '../lib/auth.js';
|
||||||
|
import { changePassword } from '../lib/member.js';
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
vus: 1,
|
||||||
|
iterations: 1,
|
||||||
|
thresholds: { checks: ['rate==1.0'] },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const { identity, tokens } = registerAndConfirm();
|
||||||
|
const bearer = { Authorization: `Bearer ${tokens.access_token}` };
|
||||||
|
const newPassword = 'K6-ChangePass-8!';
|
||||||
|
|
||||||
|
const data = changePassword(identity.password, newPassword, bearer);
|
||||||
|
if (!data.ok) {
|
||||||
|
throw new Error('change password journey: expected ok=true');
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = loginStep({
|
||||||
|
email: identity.email,
|
||||||
|
password: newPassword,
|
||||||
|
});
|
||||||
|
if (!login.access_token) {
|
||||||
|
throw new Error('change password journey: login with new password failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/auth/login', {
|
||||||
|
tenant_slug: cfg.tenantSlug,
|
||||||
|
email: identity.email,
|
||||||
|
password: identity.password,
|
||||||
|
}),
|
||||||
|
'POST /auth/login (old password after change)',
|
||||||
|
401,
|
||||||
|
28501000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Journey: login with TOTP MFA — enroll → login (mfa_required) → login/mfa → /me
|
||||||
|
//
|
||||||
|
// Endpoints:
|
||||||
|
// POST /api/v1/auth/register/confirm path (via registerAndConfirm)
|
||||||
|
// POST /api/v1/members/me/totp/enroll-start + enroll-confirm
|
||||||
|
// POST /api/v1/auth/login
|
||||||
|
// POST /api/v1/auth/login/mfa
|
||||||
|
// GET /api/v1/members/me
|
||||||
|
import { get, checkEnvelope } from '../lib/http.js';
|
||||||
|
import { registerAndConfirm, loginExpectMFA, loginMfaConfirm } from '../lib/auth.js';
|
||||||
|
import { enrollTOTP } from '../lib/member.js';
|
||||||
|
import { generateTOTP } from '../lib/totp.js';
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
vus: 1,
|
||||||
|
iterations: 1,
|
||||||
|
thresholds: { checks: ['rate==1.0'] },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const { identity, tokens } = registerAndConfirm();
|
||||||
|
const bearer = { Authorization: `Bearer ${tokens.access_token}` };
|
||||||
|
const { otpauthUrl } = enrollTOTP(bearer);
|
||||||
|
|
||||||
|
const mfa = loginExpectMFA({
|
||||||
|
email: identity.email,
|
||||||
|
password: identity.password,
|
||||||
|
});
|
||||||
|
const totpCode = generateTOTP(otpauthUrl);
|
||||||
|
const session = loginMfaConfirm({
|
||||||
|
challengeId: mfa.mfa_challenge_id,
|
||||||
|
code: totpCode,
|
||||||
|
});
|
||||||
|
if (!session.access_token) {
|
||||||
|
throw new Error('login/mfa journey: missing access_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const me = checkEnvelope(
|
||||||
|
get('/api/v1/members/me', { Authorization: `Bearer ${session.access_token}` }),
|
||||||
|
'GET /members/me (after login/mfa)',
|
||||||
|
).data;
|
||||||
|
if (me.uid !== session.uid) {
|
||||||
|
throw new Error('login/mfa journey: uid mismatch');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
// Journey: forgot password → reset → login with new password
|
||||||
|
//
|
||||||
|
// Endpoints:
|
||||||
|
// POST /api/v1/auth/register + /register/confirm (setup)
|
||||||
|
// POST /api/v1/auth/password/forgot
|
||||||
|
// POST /api/v1/auth/password/reset
|
||||||
|
// POST /api/v1/auth/login
|
||||||
|
import { post, checkError } from '../lib/http.js';
|
||||||
|
import { cfg } from '../lib/config.js';
|
||||||
|
import { registerAndConfirm, passwordForgot, passwordReset, loginStep } from '../lib/auth.js';
|
||||||
|
import { fetchEmailOTP } from '../lib/otp.js';
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
vus: 1,
|
||||||
|
iterations: 1,
|
||||||
|
thresholds: { checks: ['rate==1.0'] },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const { identity } = registerAndConfirm();
|
||||||
|
const newPassword = 'K6-ResetPass-9!';
|
||||||
|
|
||||||
|
const since = Date.now();
|
||||||
|
const forgot = passwordForgot({ email: identity.email });
|
||||||
|
const code = fetchEmailOTP(identity.email, { since });
|
||||||
|
passwordReset({
|
||||||
|
challengeId: forgot.challenge_id,
|
||||||
|
code,
|
||||||
|
newPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
const login = loginStep({
|
||||||
|
email: identity.email,
|
||||||
|
password: newPassword,
|
||||||
|
});
|
||||||
|
if (!login.access_token) {
|
||||||
|
throw new Error('password reset journey: login with new password failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// old password should fail
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/auth/login', {
|
||||||
|
tenant_slug: cfg.tenantSlug,
|
||||||
|
email: identity.email,
|
||||||
|
password: identity.password,
|
||||||
|
}),
|
||||||
|
'POST /auth/login (old password after reset)',
|
||||||
|
401,
|
||||||
|
28501000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
// Journey: 未完成註冊 → resume → confirm OTP
|
||||||
|
//
|
||||||
|
// Endpoints:
|
||||||
|
// POST /api/v1/auth/register
|
||||||
|
// POST /api/v1/auth/register/resume
|
||||||
|
// POST /api/v1/auth/register/confirm
|
||||||
|
// GET /api/v1/members/me
|
||||||
|
import { sleep } from 'k6';
|
||||||
|
import { get, checkEnvelope } from '../lib/http.js';
|
||||||
|
import { cfg } from '../lib/config.js';
|
||||||
|
import {
|
||||||
|
makeIdentity,
|
||||||
|
registerEmail,
|
||||||
|
registerResume,
|
||||||
|
confirmRegister,
|
||||||
|
} from '../lib/auth.js';
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
vus: 1,
|
||||||
|
iterations: 1,
|
||||||
|
thresholds: { checks: ['rate==1.0'] },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const identity = makeIdentity('resume-journey');
|
||||||
|
registerEmail({
|
||||||
|
email: identity.email,
|
||||||
|
password: identity.password,
|
||||||
|
displayName: identity.displayName,
|
||||||
|
});
|
||||||
|
sleep(cfg.resendCooldownSeconds + 1);
|
||||||
|
|
||||||
|
const resumed = registerResume({ email: identity.email });
|
||||||
|
const tokens = confirmRegister({
|
||||||
|
email: identity.email,
|
||||||
|
challengeId: resumed.challenge_id,
|
||||||
|
});
|
||||||
|
if (!tokens.access_token) {
|
||||||
|
throw new Error('register/resume journey: missing access_token after confirm');
|
||||||
|
}
|
||||||
|
|
||||||
|
const me = checkEnvelope(
|
||||||
|
get('/api/v1/members/me', { Authorization: `Bearer ${tokens.access_token}` }),
|
||||||
|
'GET /members/me (after resume confirm)',
|
||||||
|
).data;
|
||||||
|
if (me.uid !== tokens.uid) {
|
||||||
|
throw new Error('register/resume journey: me.uid mismatch');
|
||||||
|
}
|
||||||
|
if (me.status !== 'active') {
|
||||||
|
throw new Error(`register/resume journey: expected active status, got ${me.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import { post } from './http.js';
|
||||||
import { checkEnvelope } from './http.js';
|
import { checkEnvelope } from './http.js';
|
||||||
import { cfg, unique } from './config.js';
|
import { cfg, unique } from './config.js';
|
||||||
import { fetchEmailOTP } from './otp.js';
|
import { fetchEmailOTP } from './otp.js';
|
||||||
|
import { generateTOTP } from './totp.js';
|
||||||
|
|
||||||
// makeIdentity returns a unique (email, password, display_name) tuple for the
|
// makeIdentity returns a unique (email, password, display_name) tuple for the
|
||||||
// current VU iteration. Use to avoid collisions in concurrent runs.
|
// current VU iteration. Use to avoid collisions in concurrent runs.
|
||||||
|
|
@ -62,18 +63,97 @@ export function resendRegister({ tenantSlug = cfg.tenantSlug, challengeId }) {
|
||||||
return checkEnvelope(res, 'POST /auth/register/resend').data;
|
return checkEnvelope(res, 'POST /auth/register/resend').data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// login calls POST /api/v1/auth/login. Returns AuthTokenData.
|
// registerResume calls POST /api/v1/auth/register/resume.
|
||||||
export function login({ tenantSlug = cfg.tenantSlug, email, password }) {
|
export function registerResume({ tenantSlug = cfg.tenantSlug, email }) {
|
||||||
|
const res = post('/api/v1/auth/register/resume', {
|
||||||
|
tenant_slug: tenantSlug,
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
return checkEnvelope(res, 'POST /auth/register/resume').data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// passwordForgot calls POST /api/v1/auth/password/forgot.
|
||||||
|
export function passwordForgot({ tenantSlug = cfg.tenantSlug, email }) {
|
||||||
|
const res = post('/api/v1/auth/password/forgot', {
|
||||||
|
tenant_slug: tenantSlug,
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
return checkEnvelope(res, 'POST /auth/password/forgot').data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// passwordReset calls POST /api/v1/auth/password/reset.
|
||||||
|
export function passwordReset({
|
||||||
|
tenantSlug = cfg.tenantSlug,
|
||||||
|
challengeId,
|
||||||
|
code,
|
||||||
|
newPassword,
|
||||||
|
}) {
|
||||||
|
const res = post('/api/v1/auth/password/reset', {
|
||||||
|
tenant_slug: tenantSlug,
|
||||||
|
challenge_id: challengeId,
|
||||||
|
code,
|
||||||
|
new_password: newPassword,
|
||||||
|
});
|
||||||
|
return checkEnvelope(res, 'POST /auth/password/reset').data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// loginStep calls POST /api/v1/auth/login and returns LoginData (tokens or MFA challenge).
|
||||||
|
export function loginStep({ tenantSlug = cfg.tenantSlug, email, password }) {
|
||||||
const res = post('/api/v1/auth/login', {
|
const res = post('/api/v1/auth/login', {
|
||||||
tenant_slug: tenantSlug,
|
tenant_slug: tenantSlug,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
const body = checkEnvelope(res, 'POST /auth/login');
|
return checkEnvelope(res, 'POST /auth/login').data;
|
||||||
if (!body.data || !body.data.access_token) {
|
}
|
||||||
throw new Error(`login: missing access_token in ${res.body}`);
|
|
||||||
|
// loginMfaConfirm completes login after MFA challenge.
|
||||||
|
export function loginMfaConfirm({ tenantSlug = cfg.tenantSlug, challengeId, code }) {
|
||||||
|
const res = post('/api/v1/auth/login/mfa', {
|
||||||
|
tenant_slug: tenantSlug,
|
||||||
|
challenge_id: challengeId,
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
return checkEnvelope(res, 'POST /auth/login/mfa').data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// loginExpectMFA returns LoginData when mfa_required=true.
|
||||||
|
export function loginExpectMFA({ tenantSlug = cfg.tenantSlug, email, password }) {
|
||||||
|
const data = loginStep({ tenantSlug, email, password });
|
||||||
|
if (!data.mfa_required) {
|
||||||
|
throw new Error(`loginExpectMFA: expected mfa_required, got ${JSON.stringify(data)}`);
|
||||||
}
|
}
|
||||||
return body.data;
|
if (!data.mfa_challenge_id) {
|
||||||
|
throw new Error('loginExpectMFA: missing mfa_challenge_id');
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// login calls POST /api/v1/auth/login. When TOTP is enrolled, pass totpCode or otpauthUrl.
|
||||||
|
export function login({ tenantSlug = cfg.tenantSlug, email, password, totpCode, otpauthUrl } = {}) {
|
||||||
|
const step = loginStep({ tenantSlug, email, password });
|
||||||
|
if (step.mfa_required) {
|
||||||
|
let code = totpCode;
|
||||||
|
if (!code && otpauthUrl) {
|
||||||
|
code = generateTOTP(otpauthUrl);
|
||||||
|
}
|
||||||
|
if (!code) {
|
||||||
|
throw new Error('login: MFA required but totpCode/otpauthUrl not provided');
|
||||||
|
}
|
||||||
|
const tokens = loginMfaConfirm({
|
||||||
|
tenantSlug,
|
||||||
|
challengeId: step.mfa_challenge_id,
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
if (!tokens.access_token) {
|
||||||
|
throw new Error('login/mfa: missing access_token');
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
if (!step.access_token) {
|
||||||
|
throw new Error(`login: missing access_token in ${JSON.stringify(step)}`);
|
||||||
|
}
|
||||||
|
return step;
|
||||||
}
|
}
|
||||||
|
|
||||||
// refreshToken calls POST /api/v1/auth/token/refresh.
|
// refreshToken calls POST /api/v1/auth/token/refresh.
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ export const cfg = {
|
||||||
// OTP polling parameters (MailHog / Redis)
|
// OTP polling parameters (MailHog / Redis)
|
||||||
otpPollIntervalMs: parseInt(__ENV.OTP_POLL_INTERVAL_MS || '300', 10),
|
otpPollIntervalMs: parseInt(__ENV.OTP_POLL_INTERVAL_MS || '300', 10),
|
||||||
otpPollTimeoutMs: parseInt(__ENV.OTP_POLL_TIMEOUT_MS || '5000', 10),
|
otpPollTimeoutMs: parseInt(__ENV.OTP_POLL_TIMEOUT_MS || '5000', 10),
|
||||||
|
/** 與 etc/gateway.k6.yaml Member.OTP.ResendCooldownSeconds 一致 */
|
||||||
|
resendCooldownSeconds: parseInt(__ENV.RESEND_COOLDOWN_SECONDS || '60', 10),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build a unique-ish identity per VU+iteration so concurrent runs do not collide.
|
// Build a unique-ish identity per VU+iteration so concurrent runs do not collide.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
// Member flow helpers — TOTP enroll / change password.
|
||||||
|
import { post, checkEnvelope } from './http.js';
|
||||||
|
import { generateTOTP } from './totp.js';
|
||||||
|
|
||||||
|
export function enrollTOTP(bearer) {
|
||||||
|
const enroll = checkEnvelope(
|
||||||
|
post('/api/v1/members/me/totp/enroll-start', null, bearer),
|
||||||
|
'POST /me/totp/enroll-start',
|
||||||
|
).data;
|
||||||
|
if (!enroll.otpauth_url) {
|
||||||
|
throw new Error('enroll-start: missing otpauth_url');
|
||||||
|
}
|
||||||
|
const code = generateTOTP(enroll.otpauth_url);
|
||||||
|
checkEnvelope(
|
||||||
|
post('/api/v1/members/me/totp/enroll-confirm', { code }, bearer),
|
||||||
|
'POST /me/totp/enroll-confirm',
|
||||||
|
);
|
||||||
|
return { otpauthUrl: enroll.otpauth_url };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changePassword(currentPassword, newPassword, bearer) {
|
||||||
|
const res = post(
|
||||||
|
'/api/v1/members/me/password',
|
||||||
|
{ current_password: currentPassword, new_password: newPassword },
|
||||||
|
bearer,
|
||||||
|
);
|
||||||
|
return checkEnvelope(res, 'POST /me/password').data;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
// smoke: POST /api/v1/auth/login/mfa
|
||||||
|
//
|
||||||
|
// Covers:
|
||||||
|
// login → mfa_required(已啟用 TOTP 時不回 token)
|
||||||
|
// 403 — TOTP 錯誤(29505000)
|
||||||
|
// 404 — challenge 不存在(28301000)
|
||||||
|
// 403 — tenant slug 不符(28505000)
|
||||||
|
// 400 — 缺少 code / challenge_id
|
||||||
|
//
|
||||||
|
// Happy path(login → login/mfa → JWT)見 journeys/login_mfa_full.js
|
||||||
|
import { post, checkError } from '../lib/http.js';
|
||||||
|
import { cfg } from '../lib/config.js';
|
||||||
|
import { registerAndConfirm, loginExpectMFA } from '../lib/auth.js';
|
||||||
|
import { enrollTOTP } from '../lib/member.js';
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
vus: 1,
|
||||||
|
iterations: 1,
|
||||||
|
thresholds: { checks: ['rate==1.0'] },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const { identity, tokens } = registerAndConfirm();
|
||||||
|
const bearer = { Authorization: `Bearer ${tokens.access_token}` };
|
||||||
|
const { otpauthUrl } = enrollTOTP(bearer);
|
||||||
|
|
||||||
|
const mfa = loginExpectMFA({
|
||||||
|
email: identity.email,
|
||||||
|
password: identity.password,
|
||||||
|
});
|
||||||
|
if (mfa.access_token) {
|
||||||
|
throw new Error('login with TOTP enrolled should not return access_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// bad TOTP
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/auth/login/mfa', {
|
||||||
|
tenant_slug: cfg.tenantSlug,
|
||||||
|
challenge_id: mfa.mfa_challenge_id,
|
||||||
|
code: '000000',
|
||||||
|
}),
|
||||||
|
'POST /auth/login/mfa (bad totp)',
|
||||||
|
403,
|
||||||
|
29505000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// unknown challenge — Redis miss 目前回 500(28201000)
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/auth/login/mfa', {
|
||||||
|
tenant_slug: cfg.tenantSlug,
|
||||||
|
challenge_id: '00000000-0000-0000-0000-000000000000',
|
||||||
|
code: '123456',
|
||||||
|
}),
|
||||||
|
'POST /auth/login/mfa (unknown challenge)',
|
||||||
|
500,
|
||||||
|
28201000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// unknown tenant slug(resolveTenant 失敗)
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/auth/login/mfa', {
|
||||||
|
tenant_slug: 'wrong-tenant-slug',
|
||||||
|
challenge_id: mfa.mfa_challenge_id,
|
||||||
|
code: '123456',
|
||||||
|
}),
|
||||||
|
'POST /auth/login/mfa (unknown tenant)',
|
||||||
|
404,
|
||||||
|
29301000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// missing fields
|
||||||
|
const missing = post('/api/v1/auth/login/mfa', { tenant_slug: cfg.tenantSlug });
|
||||||
|
if (missing.status !== 400) {
|
||||||
|
throw new Error(`login/mfa missing fields: expected 400 got ${missing.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otpauthUrl kept for journey reuse sanity (not used further in smoke)
|
||||||
|
if (!otpauthUrl) {
|
||||||
|
throw new Error('enrollTOTP did not return otpauthUrl');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
// smoke: POST /api/v1/auth/password/{forgot,reset}
|
||||||
|
//
|
||||||
|
// Covers forgot:
|
||||||
|
// happy — active platform 帳號寄重設 OTP
|
||||||
|
// 404 — member 不存在(28301000)
|
||||||
|
// 403 — 未驗證帳號(28505000)
|
||||||
|
// 400 — 缺少 email
|
||||||
|
// 429 — OTP 重送冷卻(29604000)
|
||||||
|
//
|
||||||
|
// Covers reset:
|
||||||
|
// 403 — OTP 錯誤(29505000)
|
||||||
|
// 404 — challenge 不存在(29301000)
|
||||||
|
// 403 — purpose 不符(用 registration challenge)(29505000)
|
||||||
|
// 400 — 新密碼太短
|
||||||
|
import { sleep } from 'k6';
|
||||||
|
import { post, checkError } from '../lib/http.js';
|
||||||
|
import { cfg } from '../lib/config.js';
|
||||||
|
import {
|
||||||
|
makeIdentity,
|
||||||
|
registerEmail,
|
||||||
|
registerAndConfirm,
|
||||||
|
passwordForgot,
|
||||||
|
} from '../lib/auth.js';
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
vus: 1,
|
||||||
|
iterations: 1,
|
||||||
|
thresholds: { checks: ['rate==1.0'] },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const active = registerAndConfirm();
|
||||||
|
|
||||||
|
// forgot happy
|
||||||
|
const forgot = passwordForgot({ email: active.identity.email });
|
||||||
|
if (!forgot.challenge_id) {
|
||||||
|
throw new Error('password/forgot happy: missing challenge_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// forgot member not found
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/auth/password/forgot', {
|
||||||
|
tenant_slug: cfg.tenantSlug,
|
||||||
|
email: `no-such-${Date.now()}@k6.local`,
|
||||||
|
}),
|
||||||
|
'POST /auth/password/forgot (member not found)',
|
||||||
|
404,
|
||||||
|
28301000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// forgot unverified account
|
||||||
|
const pending = makeIdentity('pwd-unverified');
|
||||||
|
registerEmail({
|
||||||
|
email: pending.email,
|
||||||
|
password: pending.password,
|
||||||
|
displayName: pending.displayName,
|
||||||
|
});
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/auth/password/forgot', {
|
||||||
|
tenant_slug: cfg.tenantSlug,
|
||||||
|
email: pending.email,
|
||||||
|
}),
|
||||||
|
'POST /auth/password/forgot (unverified)',
|
||||||
|
403,
|
||||||
|
28505000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// forgot missing email
|
||||||
|
const missingForgot = post('/api/v1/auth/password/forgot', { tenant_slug: cfg.tenantSlug });
|
||||||
|
if (missingForgot.status !== 400) {
|
||||||
|
throw new Error(`password/forgot missing email: expected 400 got ${missingForgot.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// forgot resend cooldown
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/auth/password/forgot', {
|
||||||
|
tenant_slug: cfg.tenantSlug,
|
||||||
|
email: active.identity.email,
|
||||||
|
}),
|
||||||
|
'POST /auth/password/forgot (resend cooldown)',
|
||||||
|
429,
|
||||||
|
29604000,
|
||||||
|
);
|
||||||
|
sleep(cfg.resendCooldownSeconds + 1);
|
||||||
|
passwordForgot({ email: active.identity.email });
|
||||||
|
|
||||||
|
// reset bad OTP
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/auth/password/reset', {
|
||||||
|
tenant_slug: cfg.tenantSlug,
|
||||||
|
challenge_id: forgot.challenge_id,
|
||||||
|
code: '000000',
|
||||||
|
new_password: 'K6-NewPass-2!',
|
||||||
|
}),
|
||||||
|
'POST /auth/password/reset (bad otp)',
|
||||||
|
403,
|
||||||
|
29505000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// reset unknown challenge — Redis miss 目前回 500(29201000),非 404
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/auth/password/reset', {
|
||||||
|
tenant_slug: cfg.tenantSlug,
|
||||||
|
challenge_id: '00000000-0000-0000-0000-000000000000',
|
||||||
|
code: '123456',
|
||||||
|
new_password: 'K6-NewPass-2!',
|
||||||
|
}),
|
||||||
|
'POST /auth/password/reset (unknown challenge)',
|
||||||
|
500,
|
||||||
|
29201000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// reset purpose mismatch — registration challenge on password reset endpoint
|
||||||
|
const regOnly = makeIdentity('pwd-purpose');
|
||||||
|
const reg = registerEmail({
|
||||||
|
email: regOnly.email,
|
||||||
|
password: regOnly.password,
|
||||||
|
displayName: regOnly.displayName,
|
||||||
|
});
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/auth/password/reset', {
|
||||||
|
tenant_slug: cfg.tenantSlug,
|
||||||
|
challenge_id: reg.challenge_id,
|
||||||
|
code: '123456',
|
||||||
|
new_password: 'K6-NewPass-2!',
|
||||||
|
}),
|
||||||
|
'POST /auth/password/reset (purpose mismatch)',
|
||||||
|
403,
|
||||||
|
29505000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// reset weak password (validate min=8)
|
||||||
|
const weak = post('/api/v1/auth/password/reset', {
|
||||||
|
tenant_slug: cfg.tenantSlug,
|
||||||
|
challenge_id: forgot.challenge_id,
|
||||||
|
code: '123456',
|
||||||
|
new_password: 'short',
|
||||||
|
});
|
||||||
|
if (weak.status !== 400) {
|
||||||
|
throw new Error(`password/reset weak password: expected 400 got ${weak.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
// smoke: POST /api/v1/auth/register/resume
|
||||||
|
//
|
||||||
|
// Covers:
|
||||||
|
// happy — 未完成註冊帳號重寄 registration OTP
|
||||||
|
// 404 — member 不存在(28301000)
|
||||||
|
// 404 — tenant 不存在(29301000)
|
||||||
|
// 409 — 帳號已驗證(28309000)
|
||||||
|
// 400 — 缺少 email
|
||||||
|
// 429 — OTP 重送冷卻(29604000)
|
||||||
|
import { sleep } from 'k6';
|
||||||
|
import { post, checkError } from '../lib/http.js';
|
||||||
|
import { cfg } from '../lib/config.js';
|
||||||
|
import {
|
||||||
|
makeIdentity,
|
||||||
|
registerEmail,
|
||||||
|
registerAndConfirm,
|
||||||
|
registerResume,
|
||||||
|
} from '../lib/auth.js';
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
vus: 1,
|
||||||
|
iterations: 1,
|
||||||
|
thresholds: { checks: ['rate==1.0'] },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
// happy: register 但不 confirm → 等冷卻後 resume 取得新 challenge
|
||||||
|
const pending = makeIdentity('resume-happy');
|
||||||
|
const reg = registerEmail({
|
||||||
|
email: pending.email,
|
||||||
|
password: pending.password,
|
||||||
|
displayName: pending.displayName,
|
||||||
|
});
|
||||||
|
sleep(cfg.resendCooldownSeconds + 1);
|
||||||
|
const resumed = registerResume({ email: pending.email });
|
||||||
|
if (!resumed.challenge_id) {
|
||||||
|
throw new Error('register/resume happy: missing challenge_id');
|
||||||
|
}
|
||||||
|
if (resumed.uid !== reg.uid) {
|
||||||
|
throw new Error(`register/resume happy: uid mismatch ${resumed.uid} vs ${reg.uid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 404 member not found
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/auth/register/resume', {
|
||||||
|
tenant_slug: cfg.tenantSlug,
|
||||||
|
email: `no-such-${Date.now()}@k6.local`,
|
||||||
|
}),
|
||||||
|
'POST /auth/register/resume (member not found)',
|
||||||
|
404,
|
||||||
|
28301000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 404 tenant not found
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/auth/register/resume', {
|
||||||
|
tenant_slug: 'no-such-tenant-slug',
|
||||||
|
email: pending.email,
|
||||||
|
}),
|
||||||
|
'POST /auth/register/resume (tenant not found)',
|
||||||
|
404,
|
||||||
|
29301000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 409 already verified
|
||||||
|
const verified = registerAndConfirm();
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/auth/register/resume', {
|
||||||
|
tenant_slug: cfg.tenantSlug,
|
||||||
|
email: verified.identity.email,
|
||||||
|
}),
|
||||||
|
'POST /auth/register/resume (already verified)',
|
||||||
|
409,
|
||||||
|
28309000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 400 missing email
|
||||||
|
const missing = post('/api/v1/auth/register/resume', { tenant_slug: cfg.tenantSlug });
|
||||||
|
if (missing.status !== 400) {
|
||||||
|
throw new Error(`register/resume missing email: expected 400 got ${missing.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 429 resend cooldown — 連續兩次 resume 同一 unverified 帳號
|
||||||
|
const cooldownUser = makeIdentity('resume-cooldown');
|
||||||
|
registerEmail({
|
||||||
|
email: cooldownUser.email,
|
||||||
|
password: cooldownUser.password,
|
||||||
|
displayName: cooldownUser.displayName,
|
||||||
|
});
|
||||||
|
sleep(cfg.resendCooldownSeconds + 1);
|
||||||
|
registerResume({ email: cooldownUser.email });
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/auth/register/resume', {
|
||||||
|
tenant_slug: cfg.tenantSlug,
|
||||||
|
email: cooldownUser.email,
|
||||||
|
}),
|
||||||
|
'POST /auth/register/resume (resend cooldown)',
|
||||||
|
429,
|
||||||
|
29604000,
|
||||||
|
);
|
||||||
|
sleep(cfg.resendCooldownSeconds + 1);
|
||||||
|
registerResume({ email: cooldownUser.email });
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// smoke: member endpoints (Bearer)
|
// smoke: member endpoints (Bearer)
|
||||||
//
|
//
|
||||||
// Covers (11 endpoints):
|
// Covers (14 endpoints + change password negatives):
|
||||||
// GET /api/v1/members/me
|
// GET /api/v1/members/me
|
||||||
// PATCH /api/v1/members/me
|
// PATCH /api/v1/members/me
|
||||||
// POST /api/v1/members/me/verifications/email/start
|
// POST /api/v1/members/me/verifications/email/start
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
// POST /api/v1/members/me/totp/enroll-confirm (negative: invalid code)
|
// POST /api/v1/members/me/totp/enroll-confirm (negative: invalid code)
|
||||||
// POST /api/v1/members/me/totp/verify (negative: not enrolled)
|
// POST /api/v1/members/me/totp/verify (negative: not enrolled)
|
||||||
// POST /api/v1/members/me/totp/backup-codes (negative: not enrolled)
|
// POST /api/v1/members/me/totp/backup-codes (negative: not enrolled)
|
||||||
// DELETE /api/v1/members/me/totp (negative: not enrolled / 404)
|
// POST /api/v1/members/me/password (negative: wrong current / no bearer / weak)
|
||||||
//
|
//
|
||||||
// Happy paths for TOTP and verification end-to-end live in journeys/.
|
// Happy paths for TOTP and verification end-to-end live in journeys/.
|
||||||
import { get, post, patch, del, checkEnvelope, checkError } from '../lib/http.js';
|
import { get, post, patch, del, checkEnvelope, checkError } from '../lib/http.js';
|
||||||
|
|
@ -26,7 +26,7 @@ export const options = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const { tokens } = registerAndConfirm();
|
const { identity, tokens } = registerAndConfirm();
|
||||||
const bearer = { Authorization: `Bearer ${tokens.access_token}` };
|
const bearer = { Authorization: `Bearer ${tokens.access_token}` };
|
||||||
|
|
||||||
// 1. GET /me
|
// 1. GET /me
|
||||||
|
|
@ -106,4 +106,37 @@ export default function () {
|
||||||
if (delRes.status !== 200 && delRes.status !== 404) {
|
if (delRes.status !== 200 && delRes.status !== 404) {
|
||||||
throw new Error(`DELETE /me/totp unexpected status ${delRes.status}: ${delRes.body}`);
|
throw new Error(`DELETE /me/totp unexpected status ${delRes.status}: ${delRes.body}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 13. POST /me/password (negative — wrong current password)
|
||||||
|
checkError(
|
||||||
|
post(
|
||||||
|
'/api/v1/members/me/password',
|
||||||
|
{ current_password: 'WrongPass-1!', new_password: 'K6-NewPass-2!' },
|
||||||
|
bearer,
|
||||||
|
),
|
||||||
|
'POST /me/password (wrong current)',
|
||||||
|
401,
|
||||||
|
29501000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 14. POST /me/password (negative — no bearer)
|
||||||
|
checkError(
|
||||||
|
post('/api/v1/members/me/password', {
|
||||||
|
current_password: identity.password,
|
||||||
|
new_password: 'K6-NewPass-2!',
|
||||||
|
}),
|
||||||
|
'POST /me/password (no bearer)',
|
||||||
|
401,
|
||||||
|
29501000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 15. POST /me/password (negative — weak new password)
|
||||||
|
const weakPwd = post(
|
||||||
|
'/api/v1/members/me/password',
|
||||||
|
{ current_password: identity.password, new_password: 'short' },
|
||||||
|
bearer,
|
||||||
|
);
|
||||||
|
if (weakPwd.status !== 400) {
|
||||||
|
throw new Error(`POST /me/password weak password: expected 400 got ${weakPwd.status}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue