food-market/tests/e2e/scenarios/platform-smtp.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

89 lines
5.8 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 для platform-smtp.
*
* SMTP-настройки платформы (SuperAdmin). Безопасность: пароль хранится
* зашифрованным (DataProtection) и НИКОГДА не отдаётся клиентом — только флаг
* hasSmtpPassword. Изменение требует осмысленную причину (≥10) для журнала.
* В конце возвращаем настройки в чистое состояние, чтобы не ломать
* forgot-password в других сценариях.
*/
import { login, makeClient } from '../lib/api.js'
import { psql } from '../lib/db.js'
import type { CheckResult, Step, Report } from '../lib/report.js'
interface Ctx { apiOnly: boolean; sa?: string }
interface StepCtx { ctx: Ctx; step: Step; report: Report }
function check(step: Step, c: CheckResult) { step.checks.push(c) }
function q1(sql: string): string { return (psql(sql).trim().split('\n')[0] ?? '').trim() }
const PLAINTEXT = 'SuperSecret123pwd'
async function ensureSa(ctx: Ctx) {
if (!ctx.sa) ctx.sa = (await login('admin@food-market.local', 'Admin12345!')).accessToken
return makeClient(ctx.sa)
}
export async function step01_clean_state({ ctx, step }: StepCtx) {
const api = await ensureSa(ctx)
const r = await api.put('/api/super-admin/platform-settings', {
reason: 'Сброс SMTP для e2e-теста', smtpHost: null, smtpPort: null, smtpUseSsl: false,
smtpStartTls: false, smtpUsername: null, newSmtpPassword: '__clear__', fromEmail: null, fromName: null,
})
check(step, { kind: 'api', description: 'PUT очистки → 204', ok: r.status === 204, detail: `status=${r.status}` })
const get = await api.get('/api/super-admin/platform-settings')
check(step, { kind: 'api', description: 'hasSmtpPassword=false после очистки', ok: get.data?.hasSmtpPassword === false, detail: `has=${get.data?.hasSmtpPassword}` })
}
export async function step02_reason_required({ ctx, step }: StepCtx) {
const api = await ensureSa(ctx)
const empty = await api.put('/api/super-admin/platform-settings', { reason: '', smtpHost: 'smtp.example.com' })
check(step, { kind: 'api', description: 'PUT без причины → 400', ok: empty.status === 400, detail: `status=${empty.status}` })
const short = await api.put('/api/super-admin/platform-settings', { reason: 'коротко', smtpHost: 'smtp.example.com' })
check(step, { kind: 'api', description: 'PUT причина <10 → 400', ok: short.status === 400, detail: `status=${short.status}` })
}
export async function step03_test_send_not_configured({ ctx, step }: StepCtx) {
const api = await ensureSa(ctx)
const r = await api.post('/api/super-admin/platform-settings/test-send', { toEmail: 'nobody@example.kz' })
check(step, { kind: 'api', description: 'test-send без настроенного SMTP → 400', ok: r.status === 400, detail: `status=${r.status}` })
}
export async function step04_save_smtp({ ctx, step }: StepCtx) {
const api = await ensureSa(ctx)
const r = await api.put('/api/super-admin/platform-settings', {
reason: 'Настройка SMTP-сервера для рассылок', smtpHost: 'smtp.example.com', smtpPort: 587,
smtpUseSsl: false, smtpStartTls: true, smtpUsername: 'mailer@example.com',
newSmtpPassword: PLAINTEXT, fromEmail: 'no-reply@example.com', fromName: 'Food Market',
})
check(step, { kind: 'api', description: 'PUT валидной конфигурации → 204', ok: r.status === 204, detail: `status=${r.status}` })
const get = await api.get('/api/super-admin/platform-settings')
check(step, { kind: 'api', description: 'hasSmtpPassword=true', ok: get.data?.hasSmtpPassword === true, detail: `has=${get.data?.hasSmtpPassword}` })
check(step, { kind: 'api', description: 'smtpHost/username сохранены и возвращаются',
ok: get.data?.smtpHost === 'smtp.example.com' && get.data?.smtpUsername === 'mailer@example.com',
detail: `host=${get.data?.smtpHost}` })
}
export async function step05_password_encrypted({ ctx, step, report }: StepCtx) {
const api = await ensureSa(ctx)
const get = await api.get('/api/super-admin/platform-settings')
const raw = JSON.stringify(get.data ?? {})
check(step, { kind: 'api', description: 'Ответ GET не содержит пароль в открытом виде',
ok: !raw.includes(PLAINTEXT), detail: raw.includes(PLAINTEXT) ? 'НАЙДЕН ПЛЕЙНТЕКСТ' : 'ok' })
if (raw.includes(PLAINTEXT)) report.bug({ step: '05', severity: 'critical', title: 'SMTP-пароль возвращается клиенту в открытом виде', detail: '' })
const enc = q1(`SELECT "SmtpPasswordEncrypted" FROM platform_settings LIMIT 1`)
check(step, { kind: 'db', description: 'В БД пароль не плейнтекст и не пуст',
ok: !!enc && enc !== PLAINTEXT, detail: enc === PLAINTEXT ? 'ПЛЕЙНТЕКСТ В БД' : `len=${enc.length}` })
if (enc === PLAINTEXT) report.bug({ step: '05', severity: 'critical', title: 'SMTP-пароль хранится в БД в открытом виде', detail: '' })
}
export async function step06_clear_password({ ctx, step }: StepCtx) {
const api = await ensureSa(ctx)
const r = await api.put('/api/super-admin/platform-settings', {
reason: 'Очистка пароля и сброс хоста после теста', smtpHost: null, smtpPort: null,
smtpUseSsl: false, smtpStartTls: false, smtpUsername: null, newSmtpPassword: '__clear__', fromEmail: null, fromName: null,
})
check(step, { kind: 'api', description: 'PUT __clear__ → 204', ok: r.status === 204, detail: `status=${r.status}` })
const get = await api.get('/api/super-admin/platform-settings')
check(step, { kind: 'api', description: 'hasSmtpPassword=false', ok: get.data?.hasSmtpPassword === false, detail: `has=${get.data?.hasSmtpPassword}` })
}