From 87c4571170c77fe7aaed3fbc775876235e590b51 Mon Sep 17 00:00:00 2001 From: Vladimir Valekzhanin Date: Tue, 6 Oct 2026 13:40:30 +0300 Subject: [PATCH 1/5] docs: add an example of middleware --- ...nteract-with-api-from-ui-via-middleware.md | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 docs/examples/how-to-interact-with-api-from-ui-via-middleware.md diff --git a/docs/examples/how-to-interact-with-api-from-ui-via-middleware.md b/docs/examples/how-to-interact-with-api-from-ui-via-middleware.md new file mode 100644 index 0000000..e634f82 --- /dev/null +++ b/docs/examples/how-to-interact-with-api-from-ui-via-middleware.md @@ -0,0 +1,86 @@ +# how-to-interact-with-api-from-ui-via-middleware + +```java +package com.DesktopApplicationClientJava.controllers; + +import com.DesktopApplicationClientJava.controllers.utils.Controller; +import com.DesktopApplicationClientJava.entities.User; +import com.DesktopApplicationClientJava.services.ServiceException; +import com.DesktopApplicationClientJava.validation.EmailValidator; +import com.DesktopApplicationClientJava.validation.PasswordValidator; +import com.DesktopApplicationClientJava.validation.utils.ErrorMessages; +import com.dlsc.formsfx.model.structure.Field; +import com.dlsc.formsfx.model.structure.Form; +import com.dlsc.formsfx.model.structure.PasswordField; +import com.dlsc.formsfx.model.structure.Section; +import com.dlsc.formsfx.model.structure.StringField; +import com.dlsc.formsfx.model.validators.Validator; +import com.dlsc.formsfx.view.renderer.FormRenderer; +import java.util.List; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.layout.VBox; + +public class LoginFormController extends Controller { + + @FXML private VBox formContainer; + @FXML private Button submitButton; + + private StringField email; + private PasswordField password; + private Form loginForm; + + @FXML + private void initialize() { + email = + Field.ofStringType("") + .label(EmailValidator.FIELD_NAME) + .placeholder("youremail@example.com") + .required(ErrorMessages.Required) + .validate(toArray(EmailValidator.validators())); + password = + Field.ofPasswordType("") + .label(PasswordValidator.FIELD_NAME) + .required(ErrorMessages.Required) + .validate(toArray(PasswordValidator.validators())); + + loginForm = Form.of(Section.of(email, password)); + formContainer.getChildren().add(new FormRenderer(loginForm)); + } + + @FXML + private void onSubmit(ActionEvent event) { + // клиентская валидация через FormsFX до вызова сервера + boolean valid = email.validate() & password.validate(); + if (!valid) { + return; + } + + try { + // ← ВОТ ЗДЕСЬ ВЫЗОВ СЕРВИСА + User user = services.getAuthService().login(email.getValue(), password.getValue()); + if (user == null) { + return; + } + + // если дошли сюда — логин успешен, session уже обновлён сервисом + navigator.goTo("/fxml/ResourceList.fxml"); + + } catch (ServiceException e) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Ошибка входа"); + alert.setHeaderText(null); + alert.setContentText(e.getMessage()); + alert.showAndWait(); + } + } + + @SuppressWarnings("unchecked") + private static Validator[] toArray(List> validators) { + return validators.toArray(new Validator[0]); + } +} +``` From 7fcad47d2bf56b59054af92d3d2590d24517d8d2 Mon Sep 17 00:00:00 2001 From: Vladimir Valekzhanin Date: Tue, 6 Oct 2026 13:44:12 +0300 Subject: [PATCH 2/5] docs: update plan --- ...20\260\320\275\320\270\321\206\321\213.md" | 82 ++++++++++++++++--- 1 file changed, 72 insertions(+), 10 deletions(-) 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" index fe19b74..fb077c6 100644 --- "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" @@ -7,18 +7,80 @@ Маршрут — путь к FXML для `Navigator.goTo(...)`. -| № | Название страницы | Маршрут (FXML) | Доступ | Назначение | -| --- | -------------------------------------------- | ---------------------------------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------- | -| 1 | Вход (email + пароль) | `/fxml/LoginForm.fxml` | гость (публичная) | Аутентификация через `AuthService`, после успеха → №2 | -| 2 | Список ресурсов и сортировка (главная) | `/fxml/ResourceList.fxml` (переименовать из `Search.fxml`) | `USER` | Поиск/фильтр/сортировка, пагинация `offset/count` через `ResourceService.search` | -| 3 | Просмотр метаданных документа | `/fxml/ResourceDetails.fxml` (новая) | `USER` | Только чтение: `ResourceService.getById` → `title, description, createdAt, updatedAt, uuid` | -| 4 | Форма ресурса (создание/редактирование) | `/fxml/ResourceForm.fxml` (новая) | `MODER` | `create/update` через `ResourceService`; удаление — действием из №2/№3 с подтверждением, без отдельной страницы | -| 5 | Список пользователей | `/fxml/UserList.fxml` (новая) | `ADMIN` | Поиск/фильтр через `UsersService.search` + `UserFilter`; удаление действием из списка | -| 6 | Форма пользователя (создание/редактирование) | `/fxml/UserForm.fxml` (новая) | `ADMIN` | `create` через `RegisterData`, `update` через `UpdateUserData`, `getById` для предзаполнения | +## Паттерн взаимодействия с 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 | +| 2 | Список ресурсов и сортировка (главная) | `/fxml/ResourceList.fxml` | `USER` | Поиск/фильтр/сортировка, пагинация `offset/count` через `ResourceService.search` | +| 3 | Просмотр метаданных документа | `/fxml/ResourceDetails.fxml` (новая) | `USER` | Только чтение: `ResourceService.getById` → `title, description, createdAt, updatedAt, uuid` | +| 4 | Форма ресурса (создание/редактирование) | `/fxml/ResourceForm.fxml` (новая) | `MODER` | `create/update` через `ResourceService`; удаление — действием из №2/№3 с подтверждением, без отдельной страницы | +| 5 | Список пользователей | `/fxml/UserList.fxml` (новая) | `ADMIN` | Поиск/фильтр через `UsersService.search` + `UserFilter`; удаление действием из списка | +| 6 | Форма пользователя (создание/редактирование) | `/fxml/UserForm.fxml` (новая) | `ADMIN` | `create` через `RegisterData`, `update` через `UpdateUserData`, `getById` для предзаполнения | + +## Интеграции страниц с эндпоинтами (через сервисы) + +Базовый 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` — удалить `firstName/lastName/thirdName`, оставить `email + password` + вызов `AuthService`/`LoginRequest` (сейчас там заглушка `authenticate()` с `Role.USER`). -* №2: переименование `Search.fxml → ResourceList.fxml` + переименование `SearchController → ResourceListController`; обновить `Program.INITIAL_PAGE_PATH` и `navigator.goTo("/fxml/Search.fxml")` в `LoginFormController`. +* №1: ✅ готово — `LoginFormController` только email + пароль, вызов `AuthService`. +* №2: ✅ переименование `Search.fxml → ResourceList.fxml` готово; наполнить список: + `ResourceService.search` + пагинация, фильтры `ResourceFilter(title, description)`. * 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. From 5118566d87bf62e8b774a71118f97a9773df7436 Mon Sep 17 00:00:00 2001 From: Vladimir Valekzhanin Date: Tue, 6 Oct 2026 13:52:26 +0300 Subject: [PATCH 3/5] feat(login): redesign login page with Bootstrap and add tests --- pom.xml | 6 + .../DesktopApplicationClientJava/Program.java | 4 +- .../controllers/LoginFormController.java | 76 +++++------ src/main/resources/fxml/LoginForm.fxml | 38 ++++-- src/test/java/ui/LoginFormControllerTest.java | 129 ++++++++++++++++++ src/test/java/ui/LoginIntegrationTest.java | 47 +++++++ 6 files changed, 244 insertions(+), 56 deletions(-) create mode 100644 src/test/java/ui/LoginFormControllerTest.java create mode 100644 src/test/java/ui/LoginIntegrationTest.java diff --git a/pom.xml b/pom.xml index 3ea95ac..9c44ee1 100644 --- a/pom.xml +++ b/pom.xml @@ -105,6 +105,12 @@ bootstrapfx-core 0.4.0 + + org.mockito + mockito-core + 5.23.0 + test + diff --git a/src/main/java/com/DesktopApplicationClientJava/Program.java b/src/main/java/com/DesktopApplicationClientJava/Program.java index 1576b79..b6750a0 100644 --- a/src/main/java/com/DesktopApplicationClientJava/Program.java +++ b/src/main/java/com/DesktopApplicationClientJava/Program.java @@ -11,9 +11,10 @@ import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; +import org.kordamp.bootstrapfx.BootstrapFX; public class Program extends Application { - private static final String INITIAL_PAGE_PATH = "/fxml/ResourceList.fxml"; + private static final String INITIAL_PAGE_PATH = "/fxml/LoginForm.fxml"; private static final double WINDOW_WIDTH = 800.0; private static final double WINDOW_HEIGHT = 600.0; @@ -43,6 +44,7 @@ public void start(Stage stage) throws IOException { Parent root = loader.load(); Scene scene = new Scene(root); + scene.getStylesheets().add(BootstrapFX.bootstrapFXStylesheet()); loadStyleSheet(scene, "/styles.css"); stage.setScene(scene); diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java index 86d8d6f..a88dd82 100644 --- a/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java @@ -4,72 +4,58 @@ import com.DesktopApplicationClientJava.entities.User; import com.DesktopApplicationClientJava.services.ServiceException; import com.DesktopApplicationClientJava.validation.EmailValidator; -import com.DesktopApplicationClientJava.validation.PasswordValidator; -import com.DesktopApplicationClientJava.validation.utils.ErrorMessages; -import com.dlsc.formsfx.model.structure.Field; -import com.dlsc.formsfx.model.structure.Form; -import com.dlsc.formsfx.model.structure.PasswordField; -import com.dlsc.formsfx.model.structure.Section; -import com.dlsc.formsfx.model.structure.StringField; -import com.dlsc.formsfx.model.validators.Validator; -import com.dlsc.formsfx.view.renderer.FormRenderer; -import java.util.List; import javafx.event.ActionEvent; import javafx.fxml.FXML; -import javafx.scene.control.Alert; import javafx.scene.control.Button; -import javafx.scene.layout.VBox; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; public class LoginFormController extends Controller { - @FXML private VBox formContainer; - @FXML private Button submitButton; - private StringField email; - private PasswordField password; - private Form loginForm; + @FXML private TextField emailField; + @FXML private PasswordField passwordField; + @FXML private Label errorLabel; + @FXML private Button submitButton; @FXML - private void initialize() { - email = - Field.ofStringType("") - .label(EmailValidator.FIELD_NAME) - .placeholder("youremail@example.com") - .required(ErrorMessages.Required) - .validate(toArray(EmailValidator.validators())); - password = - Field.ofPasswordType("") - .label(PasswordValidator.FIELD_NAME) - .required(ErrorMessages.Required) - .validate(toArray(PasswordValidator.validators())); + private void onSubmit(ActionEvent event) { + String email = emailField.getText() == null ? "" : emailField.getText().trim(); + String password = passwordField.getText() == null ? "" : passwordField.getText(); - loginForm = Form.of(Section.of(email, password)); - formContainer.getChildren().add(new FormRenderer(loginForm)); - } + if (email.isBlank() || password.isBlank()) { + showError("Заполните все поля"); + return; + } - @FXML - private void onSubmit(ActionEvent event) { - boolean valid = email.validate() & password.validate(); - if (!valid) { + String emailError = EmailValidator.validate(email); + if (emailError != null) { + showError(emailError); return; } try { - User user = services.getAuthService().login(email.getValue(), password.getValue()); + // ← ВЫЗОВ СЕРВИСА (middleware): session уже обновлён сервисом при успехе + User user = services.getAuthService().login(email, password); if (user == null) { return; } + hideError(); navigator.goTo("/fxml/ResourceList.fxml"); } catch (ServiceException e) { - Alert alert = new Alert(Alert.AlertType.ERROR); - alert.setTitle("Ошибка входа"); - alert.setHeaderText(null); - alert.setContentText(e.getMessage()); - alert.showAndWait(); + showError(e.getMessage()); } } - @SuppressWarnings("unchecked") - private static Validator[] toArray(List> validators) { - return validators.toArray(new Validator[0]); + private void showError(String message) { + errorLabel.setText(message); + errorLabel.setVisible(true); + errorLabel.setManaged(true); + } + + private void hideError() { + errorLabel.setText(""); + errorLabel.setVisible(false); + errorLabel.setManaged(false); } } diff --git a/src/main/resources/fxml/LoginForm.fxml b/src/main/resources/fxml/LoginForm.fxml index 2b86c50..52d75ac 100644 --- a/src/main/resources/fxml/LoginForm.fxml +++ b/src/main/resources/fxml/LoginForm.fxml @@ -1,21 +1,39 @@ - + + + + - - - - - - + + + + + + + + + + + + + +