From f264c802b3187d9092bf7e5747d47af6d9133aaa Mon Sep 17 00:00:00 2001 From: Vladimir Valekzhanin Date: Thu, 15 Oct 2026 13:55:26 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=B0=D0=BA=D1=82=D1=83=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D1=81?= =?UTF-8?q?=D0=BF=D0=B5=D1=86=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8E=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=20=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B8=D0=BC=D0=B5=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=B2=20pages.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .opencode/commands/issue-branch-commit-pr.md | 13 ++ .opencode/opencode.json | 10 ++ docs/pages.md | 116 ++++++++++++++++++ ...20\260\320\275\320\270\321\206\321\213.md" | 91 -------------- 4 files changed, 139 insertions(+), 91 deletions(-) create mode 100644 .opencode/commands/issue-branch-commit-pr.md create mode 100644 .opencode/opencode.json create mode 100644 docs/pages.md delete mode 100644 "docs/\321\201\321\202\321\200\320\260\320\275\320\270\321\206\321\213.md" diff --git a/.opencode/commands/issue-branch-commit-pr.md b/.opencode/commands/issue-branch-commit-pr.md new file mode 100644 index 0000000..a5ded96 --- /dev/null +++ b/.opencode/commands/issue-branch-commit-pr.md @@ -0,0 +1,13 @@ +--- +description: Create an issue, create a branch, commit changes, create a pr +agent: build +--- + +# issue-branch-commit-pr + +## Workflow + +1. Create an issue. +2. Check what branch you are on. If you are on a branch other than main, stash all changes, checkout to main, pull latest changes (`--ff-only`), and unstash changes. If no conflicts, create a new branch. +3. Commit any unstaged and staged files. +4. create a pr, link it to the issue, set appropriate labels, assignees (by default - the user) and reviewers. \ No newline at end of file diff --git a/.opencode/opencode.json b/.opencode/opencode.json new file mode 100644 index 0000000..aef72f9 --- /dev/null +++ b/.opencode/opencode.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://opencode.ai/config.json", + "commands": { + // This becomes the name of the command + "issue-branch-commit-pr": { + "template": "Create an issue, create a branch, commit changes, create a pr.", + "agent": "build", + } + } +} \ No newline at end of file diff --git a/docs/pages.md b/docs/pages.md new file mode 100644 index 0000000..a130d9c --- /dev/null +++ b/docs/pages.md @@ -0,0 +1,116 @@ +# Страницы приложения + +## RBAC + +Иерархия: `USER <- MODER <- ADMIN` (старшая наследует права младшей). +Доступ `USER` означает `USER, MODER, ADMIN`; `MODER` означает `MODER, ADMIN`. + +Маршрут — путь к FXML для `Navigator.goTo(...)`. + +## Паттерн взаимодействия с API + +UI не ходит в HTTP напрямую. Контроллер вызывает сервис («middleware»): +`Controller → services.getXService() → ApiClient → HTTP + Session (Bearer)`. +Сервис кладёт пользователя/токены в `Session`, ошибки пробрасывает как +`ServiceException`, контроллер показывает их через `Alert`. +Пример: docs/examples/how-to-interact-with-api-from-ui-via-middleware.md. + +| № | Название страницы | Маршрут (FXML) | Доступ | Назначение | Ответственный | Статус | +| --- | -------------------------------------------- | ----------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ----------------------------- | +| 1 | Вход (email + пароль) | `/fxml/LoginForm.fxml` | гость (публичная) | Аутентификация через `AuthService`, после успеха → №2 | @vladcheck | ✅ готово | +| 2 | Список ресурсов и сортировка (главная) | `/fxml/ResourceList.fxml` | `USER` | Поиск по `title`, сортировка, пагинация `offset/count` (`PAGE_SIZE=20`) через `ResourceService.search` | @miyagi18 | ⚠️ частично (см. DOD №2) | +| 3 | Просмотр метаданных ресурса | `/fxml/ResourceDetails.fxml` | `USER` | Только чтение: `ResourceService.getById` → `title, description, createdAt, updatedAt, uuid`; таблица файлов (имя/размер/тип + «Скачать») | @vladcheck | ⚠️ частично (см. DOD №3) | +| 4 | Форма ресурса (создание/редактирование) | `/fxml/ResourceForm.fxml` | `MODER` | `create/update` через `ResourceService`; удаление — действием из №2/№3 с подтверждением, без отдельной страницы | @miyagi18 | ⚠️ частично (см. DOD №4) | +| 5 | Список пользователей | `/fxml/UserList.fxml` (нет) | `ADMIN` | Поиск/фильтр через `UsersService.search` + `UserFilter`; удаление действием из списка | @deatheh | ❌ нет (сервис готов, UI нет) | +| 6 | Форма пользователя (создание/редактирование) | `/fxml/UserForm.fxml` (нет) | `ADMIN` | `create` через `RegisterData`, `update` через `UpdateUserData`, `getById` для предзаполнения | @deatheh | ❌ нет (сервис готов, UI нет) | + +## DOD для ⚠️-страниц + +### №2 — Список ресурсов + +* Есть: `ResourceListController` + `ResourceList.fxml`: поле поиска по `title`, `ComboBox` сортировки (4 опции: `createdAt desc/asc`, `title asc/desc`), таблица (`title, description, createdAt, updatedAt`), пагинация `offset/count` с `PAGE_SIZE=20`, переход дабл-кликом в №3 через `Navigator.goTo(path, c -> setResourceId(uuid))`, кнопка «+ Создать документ» только для `MODER/ADMIN`. +* Нет: поля фильтра по `description` в UI нет (`ResourceFilter.description` всегда `null`); кнопки удаления в списке нет. +* Готово когда: заведено поле `description` (или зафиксировано решение «искать только по title») и заведена кнопка удаления с подтверждением (см. DOD №4). + +### №3 — Просмотр метаданных ресурса + +* Есть: `ResourceDetailsController` (read-only): метаданные + таблица файлов (`name, size, contentType` + колонка «Скачать»), скачивание через `FileService.downloadFile` (`DirectoryChooser`, имя из `Content-Disposition`, fallback `fileUuid`), вход из №2 через `setResourceId(uuid)` + `load()`, кнопка «Редактировать» только для `MODER/ADMIN` (переход в №4). +* Нет: кнопок загрузки/удаления файлов в UI нет; кнопки экспорта архива файлов нет. +* Готово когда: заведены upload/delete (`FileService.{uploadFile, deleteFile}` уже есть) и экспорт (`ExportService.exportResourceFiles` уже есть) либо зафиксировано решение «в №3 только скачивание». + +### №4 — Форма ресурса + +* Есть: `ResourceFormController` + `ResourceForm.fxml`: режимы `create/edit` по наличию `resourceId`, предзаполнение через `getById`, ручная проверка `title` (`обязательно, 3–255 символов`, пустой `description` → `null`), сохранение через `create/update`, отмена → №2. +* Нет: кнопки удаления в №2/№3/№4 нет, диалога подтверждения нет (`ResourceService.delete` реализован, UI не заведено). +* Готово когда: удаление вызывается действием из №2/№3 с диалогом подтверждения, без отдельной страницы. + +## Интеграции страниц с эндпоинтами (через сервисы) + +Базовый URL: `api.baseUrl` (`http://localhost:8080`, см. `src/main/resources/api.properties`). + +| № | Действие UI | Сервис (middleware) | ApiClient → HTTP | +| --- | --------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | Войти | `AuthService.login(email, password)` | `POST /api/v1/auth/login` (`LoginRequest`), затем `GET /api/v1/user/me`; токены → `Session` | +| 1 | Выйти | `AuthService.logout()` | `POST /api/v1/auth/logout` (`RefreshTokenRequest`), затем `Session.clear()` | +| 2 | Поиск/сортировка ресурсов (только UI-факт: поиск по `title`) | `ResourceService.search(filter, offset, count[, sortBy, sortDir])` | `GET /api/v1/resource?title=&description=&offset=&count=&sortBy=&sortDir=` (UI шлёт только `title`; `description` поддерживается сервисом, из UI всегда `null`; сортировка `createdAt/title asc/desc`) | +| 3 | Открыть метаданные | `ResourceService.getById(id)` | `GET /api/v1/resource/{id}` | +| 3 | Скачать файл (UI-факт) | `FileService.downloadFile` | `GET /api/v1/resource/{id}/file/{fileId}` (имя из `Content-Disposition`) | +| 3 | Загрузить/удалить файл (сервис готов, UI не заведено) | `FileService.{uploadFile, deleteFile}` | `POST /api/v1/resource/{id}/file` (multipart), `DELETE /api/v1/resource/{id}/file/{fileId}` | +| 3 | Экспорт файлов ресурса (сервис готов, UI не заведено) | `ExportService.exportResourceFiles(id, dir)` | `GET /api/v1/admin/resource/{id}/files/export` (бинарный ответ → файл `files-{uuid}.xlsx`) | +| 4 | Создать ресурс | `ResourceService.create(title, description)` | `POST /api/v1/resource` (`CreateResourceRequest`) | +| 4 | Обновить ресурс | `ResourceService.update(id, title, description)` | `PUT /api/v1/resource/{id}` (`UpdateResourceRequest`) | +| 4 | Удалить ресурс из №2/№3 (сервис готов, UI не заведено) | `ResourceService.delete(id)` | `DELETE /api/v1/resource/{id}` | +| 5 | Поиск/фильтр пользователей (сервис готов, UI не заведено) | `UsersService.search(filter, offset, count)` | `GET /api/v1/admin?email=&firstName=&lastName=&role=&enabled=&offset=&count=&sortBy=createdAt&sortDir=desc` (сортировка захардкожена в `UsersServiceImpl`) | +| 5 | Удалить пользователя (сервис готов, UI не заведено) | `UsersService.delete(id)` | `DELETE /api/v1/admin/{id}` | +| 5 | Экспорт пользователей/ресурсов (сервис готов, UI не заведено) | `ExportService.{exportUsers, exportResources}` | `GET /api/v1/admin/users/export` (→ `users.xlsx`), `GET /api/v1/admin/resource/export` (→ `resources.xlsx`) | +| 6 | Создать пользователя (сервис готов, UI не заведено) | `UsersService.create(RegisterData)` | `POST /api/v1/admin` (`RegisterRequest`: email, password, role, firstName, lastName) | +| 6 | Открыть пользователя для редактирования (сервис готов, UI не заведено) | `UsersService.getById(id)` | `GET /api/v1/admin/{id}` | +| 6 | Обновить пользователя (сервис готов, UI не заведено) | `UsersService.update(id, UpdateUserData)` | `PUT /api/v1/admin/{id}` (`UpdateUserRequest`: email, enabled, role, firstName, lastName) | + +Примечания: +* `ApiClient` при 401 (кроме `/api/v1/auth/*`) один раз обновляет токены через + `POST /api/v1/auth/refresh` и повторяет запрос; если refresh нет/не удался — + исходный ответ уходит в `ServiceException`. +* Серверные ошибки (`ApiException` → `ServiceException`) показывает контроллер; + проверка ввода выполняется до вызова сервиса (см. «Примечания по реализации»). + +## Примечания по реализации + +* №1: ✅ готово — `LoginFormController` только email + пароль: проверка непустоты + `EmailValidator.validate`, вызов `AuthService.login`, после успеха → №2. +* №2: переименование `Search.fxml → ResourceList.fxml` готово; список наполнен: `ResourceService.search` + пагинация `PAGE_SIZE=20`, фильтр `ResourceFilter` (факт: из UI заполняется только `title`). +* №3: частично — `ResourceDetailsController` (read-only): метаданные + таблица файлов + со скачиванием (`FileService.downloadFile`), вход из №2 — `Navigator.goTo(path, c -> ...)` + с `setResourceId(uuid)` + `load()`. Upload/delete/экспорт не заведены. +* №4: частично — `ResourceFormController` в режимах `create/edit` по наличию `resourceId` + (предзаполнение через `getById`); проверка `title` вручную (`3–255`), не через `validation/*Validator`. +* №5–№6: ❌ UI нет (`UserList.fxml`/`UserForm.fxml`, контроллеров нет). Сервисный слой готов: + `UsersServiceImpl` + `ApiClient` (`getUsersByFilters/createUser/getUserById/updateUser/deleteUser`). +* Роли: `AuthServiceImpl.hasRole` сравнивает по уровню (`USER=0 < MODER=1 < ADMIN=2`), + поэтому `isModerator()` возвращает true и для `ADMIN` — соответствует RBAC выше. +* SoC: списки (№2) не содержат логики форм; форма №4 работает в двух режимах `create/edit` по наличию `uuid`. +* Кнопки: `create` (№2) и `edit` (№3) рендерятся по роли (`isAdmin() || isModerator()` из `Session.getCurrentUser().role`), а не отдельными страницами. Кнопки `delete` не рендерятся нигде. +* Проверка ввода (факт): №1 — `EmailValidator`; №4 — ручные проверки длины; классы + `PasswordValidator/FirstNameValidator/LastNameValidator/ThirdNameValidator` на FormsFX существуют как задел под №6, в текущих страницах не используются. +* Терминология: в доке используется «ресурс» per `GLOSSARY.md` (avoid «документ»). + Факт расхождения: строки UI/FXML до сих пор говорят «документ» («Новый документ», + «Редактирование документа», «Архив документов», «Документов пока нет») — правится отдельно, не в этом доке. + +## Тесты (факт) + +Стек: JUnit5 + AssertJ; TestFX headless настроен в `maven-surefire-plugin` +(`-Dtestfx.headless=true`, Monocle, `--enable-native-access=ALL-UNNAMED`, +`-XX:+EnableDynamicAgentLoading`). WireMock в `pom.xml` отсутствует. + +* Дефолтный прогон (без бэкенда): unit + UI на in-memory моках — `ServiceRegistry.mock` + (`MockAuthService/MockUsersService/MockResourceService/MockFileService/MockExportService` + `MockStore`), + `MOCK_DATA=true` / `INITIAL_PAGE_PATH` в `Program`. Покрыты: `LoginFormController`, + `ResourceListController`, `ResourceDetailsController`, `ResourceFormController`, мапперы, валидаторы. +* Live-интеграционные (`@Tag("integration")`, `src/test/.../ui/integration/`): + исключены из дефолтного билда/CI через `excludedGroups` в surefire; требуют живой + бэкенд `localhost:8080` и `.env` (`IT_ADMIN_EMAIL`, `IT_ADMIN_PASSWORD`, `dotenv.path`). + Запуск явно: `mvn test -Dgroups=integration -DexcludedGroups=`. +* Скачивание/экспорт (факт кода, без UI-кнопок): `ApiClient.downloadFile` сохраняет в + `destinationDir`, имя из `Content-Disposition` (fallback `fileUuid`); `ExportServiceImpl` + пишет `users.xlsx` / `resources.xlsx` / `files-{uuid}.xlsx`. +* Refresh (факт кода): `ApiClient.executeWithRetry` при 401 (кроме `/api/v1/auth/*`) + один раз делает `POST /api/v1/auth/refresh` и повторяет запрос с новым Bearer без участия UI. diff --git "a/docs/\321\201\321\202\321\200\320\260\320\275\320\270\321\206\321\213.md" "b/docs/\321\201\321\202\321\200\320\260\320\275\320\270\321\206\321\213.md" deleted file mode 100644 index 5f78fe3..0000000 --- "a/docs/\321\201\321\202\321\200\320\260\320\275\320\270\321\206\321\213.md" +++ /dev/null @@ -1,91 +0,0 @@ -# Страницы приложения - -## RBAC - -Иерархия: `USER <- MODER <- ADMIN` (старшая наследует права младшей). -Доступ `USER` означает `USER, MODER, ADMIN`; `MODER` означает `MODER, ADMIN`. - -Маршрут — путь к FXML для `Navigator.goTo(...)`. - -## Паттерн взаимодействия с API - -UI не ходит в HTTP напрямую. Контроллер вызывает сервис («middleware»): -`Controller → services.getXService() → ApiClient → HTTP + Session (Bearer)`. -Сервис кладёт пользователя/токены в `Session`, ошибки пробрасывает как -`ServiceException`, контроллер показывает их через `Alert`. -Пример: docs/examples/how-to-interact-with-api-from-ui-via-middleware.md. - -| № | Название страницы | Маршрут (FXML) | Доступ | Назначение | Ответственный | -| --- | -------------------------------------------- | ------------------------------------ | ----------------- | --------------------------------------------------------------------------------------------------------------- | ------------- | -| 1 | Вход (email + пароль) | `/fxml/LoginForm.fxml` | гость (публичная) | Аутентификация через `AuthService`, после успеха → №2 | @vladcheck | -| 2 | Список ресурсов и сортировка (главная) | `/fxml/ResourceList.fxml` | `USER` | Поиск/фильтр/сортировка, пагинация `offset/count` через `ResourceService.search` | @miyagi18 | -| 3 | Просмотр метаданных документа | `/fxml/ResourceDetails.fxml` | `USER` | Только чтение: `ResourceService.getById` → `title, description, createdAt, updatedAt, uuid` | @vladcheck | -| 4 | Форма ресурса (создание/редактирование) | `/fxml/ResourceForm.fxml` (новая) | `MODER` | `create/update` через `ResourceService`; удаление — действием из №2/№3 с подтверждением, без отдельной страницы | @miyagi18 | -| 5 | Список пользователей | `/fxml/UserList.fxml` (новая) | `ADMIN` | Поиск/фильтр через `UsersService.search` + `UserFilter`; удаление действием из списка | @deatheh | -| 6 | Форма пользователя (создание/редактирование) | `/fxml/UserForm.fxml` (новая) | `ADMIN` | `create` через `RegisterData`, `update` через `UpdateUserData`, `getById` для предзаполнения | @deatheh | - -## Интеграции страниц с эндпоинтами (через сервисы) - -Базовый URL: `api.baseUrl` (`http://localhost:8080`). - -| № | Действие UI | Сервис (middleware) | ApiClient → HTTP | -| --- | --------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| 1 | Войти | `AuthService.login(email, password)` | `POST /api/v1/auth/login` (`LoginRequest`), затем `GET /api/v1/user/me`; токены → `Session` | -| 1 | Выйти | `AuthService.logout()` | `POST /api/v1/auth/logout` (`RefreshTokenRequest`), затем `Session.clear()` | -| 2 | Поиск/фильтр/сортировка ресурсов | `ResourceService.search(filter, offset, count)` | `GET /api/v1/resource?title=&description=&offset=&count=&sortBy=createdAt&sortDir=desc` | -| 3 | Открыть метаданные | `ResourceService.getById(id)` | `GET /api/v1/resource/{id}` | -| 3 | Список/скачать/загрузить/удалить файлы | `FileService.{uploadFile, downloadFile, deleteFile}` | `POST /api/v1/resource/{id}/file` (multipart), `GET /api/v1/resource/{id}/file/{fileId}`, `DELETE /api/v1/resource/{id}/file/{fileId}` | -| 3 | Экспорт файлов ресурса | `ExportService.exportResourceFiles(id, dir)` | `GET /api/v1/admin/resource/{id}/files/export` (бинарный ответ → файл) | -| 4 | Создать ресурс | `ResourceService.create(title, description)` | `POST /api/v1/resource` (`CreateResourceRequest`) | -| 4 | Обновить ресурс | `ResourceService.update(id, title, description)` | `PUT /api/v1/resource/{id}` (`UpdateResourceRequest`) | -| 4 | Удалить ресурс (из №2/№3) | `ResourceService.delete(id)` | `DELETE /api/v1/resource/{id}` | -| 5 | Поиск/фильтр пользователей | `UsersService.search(filter, offset, count)` | `GET /api/v1/admin?email=&firstName=&lastName=&role=&enabled=&offset=&count=&sortBy=createdAt&sortDir=desc` | -| 5 | Удалить пользователя (из списка) | `UsersService.delete(id)` | `DELETE /api/v1/admin/{id}` | -| 5 | Экспорт пользователей/ресурсов | `ExportService.{exportUsers, exportResources}` | `GET /api/v1/admin/users/export`, `GET /api/v1/admin/resource/export` | -| 6 | Создать пользователя | `UsersService.create(RegisterData)` | `POST /api/v1/admin` (`RegisterRequest`: email, password, role, firstName, lastName) | -| 6 | Открыть пользователя для редактирования | `UsersService.getById(id)` | `GET /api/v1/admin/{id}` | -| 6 | Обновить пользователя | `UsersService.update(id, UpdateUserData)` | `PUT /api/v1/admin/{id}` (`UpdateUserRequest`: email, enabled, role, firstName, lastName) | - -Примечания: -* `ApiClient` при 401 (кроме `/api/v1/auth/*`) один раз обновляет токены через - `POST /api/v1/auth/refresh` и повторяет запрос; если refresh нет/не удался — - исходный ответ уходит в `ServiceException`. -* Клиентская валидация (FormsFX + `validation/*Validator`) срабатывает до вызова - сервиса; серверные ошибки (`ApiException` → `ServiceException`) показывает контроллер. - -## Примечания по реализации - -* №1: ✅ готово — `LoginFormController` только email + пароль, вызов `AuthService`. -* №2: ✅ переименование `Search.fxml → ResourceList.fxml` готово; наполнить список: - `ResourceService.search` + пагинация, фильтры `ResourceFilter(title, description)`. -* №3: ✅ готово — `ResourceDetailsController` (read-only): метаданные + таблица файлов - со скачиванием (`FileService.downloadFile`), вход из №2 — `Navigator.goTo(path, c -> ...)` - с `setResourceId(uuid)` + `load()`. -* Роли: `AuthServiceImpl.hasRole` сравнивает по уровню (`USER=0 < MODER=1 < ADMIN=2`), - поэтому `isModerator()` возвращает true и для `ADMIN` — соответствует RBAC выше. -* SoC: списки (№2, №5) не содержат логики форм; формы (№4, №6) работают в двух режимах `create/edit` по наличию `uuid`, валидация через существующие `validation/*Validator`. -* Кнопки create/edit/delete рендерятся по роли из `Session.getCurrentUser().role`, а не отдельными страницами. - -## Интеграционные тесты (rough) - -Стек: JUnit5 + AssertJ уже есть; TestFX headless настроен в `maven-surefire-plugin` -(`--enable-native-access=ALL-UNNAMED`). HTTP backend-а в тестах нет — ставим -стаб: WireMock (или подменный `ApiClient`) + TestFX для UI-слоя. - -* IT-Auth (№1): мок `POST /api/v1/auth/login` → 200 с токенами + `GET /api/v1/user/me` → 200 `UserDto`; - проверить: `Session` заполнен, переход на `ResourceList`; кейс 401 → `ServiceException`, показан `Alert`, перехода нет. -* IT-ResourceList (№2): мок `GET /api/v1/resource?...` → список `ResourceDto`; - проверить: таблица/список отрисован, пагинация меняет `offset/count` в запросе; - кейс 403 для `USER` без прав — сообщение об ошибке, без падения. -* IT-ResourceDetails (№3): мок `GET /api/v1/resource/{id}` → 200 и 404; - проверить: поля метаданных показаны; 404 → `Alert` «не найден». -* IT-ResourceForm (№4): мок `POST /api/v1/resource` → 201 и `PUT /api/v1/resource/{id}` → 200; - проверить: create/edit режимы по наличию `uuid`; `DELETE /api/v1/resource/{id}` после подтверждения. -* IT-UserList/UserForm (№5–6, ADMIN): мок `GET /api/v1/admin?...` → список, - `POST /api/v1/admin` → 201, `PUT /api/v1/admin/{id}` → 200, `DELETE` → 204; - проверить: фильтр `email/role/enabled` уходит в query params; создание требует все поля `RegisterData`. -* IT-Files/Export (№3/№5): мок multipart `POST .../file` → 201 `FileDto`, - `GET .../file/{fileId}` → бинарник, `GET .../export` → бинарник; - проверить: файл сохранён в `destinationDir`, имя взято из `Content-Disposition`. -* IT-Refresh: мок защищённого `GET` → 401, затем `POST /api/v1/auth/refresh` → 200, - повторный `GET` → 200; проверить: запрос повторён с новым Bearer без участия UI.