Skip to content

Commit 00dbeb2

Browse files
authored
Редизайн страницы входа на Bootstrap + тесты (#14)
## Что сделано - Редизайн `LoginForm.fxml` на BootstrapFX: панель `panel panel-primary`, кнопка `btn btn-primary btn-block`, ошибка — `alert alert-danger`. - `LoginFormController` переписан под паттерн из `docs/examples/how-to-interact-with-api-from-ui-via-middleware.md`: обычные `TextField`/`PasswordField` + `errorLabel`, клиентская валидация (пустые поля, формат email), вызов `AuthService.login()`, ошибка сервера — в `errorLabel`, успех — переход на `ResourceList`. - `Program`: BootstrapFX-стили подключены глобально, стартовой страницей стала форма входа. - В `pom.xml` добавлен `mockito-core 5.23.0` (scope test). ## Тесты - Unit (`ui.LoginFormControllerTest`, TestFX + мок `AuthService`): 5 тестов — пустые поля, пустой пароль, неверный email (API не вызывается, навигации нет), успешный вход (вызов `login` + переход на `ResourceList`, ошибка скрыта), ошибка сервера (текст в `errorLabel`, навигации нет). - Integration (`ui.LoginIntegrationTest`, без моков — реальный `AuthServiceImpl` + `ApiClient`): неверные креды против живого API отклоняются с `ServiceException`. Тест пропускается (assume), если бэкенд на `localhost:8080` недоступен. ## Проверки - `mvn verify`: 33 теста, 0 failures (1 skipped — нет бэкенда), spotless и checkstyle чистые.
1 parent 7b6bdf7 commit 00dbeb2

9 files changed

Lines changed: 427 additions & 66 deletions

File tree

‎README.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@
44
git pull --rebase
55
```
66

7+
## Запуск
8+
9+
Требования: JDK 25, Maven, запущенный бэкенд (иначе вход покажет ошибку соединения).
10+
11+
```bash
12+
# 1. Инфраструктура бэкенда (из корня этого репозитория):
13+
cp .env.example .env
14+
docker compose up -d postgres redis
15+
16+
# 2. Приложение бэкенда (из каталога соседнего репозитория, http://localhost:8080):
17+
./mvnw spring-boot:run -Dmaven.test.skip=true
18+
19+
# 3. Приложение (из корня этого репозитория):
20+
mvn javafx:run
21+
```
22+
23+
Откроется окно 800×600 со страницы входа (`LoginForm.fxml` — она же `INITIAL_PAGE_PATH`
24+
в `Program.java`). Для проверки вёрстки отдельной страницы можно временно подменить
25+
`INITIAL_PAGE_PATH` на нужный FXML.
26+
727
## RBAC
828

929
В системе предусмотрено 3 роли: пользователь, модератор и администратор:
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# how-to-interact-with-api-from-ui-via-middleware
2+
3+
```java
4+
package com.DesktopApplicationClientJava.controllers;
5+
6+
import com.DesktopApplicationClientJava.controllers.utils.Controller;
7+
import com.DesktopApplicationClientJava.entities.User;
8+
import com.DesktopApplicationClientJava.services.ServiceException;
9+
import com.DesktopApplicationClientJava.validation.EmailValidator;
10+
import com.DesktopApplicationClientJava.validation.PasswordValidator;
11+
import com.DesktopApplicationClientJava.validation.utils.ErrorMessages;
12+
import com.dlsc.formsfx.model.structure.Field;
13+
import com.dlsc.formsfx.model.structure.Form;
14+
import com.dlsc.formsfx.model.structure.PasswordField;
15+
import com.dlsc.formsfx.model.structure.Section;
16+
import com.dlsc.formsfx.model.structure.StringField;
17+
import com.dlsc.formsfx.model.validators.Validator;
18+
import com.dlsc.formsfx.view.renderer.FormRenderer;
19+
import java.util.List;
20+
21+
import javafx.event.ActionEvent;
22+
import javafx.fxml.FXML;
23+
import javafx.scene.control.Alert;
24+
import javafx.scene.control.Button;
25+
import javafx.scene.layout.VBox;
26+
27+
public class LoginFormController extends Controller {
28+
29+
@FXML private VBox formContainer;
30+
@FXML private Button submitButton;
31+
32+
private StringField email;
33+
private PasswordField password;
34+
private Form loginForm;
35+
36+
@FXML
37+
private void initialize() {
38+
email =
39+
Field.ofStringType("")
40+
.label(EmailValidator.FIELD_NAME)
41+
.placeholder("youremail@example.com")
42+
.required(ErrorMessages.Required)
43+
.validate(toArray(EmailValidator.validators()));
44+
password =
45+
Field.ofPasswordType("")
46+
.label(PasswordValidator.FIELD_NAME)
47+
.required(ErrorMessages.Required)
48+
.validate(toArray(PasswordValidator.validators()));
49+
50+
loginForm = Form.of(Section.of(email, password));
51+
formContainer.getChildren().add(new FormRenderer(loginForm));
52+
}
53+
54+
@FXML
55+
private void onSubmit(ActionEvent event) {
56+
// клиентская валидация через FormsFX до вызова сервера
57+
boolean valid = email.validate() & password.validate();
58+
if (!valid) {
59+
return;
60+
}
61+
62+
try {
63+
// ← ВОТ ЗДЕСЬ ВЫЗОВ СЕРВИСА
64+
User user = services.getAuthService().login(email.getValue(), password.getValue());
65+
if (user == null) {
66+
return;
67+
}
68+
69+
// если дошли сюда — логин успешен, session уже обновлён сервисом
70+
navigator.goTo("/fxml/ResourceList.fxml");
71+
72+
} catch (ServiceException e) {
73+
Alert alert = new Alert(Alert.AlertType.ERROR);
74+
alert.setTitle("Ошибка входа");
75+
alert.setHeaderText(null);
76+
alert.setContentText(e.getMessage());
77+
alert.showAndWait();
78+
}
79+
}
80+
81+
@SuppressWarnings("unchecked")
82+
private static Validator<String>[] toArray(List<Validator<String>> validators) {
83+
return validators.toArray(new Validator[0]);
84+
}
85+
}
86+
```

‎docs/страницы.md‎

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,80 @@
77

88
Маршрут — путь к FXML для `Navigator.goTo(...)`.
99

10-
| № | Название страницы | Маршрут (FXML) | Доступ | Назначение |
11-
| --- | -------------------------------------------- | ---------------------------------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------- |
12-
| 1 | Вход (email + пароль) | `/fxml/LoginForm.fxml` | гость (публичная) | Аутентификация через `AuthService`, после успеха → №2 |
13-
| 2 | Список ресурсов и сортировка (главная) | `/fxml/ResourceList.fxml` (переименовать из `Search.fxml`) | `USER` | Поиск/фильтр/сортировка, пагинация `offset/count` через `ResourceService.search` |
14-
| 3 | Просмотр метаданных документа | `/fxml/ResourceDetails.fxml` (новая) | `USER` | Только чтение: `ResourceService.getById` → `title, description, createdAt, updatedAt, uuid` |
15-
| 4 | Форма ресурса (создание/редактирование) | `/fxml/ResourceForm.fxml` (новая) | `MODER` | `create/update` через `ResourceService`; удаление — действием из №2/№3 с подтверждением, без отдельной страницы |
16-
| 5 | Список пользователей | `/fxml/UserList.fxml` (новая) | `ADMIN` | Поиск/фильтр через `UsersService.search` + `UserFilter`; удаление действием из списка |
17-
| 6 | Форма пользователя (создание/редактирование) | `/fxml/UserForm.fxml` (новая) | `ADMIN` | `create` через `RegisterData`, `update` через `UpdateUserData`, `getById` для предзаполнения |
10+
## Паттерн взаимодействия с API
11+
12+
UI не ходит в HTTP напрямую. Контроллер вызывает сервис («middleware»):
13+
`Controller → services.getXService() → ApiClient → HTTP + Session (Bearer)`.
14+
Сервис кладёт пользователя/токены в `Session`, ошибки пробрасывает как
15+
`ServiceException`, контроллер показывает их через `Alert`.
16+
Пример: docs/examples/how-to-interact-with-api-from-ui-via-middleware.md.
17+
18+
| № | Название страницы | Маршрут (FXML) | Доступ | Назначение |
19+
| --- | -------------------------------------------- | ----------------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------- |
20+
| 1 | Вход (email + пароль) | `/fxml/LoginForm.fxml` | гость (публичная) | Аутентификация через `AuthService`, после успеха → №2 |
21+
| 2 | Список ресурсов и сортировка (главная) | `/fxml/ResourceList.fxml` | `USER` | Поиск/фильтр/сортировка, пагинация `offset/count` через `ResourceService.search` |
22+
| 3 | Просмотр метаданных документа | `/fxml/ResourceDetails.fxml` (новая) | `USER` | Только чтение: `ResourceService.getById` → `title, description, createdAt, updatedAt, uuid` |
23+
| 4 | Форма ресурса (создание/редактирование) | `/fxml/ResourceForm.fxml` (новая) | `MODER` | `create/update` через `ResourceService`; удаление — действием из №2/№3 с подтверждением, без отдельной страницы |
24+
| 5 | Список пользователей | `/fxml/UserList.fxml` (новая) | `ADMIN` | Поиск/фильтр через `UsersService.search` + `UserFilter`; удаление действием из списка |
25+
| 6 | Форма пользователя (создание/редактирование) | `/fxml/UserForm.fxml` (новая) | `ADMIN` | `create` через `RegisterData`, `update` через `UpdateUserData`, `getById` для предзаполнения |
26+
27+
## Интеграции страниц с эндпоинтами (через сервисы)
28+
29+
Базовый URL: `api.baseUrl` (`http://localhost:8080`).
30+
31+
| № | Действие UI | Сервис (middleware) | ApiClient → HTTP |
32+
|---|-------------|---------------------|------------------|
33+
| 1 | Войти | `AuthService.login(email, password)` | `POST /api/v1/auth/login` (`LoginRequest`), затем `GET /api/v1/user/me`; токены → `Session` |
34+
| 1 | Выйти | `AuthService.logout()` | `POST /api/v1/auth/logout` (`RefreshTokenRequest`), затем `Session.clear()` |
35+
| 2 | Поиск/фильтр/сортировка ресурсов | `ResourceService.search(filter, offset, count)` | `GET /api/v1/resource?title=&description=&offset=&count=&sortBy=createdAt&sortDir=desc` |
36+
| 3 | Открыть метаданные | `ResourceService.getById(id)` | `GET /api/v1/resource/{id}` |
37+
| 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}` |
38+
| 3 | Экспорт файлов ресурса | `ExportService.exportResourceFiles(id, dir)` | `GET /api/v1/admin/resource/{id}/files/export` (бинарный ответ → файл) |
39+
| 4 | Создать ресурс | `ResourceService.create(title, description)` | `POST /api/v1/resource` (`CreateResourceRequest`) |
40+
| 4 | Обновить ресурс | `ResourceService.update(id, title, description)` | `PUT /api/v1/resource/{id}` (`UpdateResourceRequest`) |
41+
| 4 | Удалить ресурс (из №2/№3) | `ResourceService.delete(id)` | `DELETE /api/v1/resource/{id}` |
42+
| 5 | Поиск/фильтр пользователей | `UsersService.search(filter, offset, count)` | `GET /api/v1/admin?email=&firstName=&lastName=&role=&enabled=&offset=&count=&sortBy=createdAt&sortDir=desc` |
43+
| 5 | Удалить пользователя (из списка) | `UsersService.delete(id)` | `DELETE /api/v1/admin/{id}` |
44+
| 5 | Экспорт пользователей/ресурсов | `ExportService.{exportUsers, exportResources}` | `GET /api/v1/admin/users/export`, `GET /api/v1/admin/resource/export` |
45+
| 6 | Создать пользователя | `UsersService.create(RegisterData)` | `POST /api/v1/admin` (`RegisterRequest`: email, password, role, firstName, lastName) |
46+
| 6 | Открыть пользователя для редактирования | `UsersService.getById(id)` | `GET /api/v1/admin/{id}` |
47+
| 6 | Обновить пользователя | `UsersService.update(id, UpdateUserData)` | `PUT /api/v1/admin/{id}` (`UpdateUserRequest`: email, enabled, role, firstName, lastName) |
48+
49+
Примечания:
50+
* `ApiClient` при 401 (кроме `/api/v1/auth/*`) один раз обновляет токены через
51+
`POST /api/v1/auth/refresh` и повторяет запрос; если refresh нет/не удался —
52+
исходный ответ уходит в `ServiceException`.
53+
* Клиентская валидация (FormsFX + `validation/*Validator`) срабатывает до вызова
54+
сервиса; серверные ошибки (`ApiException` → `ServiceException`) показывает контроллер.
1855

1956
## Примечания по реализации
2057

21-
* №1: упростить `LoginFormController` — удалить `firstName/lastName/thirdName`, оставить `email + password` + вызов `AuthService`/`LoginRequest` (сейчас там заглушка `authenticate()` с `Role.USER`).
22-
* №2: переименование `Search.fxml → ResourceList.fxml` + переименование `SearchController → ResourceListController`; обновить `Program.INITIAL_PAGE_PATH` и `navigator.goTo("/fxml/Search.fxml")` в `LoginFormController`.
58+
* №1: ✅ готово — `LoginFormController` только email + пароль, вызов `AuthService`.
59+
* №2: ✅ переименование `Search.fxml → ResourceList.fxml` готово; наполнить список:
60+
`ResourceService.search` + пагинация, фильтры `ResourceFilter(title, description)`.
2361
* SoC: списки (№2, №5) не содержат логики форм; формы (№4, №6) работают в двух режимах `create/edit` по наличию `uuid`, валидация через существующие `validation/*Validator`.
2462
* Кнопки create/edit/delete рендерятся по роли из `Session.getCurrentUser().role`, а не отдельными страницами.
63+
64+
## Интеграционные тесты (rough)
65+
66+
Стек: JUnit5 + AssertJ уже есть; TestFX headless настроен в `maven-surefire-plugin`
67+
(`--enable-native-access=ALL-UNNAMED`). HTTP backend-а в тестах нет — ставим
68+
стаб: WireMock (или подменный `ApiClient`) + TestFX для UI-слоя.
69+
70+
* IT-Auth (№1): мок `POST /api/v1/auth/login` → 200 с токенами + `GET /api/v1/user/me` → 200 `UserDto`;
71+
проверить: `Session` заполнен, переход на `ResourceList`; кейс 401 → `ServiceException`, показан `Alert`, перехода нет.
72+
* IT-ResourceList (№2): мок `GET /api/v1/resource?...` → список `ResourceDto`;
73+
проверить: таблица/список отрисован, пагинация меняет `offset/count` в запросе;
74+
кейс 403 для `USER` без прав — сообщение об ошибке, без падения.
75+
* IT-ResourceDetails (№3): мок `GET /api/v1/resource/{id}` → 200 и 404;
76+
проверить: поля метаданных показаны; 404 → `Alert` «не найден».
77+
* IT-ResourceForm (№4): мок `POST /api/v1/resource` → 201 и `PUT /api/v1/resource/{id}` → 200;
78+
проверить: create/edit режимы по наличию `uuid`; `DELETE /api/v1/resource/{id}` после подтверждения.
79+
* IT-UserList/UserForm (№5–6, ADMIN): мок `GET /api/v1/admin?...` → список,
80+
`POST /api/v1/admin` → 201, `PUT /api/v1/admin/{id}` → 200, `DELETE` → 204;
81+
проверить: фильтр `email/role/enabled` уходит в query params; создание требует все поля `RegisterData`.
82+
* IT-Files/Export (№3/№5): мок multipart `POST .../file` → 201 `FileDto`,
83+
`GET .../file/{fileId}` → бинарник, `GET .../export` → бинарник;
84+
проверить: файл сохранён в `destinationDir`, имя взято из `Content-Disposition`.
85+
* IT-Refresh: мок защищённого `GET` → 401, затем `POST /api/v1/auth/refresh` → 200,
86+
повторный `GET` → 200; проверить: запрос повторён с новым Bearer без участия UI.

‎pom.xml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@
105105
<artifactId>bootstrapfx-core</artifactId>
106106
<version>0.4.0</version>
107107
</dependency>
108+
<dependency>
109+
<groupId>org.mockito</groupId>
110+
<artifactId>mockito-core</artifactId>
111+
<version>5.23.0</version>
112+
<scope>test</scope>
113+
</dependency>
108114
</dependencies>
109115

110116
<repositories></repositories>

‎src/main/java/com/DesktopApplicationClientJava/Program.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
import javafx.scene.Parent;
1212
import javafx.scene.Scene;
1313
import javafx.stage.Stage;
14+
import org.kordamp.bootstrapfx.BootstrapFX;
1415

1516
public class Program extends Application {
16-
private static final String INITIAL_PAGE_PATH = "/fxml/ResourceList.fxml";
17+
private static final String INITIAL_PAGE_PATH = "/fxml/LoginForm.fxml";
1718
private static final double WINDOW_WIDTH = 800.0;
1819
private static final double WINDOW_HEIGHT = 600.0;
1920

@@ -43,6 +44,7 @@ public void start(Stage stage) throws IOException {
4344

4445
Parent root = loader.load();
4546
Scene scene = new Scene(root);
47+
scene.getStylesheets().add(BootstrapFX.bootstrapFXStylesheet());
4648
loadStyleSheet(scene, "/styles.css");
4749

4850
stage.setScene(scene);

0 commit comments

Comments
 (0)