From 888c8c28f02b22697e01359c1c808f7f169d8e3a Mon Sep 17 00:00:00 2001 From: nns Date: Tue, 26 May 2026 12:00:59 +0500 Subject: [PATCH] =?UTF-8?q?test(e2e):=20scenario=20roles=20=E2=80=94=20CRU?= =?UTF-8?q?D=20=D1=80=D0=BE=D0=BB=D0=B5=D0=B9,=20=D0=B7=D0=B0=D1=89=D0=B8?= =?UTF-8?q?=D1=82=D0=B0=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D0=BD=D1=8B?= =?UTF-8?q?=D1=85/=D0=B7=D0=B0=D0=BD=D1=8F=D1=82=D1=8B=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- tests/e2e/scenarios/roles.steps.ts | 116 +++++++++++++++++++++++++++++ tests/e2e/scenarios/roles.yml | 28 +++++++ 2 files changed, 144 insertions(+) create mode 100644 tests/e2e/scenarios/roles.steps.ts create mode 100644 tests/e2e/scenarios/roles.yml diff --git a/tests/e2e/scenarios/roles.steps.ts b/tests/e2e/scenarios/roles.steps.ts new file mode 100644 index 0000000..841467d --- /dev/null +++ b/tests/e2e/scenarios/roles.steps.ts @@ -0,0 +1,116 @@ +/** + * 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»).') +} diff --git a/tests/e2e/scenarios/roles.yml b/tests/e2e/scenarios/roles.yml new file mode 100644 index 0000000..1cbbb17 --- /dev/null +++ b/tests/e2e/scenarios/roles.yml @@ -0,0 +1,28 @@ +name: roles +description: | + Роли сотрудников (ТЗ 2.7.2): системные роли созданы при bootstrap и не + удаляются; кастомная роль создаётся/редактируется; роль, занятая + сотрудниками, не удаляется (409); неиспользуемая удаляется. Permission-based + авторизация на эндпоинтах НЕ реализована (после P0-5) — фиксируем как gap. + +preconditions: + reset_db: true + smoke_login_super_admin: true + +steps: + - id: step01_bootstrap + title: "Орг A + логин админа" + - id: step02_system_roles_exist + title: "Системные роли (IsSystem=true) созданы — минимум 4" + - id: step03_create_custom_role + title: "Создание кастомной роли с правами → права сохранены" + - id: step04_update_permissions + title: "Изменение прав роли применяется (PUT)" + - id: step05_delete_system_role_409 + title: "Удаление системной роли → 409" + - id: step06_delete_role_in_use_409 + title: "Удаление роли, занятой сотрудником → 409" + - id: step07_delete_unused_role_ok + title: "Удаление неиспользуемой роли → 204/200" + - id: step08_permission_authz_gap + title: "Permission-based authz не enforced на API — gap"