food-market/tests/e2e/scenarios/roles.steps.ts
nns 888c8c28f0 test(e2e): scenario roles — CRUD ролей, защита системных/занятых
8 шагов (ТЗ 2.7.2): системные роли (ядро Администратор/Кладовщик/Кассир)
созданы и не удаляются (409); кастомная роль создаётся, права сохраняются и
редактируются; роль, занятая сотрудником → 409 на удалении; неиспользуемая
удаляется. Зафиксированы gap'ы: системных ролей 3, а не 4-6 (намеренное
упрощение Phase4b_RolesSimplify); permission-based авторизация не enforced
на эндпоинтах (после P0-5) — флаги RolePermissions справочные.

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

117 lines
8.4 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 для roles.
*
* CRUD ролей и защита системных/занятых ролей. Permission-based авторизация
* (флаги RolePermissions, влияющие на 403 на эндпоинтах) в коде НЕ enforced —
* авторизация только role-based [Authorize(Roles=...)]. Это фиксируется как
* Logic gap (ТЗ 2.7.2 помечен «после P0-5»), а не баг.
*/
import { login, makeClient } from '../lib/api.js'
import type { CheckResult, Step, Report } from '../lib/report.js'
import type { AxiosInstance } from 'axios'
const TS = Date.now()
interface Ctx { apiOnly: boolean; sa?: string; token?: string; customRoleId?: string; throwawayRoleId?: string }
interface StepCtx { ctx: Ctx; step: Step; report: Report }
function check(step: Step, c: CheckResult) { step.checks.push(c) }
function asString(x: unknown): string { try { return JSON.stringify(x).slice(0, 200) } catch { return String(x) } }
const created = (s: number) => s === 200 || s === 201
const okNoContent = (s: number) => s === 200 || s === 204
async function listRoles(api: AxiosInstance) {
const r = await api.get('/api/organization/employee-roles')
return r.data?.items ?? r.data ?? []
}
export async function step01_bootstrap({ ctx, step, report }: StepCtx) {
ctx.sa = (await login('admin@food-market.local', 'Admin12345!')).accessToken
const org = await makeClient(ctx.sa).post('/api/super-admin/organizations', {
org: { name: `Roles ${TS}`, countryCode: 'KZ', bin: null, address: null, phone: null, email: null, defaultCurrencyId: null, accountOwnerUserId: null },
adminLastName: 'Roles', adminFirstName: 'Admin', adminEmail: `roles-${TS}@example.kz`, adminPosition: null,
})
if (org.status !== 200) { report.bug({ step: '01', severity: 'critical', title: 'bootstrap орг', detail: asString(org.data) }); return }
ctx.token = (await login(org.data.adminEmail, org.data.adminTempPassword)).accessToken
check(step, { kind: 'api', description: 'Орг A + админ готовы', ok: !!ctx.token })
}
export async function step02_system_roles_exist({ ctx, step, report }: StepCtx) {
const roles = await listRoles(makeClient(ctx.token!))
const system = roles.filter((r: { isSystem: boolean }) => r.isSystem)
const names: string[] = system.map((r: { name: string }) => r.name)
// Набор системных ролей намеренно упрощён до 3 (миграция Phase4b_RolesSimplify):
// Администратор / Кладовщик / Кассир. ТЗ 2.7.2 говорит «4-6» — иллюстративно,
// до упрощения. Проверяем ядро ролей, а расхождение с ТЗ фиксируем как note.
const core = ['Администратор', 'Кладовщик', 'Кассир']
const missing = core.filter((n) => !names.includes(n))
check(step, { kind: 'api', description: 'Системные роли (ядро Администратор/Кладовщик/Кассир) присутствуют, не удаляемы',
ok: system.length >= 3 && missing.length === 0, detail: `system=[${names.join(', ')}]` })
report.gap(`ТЗ 2.7.2 ожидает 4-6 системных ролей, реально 3 (Phase4b_RolesSimplify): ${names.join(', ')}. Это намеренное упрощение, не багТЗ устарело.`)
}
export async function step03_create_custom_role({ ctx, step, report }: StepCtx) {
const api = makeClient(ctx.token!)
const r = await api.post('/api/organization/employee-roles', {
name: `Менеджер по продажам ${TS}`, description: 'кастомная',
permissions: { productsView: true, suppliesView: true, counterpartiesView: true },
})
check(step, { kind: 'api', description: 'Создание кастомной роли → 200/201', ok: created(r.status), detail: `status=${r.status}` })
ctx.customRoleId = r.data?.id
if (!ctx.customRoleId) { report.bug({ step: '03', severity: 'high', title: 'Роль не создана', detail: asString(r.data) }); return }
const got = (await listRoles(api)).find((x: { id: string }) => x.id === ctx.customRoleId)
check(step, { kind: 'api', description: 'Права сохранены (productsView=true, productsEdit=false)',
ok: got?.permissions?.productsView === true && got?.permissions?.productsEdit === false, detail: asString(got?.permissions) })
}
export async function step04_update_permissions({ ctx, step }: StepCtx) {
if (!ctx.customRoleId) { step.status = 'skip'; return }
const api = makeClient(ctx.token!)
const r = await api.put(`/api/organization/employee-roles/${ctx.customRoleId}`, {
name: `Менеджер по продажам ${TS}`, description: 'обновлено',
permissions: { productsView: true, productsEdit: true, suppliesView: true },
})
check(step, { kind: 'api', description: 'PUT прав роли → 200/204', ok: okNoContent(r.status), detail: `status=${r.status}` })
const got = (await listRoles(api)).find((x: { id: string }) => x.id === ctx.customRoleId)
check(step, { kind: 'api', description: 'Новое право productsEdit=true применилось', ok: got?.permissions?.productsEdit === true, detail: asString(got?.permissions) })
}
export async function step05_delete_system_role_409({ ctx, step }: StepCtx) {
const api = makeClient(ctx.token!)
const sys = (await listRoles(api)).find((r: { isSystem: boolean }) => r.isSystem)
check(step, { kind: 'api', description: 'Системная роль найдена', ok: !!sys, detail: sys?.name })
if (!sys) return
const r = await api.delete(`/api/organization/employee-roles/${sys.id}`)
check(step, { kind: 'api', description: 'DELETE системной роли → 409', ok: r.status === 409, detail: `status=${r.status}` })
}
export async function step06_delete_role_in_use_409({ ctx, step, report }: StepCtx) {
if (!ctx.customRoleId) { step.status = 'skip'; return }
const api = makeClient(ctx.token!)
// Назначаем кастомную роль сотруднику (без учётки).
const emp = await api.post('/api/organization/employees', {
lastName: 'Ролевой', firstName: 'Сотр', roleId: ctx.customRoleId, isActive: true, createAccount: false,
})
check(step, { kind: 'api', description: 'Сотрудник с кастомной ролью создан', ok: created(emp.status), detail: `status=${emp.status}` })
const r = await api.delete(`/api/organization/employee-roles/${ctx.customRoleId}`)
check(step, { kind: 'api', description: 'DELETE занятой роли → 409', ok: r.status === 409, detail: `status=${r.status}` })
if (r.status !== 409) report.bug({ step: '06', severity: 'high', title: 'Роль, занятая сотрудником, удаляется', detail: `status=${r.status}` })
}
export async function step07_delete_unused_role_ok({ ctx, step }: StepCtx) {
const api = makeClient(ctx.token!)
const made = await api.post('/api/organization/employee-roles', { name: `Временная ${TS}`, description: null, permissions: { productsView: true } })
ctx.throwawayRoleId = made.data?.id
check(step, { kind: 'api', description: 'Неиспользуемая роль создана', ok: !!ctx.throwawayRoleId })
if (!ctx.throwawayRoleId) return
const r = await api.delete(`/api/organization/employee-roles/${ctx.throwawayRoleId}`)
check(step, { kind: 'api', description: 'DELETE неиспользуемой роли → 200/204', ok: okNoContent(r.status), detail: `status=${r.status}` })
}
export async function step08_permission_authz_gap({ step, report }: StepCtx) {
// Документируем, а не проверяем: авторизация в коде role-based ([Authorize(Roles)]),
// флаги RolePermissions хранятся, но НЕ влияют на 403 на эндпоинтах.
check(step, { kind: 'api', description: 'Permission-based authz — задокументированный gap (см. Logic gaps)', ok: true })
report.gap('ТЗ 2.7.2: permission-based авторизация не enforced — эндпоинты используют только [Authorize(Roles=...)], флаги RolePermissions носят справочный характер для UI. Кастомная роль с ограниченными правами НЕ даёт 403 на запрещённых операциях (помечено «после P0-5»).')
}