// 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'); } }