From c65f00c83703c6fa0e2ae7e27b168254e27b622a Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 27 Oct 2026 01:07:30 +0400 Subject: [PATCH 1/3] =?UTF-8?q?AURORA-1690:=20=D0=BF=D0=BE=D0=BA=D0=B0?= =?UTF-8?q?=D0=B7=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D1=8F=20=D0=B8=20=D1=80=D0=BE=D0=BB=D0=B5=D0=B9=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D0=BA=D0=BB=D0=B8=D0=BA=D1=83=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=BE=D1=82=D0=B8=D0=BF=20=D0=90=D0=A0=D0=9C?= =?UTF-8?q?=20=D0=9A=D0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #184 Co-Authored-By: Claude Opus 5.5 --- .../184-user-role/specs/app-shell-ui/spec.md | 10 ++++++ web/src/widgets/Sidebar/Sidebar.tsx | 4 +-- web/src/widgets/UserMenu/UserMenu.tsx | 26 ++++++++++++++ web/src/widgets/UserMenu/userInfo.test.ts | 34 +++++++++++++++++++ web/src/widgets/UserMenu/userInfo.ts | 14 ++++++++ 5 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 openspec/changes/184-user-role/specs/app-shell-ui/spec.md create mode 100644 web/src/widgets/UserMenu/UserMenu.tsx create mode 100644 web/src/widgets/UserMenu/userInfo.test.ts create mode 100644 web/src/widgets/UserMenu/userInfo.ts diff --git a/openspec/changes/184-user-role/specs/app-shell-ui/spec.md b/openspec/changes/184-user-role/specs/app-shell-ui/spec.md new file mode 100644 index 0000000..5404af6 --- /dev/null +++ b/openspec/changes/184-user-role/specs/app-shell-ui/spec.md @@ -0,0 +1,10 @@ +## ADDED Requirements + +### Requirement: Текущий пользователь и его роли в боковом меню +Клик по логотипу «АРМ КА» в раскрытом боковом меню SHALL раскрывать блок с данными текущего пользователя из `GET /me`: +ФИО, логин (email), роли (коды как отдаёт API, через запятую; без ролей — «не назначена») и филиал. Повторный клик или +клик вне блока SHALL закрывать его. Роли показываются как есть, по ним доступ на фронте не решается. + +#### Scenario: Пользователь с ролью +- **WHEN** аналитик с ролью ANALYST кликает по логотипу «АРМ КА» +- **THEN** раскрывается блок с его ФИО, email, ролью ANALYST и филиалом diff --git a/web/src/widgets/Sidebar/Sidebar.tsx b/web/src/widgets/Sidebar/Sidebar.tsx index 5f8a7f0..db12ff7 100644 --- a/web/src/widgets/Sidebar/Sidebar.tsx +++ b/web/src/widgets/Sidebar/Sidebar.tsx @@ -4,12 +4,12 @@ import { Archive, Building2, ScrollText, UserCog2, Users2 } from 'lucide-react'; import { type ReactNode, memo, useState } from 'react'; import { NavLink, useNavigate } from 'react-router-dom'; import { USERS_ADMIN } from '../../auth/permissions'; -import { Logo } from '../../components/Logo'; import { createConsolidation } from '../../services/api/consolidations'; import { getConsolidationsListThunk } from '../../store/slices/consolidations/thunks'; import { useAppDispatch } from '../../store/store'; import { quarterLabel } from '../../utils/labels'; import { NewConsolidationModal } from '../NewConsoleModal/NewConsoleModal'; +import { UserMenu } from '../UserMenu/UserMenu'; import styles from './styles.module.css'; type CollapsedIconProps = { @@ -164,7 +164,7 @@ export function Sidebar() { trigger={null}>
- {!isCollapsed && } + {!isCollapsed && } diff --git a/web/src/widgets/UserMenu/UserMenu.tsx b/web/src/widgets/UserMenu/UserMenu.tsx new file mode 100644 index 0000000..a7832de --- /dev/null +++ b/web/src/widgets/UserMenu/UserMenu.tsx @@ -0,0 +1,26 @@ +import { Popover } from 'antd'; +import { useCurrentUser } from '../../auth/MeContext'; +import { Logo } from '../../components/Logo'; +import { userInfoRows } from './userInfo'; + +/** Логотип «АРМ КА»; по клику раскрывается блок с текущим пользователем и его ролями (`GET /me`). */ +export function UserMenu({ compact = false }: { compact?: boolean }) { + const me = useCurrentUser(); + const content = ( +
+ {userInfoRows(me).map((r) => ( +
+
{r.label}
+
{r.value}
+
+ ))} +
+ ); + return ( + + + + ); +} diff --git a/web/src/widgets/UserMenu/userInfo.test.ts b/web/src/widgets/UserMenu/userInfo.test.ts new file mode 100644 index 0000000..175d921 --- /dev/null +++ b/web/src/widgets/UserMenu/userInfo.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import type { Me } from '../../api/types'; +import { userInfoRows } from './userInfo'; + +const base: Me = { + id: '1', + email: 'analyst@rshb.ru', + full_name: 'Иванов Иван', + regional_office: { id: 'o1', name: 'Центральный офис', is_head_office: true }, + business_segment: null, + roles: ['ANALYST'], + permissions: [], +}; + +describe('userInfoRows', () => { + it('показывает ФИО, логин, роль и филиал', () => { + expect(userInfoRows(base)).toEqual([ + { label: 'Пользователь', value: 'Иванов Иван' }, + { label: 'Логин', value: 'analyst@rshb.ru' }, + { label: 'Роль', value: 'ANALYST' }, + { label: 'Филиал', value: 'Центральный офис' }, + ]); + }); + + it('несколько ролей — через запятую, подпись во множественном числе', () => { + const row = userInfoRows({ ...base, roles: ['ANALYST', 'VERIFIER'] }).find((r) => r.value.includes('VERIFIER')); + expect(row).toEqual({ label: 'Роли', value: 'ANALYST, VERIFIER' }); + }); + + it('без ролей пишет «не назначена»', () => { + const row = userInfoRows({ ...base, roles: [] }).find((r) => r.label === 'Роль'); + expect(row?.value).toBe('не назначена'); + }); +}); diff --git a/web/src/widgets/UserMenu/userInfo.ts b/web/src/widgets/UserMenu/userInfo.ts new file mode 100644 index 0000000..8893196 --- /dev/null +++ b/web/src/widgets/UserMenu/userInfo.ts @@ -0,0 +1,14 @@ +import type { Me } from '../../api/types'; + +export type UserInfoRow = { label: string; value: string }; + +/** Строки блока «Текущий пользователь»: ФИО, email, роли (коды как отдаёт API), филиал. Пустые значения не показываем. */ +export function userInfoRows(me: Me): UserInfoRow[] { + const rows: UserInfoRow[] = [ + { label: 'Пользователь', value: me.full_name }, + { label: 'Логин', value: me.email }, + { label: me.roles.length > 1 ? 'Роли' : 'Роль', value: me.roles.length > 0 ? me.roles.join(', ') : 'не назначена' }, + { label: 'Филиал', value: me.regional_office?.name ?? '' }, + ]; + return rows.filter((r) => r.value !== ''); +} -- 2.47.2 From 1590302b79450c675ac6bbf8a80c274a9a19e798 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 27 Oct 2026 01:11:28 +0400 Subject: [PATCH 2/3] =?UTF-8?q?AURORA-1690:=20=D0=BD=D0=B0=D0=B7=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=80=D0=BE=D0=BB=D0=B5=D0=B9=20?= =?UTF-8?q?=D0=B2=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B5=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=20(role?= =?UTF-8?q?=5Fdetails=20=D0=B2=20/me)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /me дополнен role_details [{code, name}] (аддитивно), фронт показывает названия ролей, при их отсутствии — коды. Требования внесены в спеки authn-authz и consolidations-ui, change 184-user-role удалён. Co-Authored-By: Claude Opus 5.5 --- api/src/api/endpoints/me.py | 3 +- api/src/repositories/users.py | 6 +++- api/src/schemas/me.py | 10 ++++++- api/tests/test_me_and_dev_token.py | 15 +++++++++- .../184-user-role/specs/app-shell-ui/spec.md | 10 ------- openspec/specs/authn-authz/spec.md | 5 ++-- openspec/specs/consolidations-ui/spec.md | 13 ++++++++ web/src/api/types.ts | 8 +++++ web/src/widgets/UserMenu/UserMenu.tsx | 2 +- web/src/widgets/UserMenu/userInfo.test.ts | 30 ++++++++++++++----- web/src/widgets/UserMenu/userInfo.ts | 11 +++++-- 11 files changed, 86 insertions(+), 27 deletions(-) delete mode 100644 openspec/changes/184-user-role/specs/app-shell-ui/spec.md diff --git a/api/src/api/endpoints/me.py b/api/src/api/endpoints/me.py index 8ca157a..6826408 100644 --- a/api/src/api/endpoints/me.py +++ b/api/src/api/endpoints/me.py @@ -10,7 +10,7 @@ from fastapi import APIRouter, Depends from src.api.dependencies.auth import Principal, current_user from src.schemas.directories import RegionalOfficeBrief -from src.schemas.me import MeResponse +from src.schemas.me import MeResponse, RoleBrief router = APIRouter(tags=["me"]) @@ -28,5 +28,6 @@ async def get_me(principal: Annotated[Principal, Depends(current_user)]) -> MeRe regional_office=RegionalOfficeBrief.model_validate(office), business_segment=user.business_segment, roles=sorted(principal.roles), + role_details=[RoleBrief(code=code, name=name) for code, name in principal.role_names], permissions=sorted(principal.permissions), ) diff --git a/api/src/repositories/users.py b/api/src/repositories/users.py index aa07668..7577c4a 100644 --- a/api/src/repositories/users.py +++ b/api/src/repositories/users.py @@ -38,6 +38,8 @@ class UserWithAccess: regional_office: RegionalOffice roles: tuple[str, ...] permissions: frozenset[str] + # (код, название) активных ролей, по коду; название — `role.name`, для показа пользователю + role_names: tuple[tuple[str, str], ...] = () async def load_principal_data(session: AsyncSession, email: str) -> UserWithAccess | None: @@ -56,7 +58,7 @@ async def load_principal_data(session: AsyncSession, email: str) -> UserWithAcce ) user = aliased(AppUser, matched) statement = ( - select(user, RegionalOffice, Role.code, Permission.code) + select(user, RegionalOffice, Role.code, Permission.code, Role.name) .join(RegionalOffice, RegionalOffice.id == user.regional_office_id) .outerjoin(UserRole, UserRole.user_id == user.id) .outerjoin(Role, and_(Role.id == UserRole.role_id, Role.is_active.is_(True))) @@ -71,11 +73,13 @@ async def load_principal_data(session: AsyncSession, email: str) -> UserWithAcce raise AmbiguousUserError(email) roles = {row[2] for row in rows if row[2] is not None} permissions = frozenset(row[3] for row in rows if row[3] is not None) + role_names = {row[2]: row[4] for row in rows if row[2] is not None} return UserWithAccess( user=rows[0][0], regional_office=rows[0][1], roles=tuple(sorted(roles)), permissions=permissions, + role_names=tuple(sorted(role_names.items())), ) diff --git a/api/src/schemas/me.py b/api/src/schemas/me.py index 2365003..6c32513 100644 --- a/api/src/schemas/me.py +++ b/api/src/schemas/me.py @@ -8,8 +8,15 @@ from src.models.enums import BusinessSegment from src.schemas.directories import RegionalOfficeBrief +class RoleBrief(BaseModel): + """Роль пользователя: код (на нём завязан фронт) и название для показа.""" + + code: str + name: str + + class MeResponse(BaseModel): - """Текущий пользователь. `roles` и `permissions` — коды, отсортированные по алфавиту.""" + """Текущий пользователь. `roles` и `permissions` — коды, по алфавиту; `role_details` — те же роли с названиями.""" model_config = ConfigDict(use_enum_values=True) @@ -19,4 +26,5 @@ class MeResponse(BaseModel): regional_office: RegionalOfficeBrief business_segment: BusinessSegment | None roles: list[str] + role_details: list[RoleBrief] permissions: list[str] diff --git a/api/tests/test_me_and_dev_token.py b/api/tests/test_me_and_dev_token.py index ccb9ed2..2d9fc7f 100644 --- a/api/tests/test_me_and_dev_token.py +++ b/api/tests/test_me_and_dev_token.py @@ -108,10 +108,22 @@ async def test_me_for_ka_rf_matches_matrix(app: FastAPI, seeded: AsyncSession) - assert response.status_code == HTTP_OK, response.text body = response.json() assert body["roles"] == ["KA_RF"] + # названия ролей для показа: код + непустое имя из role.name + assert [r["code"] for r in body["role_details"]] == ["KA_RF"] + assert body["role_details"][0]["name"] assert body["permissions"] == KA_RF_PERMISSIONS assert body["permissions"] == sorted(body["permissions"]) user = (await seeded.execute(select(AppUser).where(AppUser.email == "ka_rf@armka.local"))).scalar_one() - assert set(body) == {"id", "email", "full_name", "regional_office", "business_segment", "roles", "permissions"} + assert set(body) == { + "id", + "email", + "full_name", + "regional_office", + "business_segment", + "roles", + "role_details", + "permissions", + } assert body["id"] == str(user.id) assert body["email"] == "ka_rf@armka.local" assert body["full_name"] == user.full_name @@ -185,6 +197,7 @@ async def test_me_user_without_roles_has_empty_lists(app: FastAPI, seeded: Async response = await client.get("/api/v1/me", headers=_bearer(verifier.issue_token("norole@armka.local"))) assert response.status_code == HTTP_OK assert response.json()["roles"] == [] + assert response.json()["role_details"] == [] assert response.json()["permissions"] == [] diff --git a/openspec/changes/184-user-role/specs/app-shell-ui/spec.md b/openspec/changes/184-user-role/specs/app-shell-ui/spec.md deleted file mode 100644 index 5404af6..0000000 --- a/openspec/changes/184-user-role/specs/app-shell-ui/spec.md +++ /dev/null @@ -1,10 +0,0 @@ -## ADDED Requirements - -### Requirement: Текущий пользователь и его роли в боковом меню -Клик по логотипу «АРМ КА» в раскрытом боковом меню SHALL раскрывать блок с данными текущего пользователя из `GET /me`: -ФИО, логин (email), роли (коды как отдаёт API, через запятую; без ролей — «не назначена») и филиал. Повторный клик или -клик вне блока SHALL закрывать его. Роли показываются как есть, по ним доступ на фронте не решается. - -#### Scenario: Пользователь с ролью -- **WHEN** аналитик с ролью ANALYST кликает по логотипу «АРМ КА» -- **THEN** раскрывается блок с его ФИО, email, ролью ANALYST и филиалом diff --git a/openspec/specs/authn-authz/spec.md b/openspec/specs/authn-authz/spec.md index 74b1267..e6b37a8 100644 --- a/openspec/specs/authn-authz/spec.md +++ b/openspec/specs/authn-authz/spec.md @@ -47,12 +47,13 @@ claim `AUTH_EMAIL_CLAIM` (по умолчанию `email`) и найти `app_us ### Requirement: `GET /api/v1/me` Маршрут SHALL возвращать текущего пользователя: `id`, `email`, `full_name`, `regional_office {id, name, is_head_office}`, -`business_segment`, `roles` (список `code`), `permissions` (отсортированный список `code`). Это единственный маршрут +`business_segment`, `roles` (список `code`), `role_details` (те же активные роли списком `{code, name}`, `name` — `role.name`, +порядок как в `roles`), `permissions` (отсортированный список `code`). Это единственный маршрут Среза 0, доступный любому аутентифицированному пользователю без `require`. #### Scenario: Ответ для dev-пользователя - **WHEN** `ka_rf@armka.local` (роль `KA_RF`) запрашивает `/api/v1/me` -- **THEN** ответ 200 содержит `roles = ["KA_RF"]` и `permissions`, равные правам `KA_RF` из матрицы §10 +- **THEN** ответ 200 содержит `roles = ["KA_RF"]`, `role_details` с одним элементом `{code: "KA_RF", name: <непустое название>}` и `permissions`, равные правам `KA_RF` из матрицы §10 ### Requirement: Dev-эмитент токенов только в режиме разработки При `AUTH_DEV_ISSUER=true` **и** `DEBUG=true` ядро SHALL предоставлять `POST /api/v1/dev/token` с телом `{"email"}`, diff --git a/openspec/specs/consolidations-ui/spec.md b/openspec/specs/consolidations-ui/spec.md index 40a3561..dae7501 100644 --- a/openspec/specs/consolidations-ui/spec.md +++ b/openspec/specs/consolidations-ui/spec.md @@ -257,3 +257,16 @@ SHALL быть неактивной с подсказкой «Возьмите #### Scenario: Ничего не введено - **WHEN** модалка «Изменить статус» открыта, этап не выбран, комментарий пуст, аналитик закрывает вкладку - **THEN** вкладка закрывается без диалога + +### Requirement: Текущий пользователь и его роли в боковом меню +Клик по логотипу «АРМ КА» в раскрытом боковом меню SHALL раскрывать блок с данными из `GET /me`: ФИО, логин (email), +роли (названия из `role_details[].name` через запятую; если названия нет — код роли; без ролей — «не назначена») и филиал. +Повторный клик или клик вне блока SHALL закрывать его. Названия — только для показа, логика фронта по ним не решается. + +#### Scenario: Пользователь с несколькими ролями +- **WHEN** пользователь с ролями KA_RF и RKA кликает по логотипу «АРМ КА» +- **THEN** раскрывается блок с его ФИО, email, подписью «Роли» и названиями обеих ролей (не кодами) и филиалом + +#### Scenario: Название роли недоступно +- **WHEN** в `role_details` нет названия для роли пользователя +- **THEN** в блоке показан код этой роли diff --git a/web/src/api/types.ts b/web/src/api/types.ts index d153092..d6166a0 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -34,9 +34,17 @@ export interface Me { regional_office: RegionalOfficeBrief; business_segment: BusinessSegment | null; roles: string[]; + /** Те же роли с названиями (`role.name`) для показа; на коды завязана логика, на названия — только интерфейс. */ + role_details: RoleBrief[]; permissions: string[]; } +/** Роль в `GET /me`: код и название. */ +export interface RoleBrief { + code: string; + name: string; +} + /** `POST /dev/token`: JWT для заголовка `Authorization: Bearer` (только при AUTH_DEV_ISSUER и DEBUG). */ export interface DevTokenResponse { access_token: string; diff --git a/web/src/widgets/UserMenu/UserMenu.tsx b/web/src/widgets/UserMenu/UserMenu.tsx index a7832de..3da10c5 100644 --- a/web/src/widgets/UserMenu/UserMenu.tsx +++ b/web/src/widgets/UserMenu/UserMenu.tsx @@ -10,7 +10,7 @@ export function UserMenu({ compact = false }: { compact?: boolean }) {
{userInfoRows(me).map((r) => (
-
{r.label}
+
{r.label}
{r.value}
))} diff --git a/web/src/widgets/UserMenu/userInfo.test.ts b/web/src/widgets/UserMenu/userInfo.test.ts index 175d921..b9666a9 100644 --- a/web/src/widgets/UserMenu/userInfo.test.ts +++ b/web/src/widgets/UserMenu/userInfo.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import type { Me } from '../../api/types'; -import { userInfoRows } from './userInfo'; +import { roleLabels, userInfoRows } from './userInfo'; const base: Me = { id: '1', @@ -8,27 +8,41 @@ const base: Me = { full_name: 'Иванов Иван', regional_office: { id: 'o1', name: 'Центральный офис', is_head_office: true }, business_segment: null, - roles: ['ANALYST'], + roles: ['KA_RF'], + role_details: [{ code: 'KA_RF', name: 'Кредитный аналитик РФ' }], permissions: [], }; describe('userInfoRows', () => { - it('показывает ФИО, логин, роль и филиал', () => { + it('показывает ФИО, логин, название роли и филиал', () => { expect(userInfoRows(base)).toEqual([ { label: 'Пользователь', value: 'Иванов Иван' }, { label: 'Логин', value: 'analyst@rshb.ru' }, - { label: 'Роль', value: 'ANALYST' }, + { label: 'Роль', value: 'Кредитный аналитик РФ' }, { label: 'Филиал', value: 'Центральный офис' }, ]); }); - it('несколько ролей — через запятую, подпись во множественном числе', () => { - const row = userInfoRows({ ...base, roles: ['ANALYST', 'VERIFIER'] }).find((r) => r.value.includes('VERIFIER')); - expect(row).toEqual({ label: 'Роли', value: 'ANALYST, VERIFIER' }); + it('несколько ролей — названия через запятую, подпись во множественном числе', () => { + const me: Me = { + ...base, + roles: ['KA_RF', 'RKA'], + role_details: [ + { code: 'KA_RF', name: 'Кредитный аналитик РФ' }, + { code: 'RKA', name: 'Руководитель КА' }, + ], + }; + const row = userInfoRows(me).find((r) => r.label === 'Роли'); + expect(row?.value).toBe('Кредитный аналитик РФ, Руководитель КА'); + }); + + it('без названия показывает код роли', () => { + expect(roleLabels({ ...base, role_details: [] })).toEqual(['KA_RF']); + expect(roleLabels({ ...base, role_details: [{ code: 'KA_RF', name: '' }] })).toEqual(['KA_RF']); }); it('без ролей пишет «не назначена»', () => { - const row = userInfoRows({ ...base, roles: [] }).find((r) => r.label === 'Роль'); + const row = userInfoRows({ ...base, roles: [], role_details: [] }).find((r) => r.label === 'Роль'); expect(row?.value).toBe('не назначена'); }); }); diff --git a/web/src/widgets/UserMenu/userInfo.ts b/web/src/widgets/UserMenu/userInfo.ts index 8893196..218d1c0 100644 --- a/web/src/widgets/UserMenu/userInfo.ts +++ b/web/src/widgets/UserMenu/userInfo.ts @@ -2,12 +2,19 @@ import type { Me } from '../../api/types'; export type UserInfoRow = { label: string; value: string }; -/** Строки блока «Текущий пользователь»: ФИО, email, роли (коды как отдаёт API), филиал. Пустые значения не показываем. */ +/** Названия ролей для показа: `role_details[].name`, а если названия нет (или старый бэк без `role_details`) — код роли. */ +export function roleLabels(me: Me): string[] { + const names = new Map((me.role_details ?? []).map((r) => [r.code, r.name])); + return me.roles.map((code) => names.get(code) || code); +} + +/** Строки блока «Текущий пользователь»: ФИО, email, роли (названия), филиал. Пустые значения не показываем. */ export function userInfoRows(me: Me): UserInfoRow[] { + const roles = roleLabels(me); const rows: UserInfoRow[] = [ { label: 'Пользователь', value: me.full_name }, { label: 'Логин', value: me.email }, - { label: me.roles.length > 1 ? 'Роли' : 'Роль', value: me.roles.length > 0 ? me.roles.join(', ') : 'не назначена' }, + { label: roles.length > 1 ? 'Роли' : 'Роль', value: roles.length > 0 ? roles.join(', ') : 'не назначена' }, { label: 'Филиал', value: me.regional_office?.name ?? '' }, ]; return rows.filter((r) => r.value !== ''); -- 2.47.2 From 73a2a53e246af0423c06c6353b62822c4f7e26e8 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 27 Oct 2026 10:19:31 +0400 Subject: [PATCH 3/3] =?UTF-8?q?AURORA-1690:=20=D1=82=D0=B5=D1=81=D1=82=20?= =?UTF-8?q?=D1=81=D1=85=D0=B5=D0=BC=D1=8B=20MeResponse=20=E2=80=94=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=B5=20role=5Fdetails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- api/tests/test_auth_deps.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/tests/test_auth_deps.py b/api/tests/test_auth_deps.py index 1e0da35..7ed0a08 100644 --- a/api/tests/test_auth_deps.py +++ b/api/tests/test_auth_deps.py @@ -495,6 +495,7 @@ def test_me_response_schema_serializes_enum_and_null_segment() -> None: "full_name": "А", "regional_office": office, "roles": ["KA_GO"], + "role_details": [{"code": "KA_GO", "name": "КА ГО"}], "permissions": ["KFO_VIEW"], } empty = MeResponse(business_segment=None, **fields) -- 2.47.2