food-market/tests/e2e/scenarios/auth-password.steps.ts
nns a04b4bf2dd test(e2e): scenarios platform-smtp + auth-password
platform-smtp (ТЗ 2.9, 6 шагов): причина изменения обязательна (≥10),
test-send без настроек → 400, пароль шифруется в БД (не плейнтекст) и никогда
не возвращается клиентом, сентинел __clear__ очищает пароль.

auth-password (ТЗ 2.1.3, 6 шагов): анти-энумерация (forgot всегда 200),
reset с битым токеном / коротким паролем → 400, рейт-лимит forgot (>3/час
с IP → 429).

Оба сценария зелёные, багов в этих областях нет.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:56:29 +05:00

71 lines
4.1 KiB
TypeScript
Raw 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.

/**
* Step-handlers для auth-password.
*
* forgot/reset password. Анти-энумерация: forgot всегда отвечает 200,
* независимо от существования email (не раскрываем, есть ли такой аккаунт).
* reset валидирует токен и длину пароля. Рейт-лимит forgot отбивает перебор.
*/
import { login, makeClient } from '../lib/api.js'
import type { CheckResult, Step, Report } from '../lib/report.js'
import axios from 'axios'
import { Agent as HttpsAgent } from 'node:https'
const TS = Date.now()
const httpsAgent = new HttpsAgent({ rejectUnauthorized: false })
const BASE = process.env.E2E_ADMIN_URL ?? 'https://admin.food-market.kz'
interface Ctx { apiOnly: boolean; sa?: string; knownEmail?: string }
interface StepCtx { ctx: Ctx; step: Step; report: Report }
function check(step: Step, c: CheckResult) { step.checks.push(c) }
async function post(path: string, body: unknown) {
const res = await axios.post(`${BASE}${path}`, body, { httpsAgent, validateStatus: () => true })
return { status: res.status, data: res.data }
}
export async function step01_bootstrap({ ctx, step, report }: StepCtx) {
ctx.sa = (await login('admin@food-market.local', 'Admin12345!')).accessToken
const r = await makeClient(ctx.sa).post('/api/super-admin/organizations', {
org: { name: `Pwd ${TS}`, countryCode: 'KZ', bin: null, address: null, phone: null, email: null, defaultCurrencyId: null, accountOwnerUserId: null },
adminLastName: 'Pwd', adminFirstName: 'Admin', adminEmail: `pwd-${TS}@example.kz`, adminPosition: null,
})
ctx.knownEmail = r.data?.adminEmail
check(step, { kind: 'api', description: 'Орг + известный email готовы', ok: !!ctx.knownEmail, detail: ctx.knownEmail })
}
export async function step02_forgot_unknown_200({ step }: StepCtx) {
const r = await post('/api/auth/forgot-password', { email: `nobody-${TS}@example.kz` })
check(step, { kind: 'api', description: 'forgot несуществующего → 200 (анти-энумерация)', ok: r.status === 200, detail: `status=${r.status}` })
}
export async function step03_forgot_known_200({ ctx, step }: StepCtx) {
if (!ctx.knownEmail) { step.status = 'skip'; return }
const r = await post('/api/auth/forgot-password', { email: ctx.knownEmail })
check(step, { kind: 'api', description: 'forgot существующего → 200', ok: r.status === 200, detail: `status=${r.status}` })
}
export async function step04_reset_bad_token({ ctx, step }: StepCtx) {
if (!ctx.knownEmail) { step.status = 'skip'; return }
const r = await post('/api/auth/reset-password', { email: ctx.knownEmail, token: 'totally-invalid-token', newPassword: 'NewPass12345!' })
check(step, { kind: 'api', description: 'reset с битым токеном → 400', ok: r.status === 400, detail: `status=${r.status}` })
}
export async function step05_reset_short_password({ ctx, step }: StepCtx) {
if (!ctx.knownEmail) { step.status = 'skip'; return }
const r = await post('/api/auth/reset-password', { email: ctx.knownEmail, token: 'any-token', newPassword: 'short' })
check(step, { kind: 'api', description: 'reset со слишком коротким паролем → 400', ok: r.status === 400, detail: `status=${r.status}` })
}
export async function step06_forgot_rate_limited({ step, report }: StepCtx) {
// Серия forgot с одного IP. Лимит >3/час → ожидаем 429 среди ответов.
const statuses: number[] = []
for (let i = 0; i < 6; i++) {
const r = await post('/api/auth/forgot-password', { email: `flood-${TS}-${i}@example.kz` })
statuses.push(r.status)
}
const got429 = statuses.includes(429)
check(step, { kind: 'api', description: 'Серия forgot упирается в 429 (рейт-лимит)', ok: got429, detail: `statuses=${statuses.join(',')}` })
if (!got429) report.bug({ step: '06', severity: 'medium',
title: 'forgot-password не рейт-лимитится', detail: `Перебор email не отбивается 429 (ТЗ 2.1.3). statuses=${statuses.join(',')}` })
}