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>
71 lines
4.1 KiB
TypeScript
71 lines
4.1 KiB
TypeScript
/**
|
||
* 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(',')}` })
|
||
}
|