From 532418616d1446ee0a903f3a8f975d398f9a97f7 Mon Sep 17 00:00:00 2001 From: Vladimir Valekzhanin Date: Wed, 14 Oct 2026 21:44:28 +0300 Subject: [PATCH 1/3] feat(compose): add backend to compose --- .env.example | 11 ++++- .vscode/extensions.json | 10 ++++- compose.yaml | 96 ++++++++++++++++++++++++++--------------- 3 files changed, 79 insertions(+), 38 deletions(-) diff --git a/.env.example b/.env.example index 53bf5f9..94d8dbd 100644 --- a/.env.example +++ b/.env.example @@ -3,15 +3,22 @@ # cp .env.example .env # --- Postgres --- +POSTGRES_HOST=db +POSTGRES_PORT=5432 POSTGRES_DB=pks_dev POSTGRES_USER=pks_user POSTGRES_PASSWORD=changeme -# --- Backend app settings (used by the backend, not by compose) --- +# --- Backend app settings --- +JWT_SECRET=replace-me-with-64-hex-chars # openssl rand -hex 32 +JWT_ACCESS_EXPIRATION_IN_MS=900000 # 15 minutes +JWT_REFRESH_EXPIRATION_IN_MS=604800000 # 7 days APP_PORT=8080 REDIS_PASSWORD=changeme -# --- MinIO (only needed with: docker compose --profile files up -d) --- +# --- MinIO --- +MINIO_HOST=minio +MINIO_PORT=9000 MINIO_ROOT_USER=minioadmin MINIO_ROOT_PASSWORD=changeme-minio-root MINIO_BUCKET_NAME=local-bucket diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 301782b..f2f5133 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,11 @@ { - "recommendations": ["vscjava.vscode-lombok"] + "recommendations": [ + "vscjava.vscode-lombok", + "github.vscode-github-actions", + "dhruv.maven-dependency-explorer", + "vscjava.vscode-maven", + "vscjava.vscode-java-dependency", + "vscjava.vscode-java-test", + "shengchen.vscode-checkstyle" + ] } diff --git a/compose.yaml b/compose.yaml index 744b1cd..f0aed11 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,3 +1,5 @@ +--- + # Test infrastructure for DesktopApplicationClientJava integration tests. # # Mirrors the contract of the backend (pks-java) compose file: same services, @@ -6,10 +8,6 @@ # # Default: postgres + redis (enough for auth/resource/user/admin ITs): # docker compose up -d -# With object storage (for file/export ITs): -# docker compose --profile files up -d -# Desktop client (GUI: нужен X-сервер, подробности в README; к бэкенду ходит по HTTP, поэтому без depends_on): -# docker compose --profile gui run --rm client services: client: @@ -23,58 +21,85 @@ services: volumes: - /tmp/.X11-unix:/tmp/.X11-unix:ro + backend: + image: detfiniktn/java-pks:latest + container_name: backend + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + minio: + condition: service_healthy + healthcheck: + interval: 10s + retries: 5 + test: ["CMD-SHELL", "curl -f http://localhost:8080/java-pks/health/live || exit 1"] + timeout: 5s + ports: + - "8080:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/pks_dev + SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER} + SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD} + SPRING_DATA_REDIS_HOST: redis + SPRING_DATA_REDIS_PORT: "6379" + JWT_SECRET: ${JWT_SECRET} + volumes: + - ./.env:/workspace/.env:ro + restart: on-failure:1 + postgres: image: postgres:16-alpine + container_name: db environment: - POSTGRES_DB=${POSTGRES_DB} - - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - ports: - - '5432:5432' - volumes: - - postgres_data:/var/lib/postgresql/data - restart: unless-stopped + - POSTGRES_USER=${POSTGRES_USER} healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s - timeout: 5s retries: 5 + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + timeout: 5s + ports: + - "5432:5432" + restart: on-failure + volumes: + - postgres_data:/var/lib/postgresql/data redis: image: redis:7-alpine - ports: - - '6379:6379' - volumes: - - redis_basic_data:/data - restart: unless-stopped healthcheck: - test: ["CMD", "redis-cli", "ping"] interval: 10s - timeout: 5s retries: 5 + test: ["CMD", "redis-cli", "ping"] + timeout: 5s + ports: + - "6379:6379" + restart: on-failure + volumes: + - redis_basic_data:/data minio: - image: quay.io/minio/minio - profiles: [files] + image: pgsty/minio:latest command: server /data --console-address ":9001" environment: - - MINIO_ROOT_USER=${MINIO_ROOT_USER} - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} - ports: - - '9000:9000' # S3 API - - '9001:9001' # Web Console - volumes: - - minio_data:/data - restart: unless-stopped + - MINIO_ROOT_USER=${MINIO_ROOT_USER} healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:9000/minio/health/live || exit 1"] interval: 10s - timeout: 5s retries: 5 + test: ["CMD-SHELL", "curl -f http://localhost:9000/minio/health/live || exit 1"] + timeout: 5s + ports: + - "9000:9000" # S3 API + - "9001:9001" # Web Console + restart: on-failure + volumes: + - minio_data:/data minio-init: - image: minio/mc:latest - profiles: [files] + image: pgsty/mc:latest depends_on: minio: condition: service_healthy @@ -86,10 +111,11 @@ services: mc admin policy attach myminio readwrite --user ${MINIO_ACCESS_TOKEN}; exit 0; " + restart: on-failure volumes: - redis_basic_data: - postgres_data: - driver: local minio_data: driver: local + postgres_data: + driver: local + redis_basic_data: From 61b610847ae97ae807322aa1b98f67ccdb6a2266 Mon Sep 17 00:00:00 2001 From: Vladimir Valekzhanin Date: Wed, 14 Oct 2026 21:59:58 +0300 Subject: [PATCH 2/3] fix(perf): refactor some tests that don't require testfx --- lefthook.yml | 29 +-- pom.xml | 19 +- .../controllers/LoginFormController.java | 10 +- .../ResourceDetailsController.java | 33 +-- .../controllers/ResourceFormController.java | 18 +- .../controllers}/LoginFormControllerTest.java | 108 ++++---- .../ResourceDetailsControllerTest.java | 133 +++++----- .../ResourceFormControllerTest.java | 192 +++++++++++++++ .../services/AuthServiceTest.java | 3 +- .../services/mock/MockServicesTest.java | 7 +- .../ui/FxTestBase.java | 2 +- .../ui/MockFlowTest.java | 3 +- .../ui/ResourceListControllerTest.java | 4 +- .../ui/integration/IntegrationTestCase.java | 2 +- .../ui/integration/LoginIntegrationTest.java | 2 +- .../ResourceDetailsIntegrationTest.java | 2 +- .../ResourceFormIntegrationTest.java | 6 +- .../ResourceListIntegrationTest.java | 5 +- .../validation/EmailValidatorTest.java | 3 +- .../validation/FirstNameValidatorTest.java | 3 +- .../validation/LastNameValidatorTest.java | 3 +- .../validation/PasswordValidatorTest.java | 3 +- .../validation/ThirdNameValidatorTest.java | 3 +- .../java/ui/ResourceFormControllerTest.java | 231 ------------------ 24 files changed, 376 insertions(+), 448 deletions(-) rename src/test/java/{ui => com/DesktopApplicationClientJava/controllers}/LoginFormControllerTest.java (53%) rename src/test/java/{ui => com/DesktopApplicationClientJava/controllers}/ResourceDetailsControllerTest.java (60%) create mode 100644 src/test/java/com/DesktopApplicationClientJava/controllers/ResourceFormControllerTest.java rename src/test/java/{ => com/DesktopApplicationClientJava}/services/AuthServiceTest.java (96%) rename src/test/java/{ => com/DesktopApplicationClientJava}/services/mock/MockServicesTest.java (91%) rename src/test/java/{ => com/DesktopApplicationClientJava}/ui/FxTestBase.java (97%) rename src/test/java/{ => com/DesktopApplicationClientJava}/ui/MockFlowTest.java (98%) rename src/test/java/{ => com/DesktopApplicationClientJava}/ui/ResourceListControllerTest.java (99%) rename src/test/java/{ => com/DesktopApplicationClientJava}/ui/integration/IntegrationTestCase.java (97%) rename src/test/java/{ => com/DesktopApplicationClientJava}/ui/integration/LoginIntegrationTest.java (93%) rename src/test/java/{ => com/DesktopApplicationClientJava}/ui/integration/ResourceDetailsIntegrationTest.java (96%) rename src/test/java/{ => com/DesktopApplicationClientJava}/ui/integration/ResourceFormIntegrationTest.java (98%) rename src/test/java/{ => com/DesktopApplicationClientJava}/ui/integration/ResourceListIntegrationTest.java (98%) rename src/test/java/{ => com/DesktopApplicationClientJava}/validation/EmailValidatorTest.java (93%) rename src/test/java/{ => com/DesktopApplicationClientJava}/validation/FirstNameValidatorTest.java (89%) rename src/test/java/{ => com/DesktopApplicationClientJava}/validation/LastNameValidatorTest.java (89%) rename src/test/java/{ => com/DesktopApplicationClientJava}/validation/PasswordValidatorTest.java (89%) rename src/test/java/{ => com/DesktopApplicationClientJava}/validation/ThirdNameValidatorTest.java (90%) delete mode 100644 src/test/java/ui/ResourceFormControllerTest.java diff --git a/lefthook.yml b/lefthook.yml index d9871e9..b42a2d4 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,24 +1,9 @@ -# Lefthook config for this Java/Maven project (replaces pre-commit). -# -# All jobs are CHECK-ONLY (no auto-fix): -# - lint via `mvn checkstyle:check` -# - format via `mvn spotless:check` (use `mvn spotless:apply` manually to fix) -# - unit tests via `mvn test` (integration tests tagged `integration` are -# already excluded from the default build via surefire `excludedGroups` -# in pom.xml, so no live backend is needed) -# -# Install/activate (per clone): -# brew install lefthook # or: winget install evilmartians.lefthook / scoop install lefthook -# lefthook install -# -# Run manually: -# lefthook run pre-commit pre-commit: - parallel: false + parallel: true jobs: - - name: "lint: mvn checkstyle:check" - run: mvn -B checkstyle:check - - name: "format: mvn spotless:check" - run: mvn -B spotless:check - - name: "test: mvn test (unit only, integration excluded)" - run: mvn -B test + - name: "lint" + run: mvn -B -q checkstyle:check + - name: "format" + run: mvn -B -q spotless:check + - name: "unit tests" + run: mvn -B -q clean test diff --git a/pom.xml b/pom.xml index d2c0902..9d25352 100644 --- a/pom.xml +++ b/pom.xml @@ -134,6 +134,18 @@ + + org.apache.maven.plugins + maven-dependency-plugin + 3.11.0 + + + + properties + + + + org.apache.maven.plugins maven-surefire-plugin @@ -148,14 +160,17 @@ -Dprism.text=t2k -Djava.awt.headless=true --enable-native-access=ALL-UNNAMED + -XX:+EnableDynamicAgentLoading + -Djdk.instrument.traceUsage=false + -javaagent:${org.mockito:mockito-core:jar} + -Djava.util.logging.config.file=${project.basedir}/src/test/resources/logging.properties + -Xshare:off ${excludedGroups} - ${project.basedir}/.env diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java index 5af6832..affdd7c 100644 --- a/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java @@ -13,13 +13,13 @@ public class LoginFormController extends Controller { - @FXML private TextField emailField; - @FXML private PasswordField passwordField; - @FXML private Label errorLabel; - @FXML private Button submitButton; + @FXML TextField emailField; + @FXML PasswordField passwordField; + @FXML Label errorLabel; + @FXML Button submitButton; @FXML - private void onSubmit(ActionEvent event) { + void onSubmit(ActionEvent event) { String email = emailField.getText() == null ? "" : emailField.getText().trim(); String password = passwordField.getText() == null ? "" : passwordField.getText(); diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsController.java index dfd28ab..ff24f32 100644 --- a/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsController.java +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsController.java @@ -30,20 +30,20 @@ public class ResourceDetailsController extends Controller { static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); - @FXML private Label titleLabel; - @FXML private Label descriptionLabel; - @FXML private Label createdLabel; - @FXML private Label updatedLabel; - @FXML private Label uuidLabel; - @FXML private Label errorLabel; - @FXML private Label statusLabel; - @FXML private TableView filesTable; - @FXML private TableColumn nameColumn; - @FXML private TableColumn sizeColumn; - @FXML private TableColumn contentTypeColumn; - @FXML private TableColumn downloadColumn; - @FXML private Button backButton; - @FXML private Button editButton; + @FXML Label titleLabel; + @FXML Label descriptionLabel; + @FXML Label createdLabel; + @FXML Label updatedLabel; + @FXML Label uuidLabel; + @FXML Label errorLabel; + @FXML Label statusLabel; + @FXML TableView filesTable; + @FXML TableColumn nameColumn; + @FXML TableColumn sizeColumn; + @FXML TableColumn contentTypeColumn; + @FXML TableColumn downloadColumn; + @FXML Button backButton; + @FXML Button editButton; private UUID resourceId; @@ -101,11 +101,12 @@ public void load() { } @FXML - private void onBack(ActionEvent event) { + void onBack(ActionEvent event) { navigator.goTo("/fxml/ResourceList.fxml"); } - private void onDownload(FileInfo file) { + @FXML + void onDownload(FileInfo file) { File dir = chooseDirectory(backButton.getScene().getWindow()); if (dir == null) { return; diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceFormController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceFormController.java index 5320129..ca1866a 100644 --- a/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceFormController.java +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceFormController.java @@ -16,12 +16,12 @@ public class ResourceFormController extends Controller { private static final int TITLE_MIN = 3; private static final int TITLE_MAX = 255; - @FXML private Label formTitleLabel; - @FXML private TextField titleField; - @FXML private TextArea descriptionArea; - @FXML private Label errorLabel; - @FXML private Button cancelButton; - @FXML private Button saveButton; + @FXML Label formTitleLabel; + @FXML TextField titleField; + @FXML TextArea descriptionArea; + @FXML Label errorLabel; + @FXML Button cancelButton; + @FXML Button saveButton; private UUID resourceId; @@ -39,7 +39,7 @@ public void setResourceId(UUID resourceId) { } @FXML - private void initialize() { + void initialize() { hideError(); if (resourceId == null) { formTitleLabel.setText("Новый документ"); @@ -60,7 +60,7 @@ private void loadExisting() { } @FXML - private void onSave(ActionEvent event) { + void onSave(ActionEvent event) { hideError(); String title = titleField.getText() == null ? "" : titleField.getText().trim(); String description = descriptionArea.getText() == null ? "" : descriptionArea.getText().trim(); @@ -95,7 +95,7 @@ private void onSave(ActionEvent event) { } @FXML - private void onCancel(ActionEvent event) { + void onCancel(ActionEvent event) { navigator.goTo("/fxml/ResourceList.fxml"); } diff --git a/src/test/java/ui/LoginFormControllerTest.java b/src/test/java/com/DesktopApplicationClientJava/controllers/LoginFormControllerTest.java similarity index 53% rename from src/test/java/ui/LoginFormControllerTest.java rename to src/test/java/com/DesktopApplicationClientJava/controllers/LoginFormControllerTest.java index 2aed3df..ede4b1f 100644 --- a/src/test/java/ui/LoginFormControllerTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/controllers/LoginFormControllerTest.java @@ -1,4 +1,4 @@ -package ui; +package com.DesktopApplicationClientJava.controllers; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -18,89 +18,79 @@ import com.DesktopApplicationClientJava.session.Session; import com.DesktopApplicationClientJava.utils.ControllerWiring; import java.util.UUID; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; -import javafx.scene.Scene; +import javafx.application.Platform; +import javafx.event.ActionEvent; import javafx.scene.control.Label; -import javafx.stage.Stage; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.testfx.api.FxToolkit; -class LoginFormControllerTest extends FxTestBase { +class LoginFormControllerTest { private static final String VALID_EMAIL = "user@example.com"; private static final String VALID_PASSWORD = "secret123"; private AuthService authService; private Navigator navigator; - - @BeforeEach - void resetScene() throws Exception { - FxToolkit.setupStage( - stage -> { - try { - start(stage); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + private LoginFormController controller; + + @BeforeAll + static void bootstrapFx() { + try { + Platform.startup(() -> {}); + Platform.setImplicitExit(false); + } catch (IllegalStateException alreadyStarted) { + // JVM-wide singleton; another test class already started the toolkit. + } } - void start(Stage stage) throws Exception { + @BeforeEach + void setUp() { authService = mock(AuthService.class); + navigator = mock(Navigator.class); + ServiceRegistry services = mock(ServiceRegistry.class); when(services.getAuthService()).thenReturn(authService); - navigator = mock(Navigator.class); - FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/LoginForm.fxml")); - loader.setControllerFactory( - clazz -> { - try { - Object controller = clazz.getDeclaredConstructor().newInstance(); - ControllerWiring.wire(controller, navigator, new Session(), services); - return controller; - } catch (Exception e) { - throw new RuntimeException("Cannot create controller: " + clazz, e); - } - }); - Parent root = loader.load(); - stage.setScene(new Scene(root)); - stage.show(); + controller = new LoginFormController(); + ControllerWiring.wire(controller, navigator, new Session(), services); + + controller.emailField = new TextField(); + controller.passwordField = new PasswordField(); + controller.errorLabel = new Label(); } @Test void emptyFieldsShowErrorAndDoNotCallApi() { - robot.clickOn("#submitButton"); + controller.onSubmit(new ActionEvent()); - Label error = robot.lookup("#errorLabel").queryAs(Label.class); - assertTrue(error.isVisible()); - assertEquals("Заполните все поля", error.getText()); + assertTrue(controller.errorLabel.isVisible()); + assertEquals("Заполните все поля", controller.errorLabel.getText()); verify(authService, never()).login(anyString(), anyString()); verify(navigator, never()).goTo(anyString()); } @Test void blankPasswordShowsErrorAndDoesNotCallApi() { - fillText(robot, "#emailField", VALID_EMAIL); - robot.clickOn("#submitButton"); + controller.emailField.setText(VALID_EMAIL); + controller.onSubmit(new ActionEvent()); - Label error = robot.lookup("#errorLabel").queryAs(Label.class); - assertTrue(error.isVisible()); - assertEquals("Заполните все поля", error.getText()); + assertTrue(controller.errorLabel.isVisible()); + assertEquals("Заполните все поля", controller.errorLabel.getText()); verify(authService, never()).login(anyString(), anyString()); verify(navigator, never()).goTo(anyString()); } @Test void invalidEmailShowsErrorAndDoesNotCallApi() { - fillText(robot, "#emailField", "not-an-email"); - fillText(robot, "#passwordField", VALID_PASSWORD); - robot.clickOn("#submitButton"); + controller.emailField.setText("not-an-email"); + controller.passwordField.setText(VALID_PASSWORD); + controller.onSubmit(new ActionEvent()); - Label error = robot.lookup("#errorLabel").queryAs(Label.class); - assertTrue(error.isVisible()); - assertEquals("Неверный формат", error.getText()); + assertTrue(controller.errorLabel.isVisible()); + assertEquals("Неверный формат", controller.errorLabel.getText()); verify(authService, never()).login(anyString(), anyString()); verify(navigator, never()).goTo(anyString()); } @@ -110,14 +100,13 @@ void successfulLoginNavigatesToResourceList() { User user = new User(UUID.randomUUID(), VALID_EMAIL, Role.USER, "Имя", "Фамилия", true); when(authService.login(VALID_EMAIL, VALID_PASSWORD)).thenReturn(user); - fillText(robot, "#emailField", VALID_EMAIL); - fillText(robot, "#passwordField", VALID_PASSWORD); - robot.clickOn("#submitButton"); + controller.emailField.setText(VALID_EMAIL); + controller.passwordField.setText(VALID_PASSWORD); + controller.onSubmit(new ActionEvent()); verify(authService).login(VALID_EMAIL, VALID_PASSWORD); verify(navigator).goTo("/fxml/ResourceList.fxml"); - Label error = robot.lookup("#errorLabel").queryAs(Label.class); - assertFalse(error.isVisible()); + assertFalse(controller.errorLabel.isVisible()); } @Test @@ -125,13 +114,12 @@ void failedLoginShowsServerErrorAndDoesNotNavigate() { when(authService.login(VALID_EMAIL, VALID_PASSWORD)) .thenThrow(new ServiceException("Неверный логин или пароль")); - fillText(robot, "#emailField", VALID_EMAIL); - fillText(robot, "#passwordField", VALID_PASSWORD); - robot.clickOn("#submitButton"); + controller.emailField.setText(VALID_EMAIL); + controller.passwordField.setText(VALID_PASSWORD); + controller.onSubmit(new ActionEvent()); - Label error = robot.lookup("#errorLabel").queryAs(Label.class); - assertTrue(error.isVisible()); - assertEquals("Неверный логин или пароль", error.getText()); + assertTrue(controller.errorLabel.isVisible()); + assertEquals("Неверный логин или пароль", controller.errorLabel.getText()); verify(navigator, never()).goTo(anyString()); } } diff --git a/src/test/java/ui/ResourceDetailsControllerTest.java b/src/test/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsControllerTest.java similarity index 60% rename from src/test/java/ui/ResourceDetailsControllerTest.java rename to src/test/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsControllerTest.java index f25c083..e3322cd 100644 --- a/src/test/java/ui/ResourceDetailsControllerTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsControllerTest.java @@ -1,4 +1,4 @@ -package ui; +package com.DesktopApplicationClientJava.controllers; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -10,7 +10,6 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; -import com.DesktopApplicationClientJava.controllers.ResourceDetailsController; import com.DesktopApplicationClientJava.entities.FileInfo; import com.DesktopApplicationClientJava.entities.Resource; import com.DesktopApplicationClientJava.navigation.Navigator; @@ -25,20 +24,20 @@ import java.time.LocalDateTime; import java.util.List; import java.util.UUID; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; +import javafx.application.Platform; +import javafx.event.ActionEvent; import javafx.scene.Scene; +import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TableView; -import javafx.stage.Stage; +import javafx.scene.layout.StackPane; import javafx.stage.Window; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.testfx.api.FxRobot; -import org.testfx.api.FxToolkit; -class ResourceDetailsControllerTest extends FxTestBase { +class ResourceDetailsControllerTest { private static final UUID RESOURCE_ID = UUID.randomUUID(); private static final UUID FILE_ID_1 = UUID.randomUUID(); @@ -62,121 +61,122 @@ protected File chooseDirectory(Window owner) { } } - @BeforeEach - void resetScene() throws Exception { - FxToolkit.setupStage( - stage -> { - try { - start(stage); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + @BeforeAll + static void bootstrapFx() { + try { + Platform.startup(() -> {}); + Platform.setImplicitExit(false); + } catch (IllegalStateException alreadyStarted) { + // JVM-wide singleton; another test class already started the toolkit. + } } - void start(Stage stage) throws Exception { + @BeforeEach + void setUp() { resourceService = mock(ResourceService.class); fileService = mock(FileService.class); + navigator = mock(Navigator.class); + ServiceRegistry services = mock(ServiceRegistry.class); when(services.getResourceService()).thenReturn(resourceService); when(services.getFileService()).thenReturn(fileService); - navigator = mock(Navigator.class); controller = new TestableResourceDetailsController(); - FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/ResourceDetails.fxml")); - loader.setControllerFactory(clazz -> controller); ControllerWiring.wire(controller, navigator, new Session(), services); - Parent root = loader.load(); - stage.setScene(new Scene(root)); - stage.show(); + + controller.titleLabel = new Label(); + controller.descriptionLabel = new Label(); + controller.createdLabel = new Label(); + controller.updatedLabel = new Label(); + controller.uuidLabel = new Label(); + controller.errorLabel = new Label(); + controller.statusLabel = new Label(); + controller.filesTable = new TableView<>(); + + // onDownload does backButton.getScene().getWindow() before calling chooseDirectory. + // Attaching the button to a Scene keeps that expression from NPE-ing; the window itself + // is never dereferenced because our chooseDirectory override ignores its argument. + controller.backButton = new Button(); + new Scene(new StackPane(controller.backButton)); } @Test void successfulLoadShowsMetadataAndFiles() { when(resourceService.getById(RESOURCE_ID)).thenReturn(resourceWithFiles()); - loadPage(robot); - - assertEquals("Годовой отчёт", textOf(robot, "#titleLabel")); - assertEquals("Описание ресурса", textOf(robot, "#descriptionLabel")); - assertEquals("06.05.2024 07:08", textOf(robot, "#createdLabel")); - assertEquals("07.05.2024 09:10", textOf(robot, "#updatedLabel")); - assertEquals(RESOURCE_ID.toString(), textOf(robot, "#uuidLabel")); - - TableView table = filesTable(robot); - assertEquals(2, table.getItems().size()); - assertEquals("report.pdf", table.getItems().get(0).getName()); - assertEquals("photo.png", table.getItems().get(1).getName()); - assertFalse(robot.lookup("#errorLabel").queryAs(Label.class).isVisible()); + controller.setResourceId(RESOURCE_ID); + + assertEquals("Годовой отчёт", controller.titleLabel.getText()); + assertEquals("Описание ресурса", controller.descriptionLabel.getText()); + assertEquals("06.05.2024 07:08", controller.createdLabel.getText()); + assertEquals("07.05.2024 09:10", controller.updatedLabel.getText()); + assertEquals(RESOURCE_ID.toString(), controller.uuidLabel.getText()); + assertEquals(2, controller.filesTable.getItems().size()); + assertEquals("report.pdf", controller.filesTable.getItems().get(0).getName()); + assertFalse(controller.errorLabel.isVisible()); } @Test void resourceNotFoundShowsErrorAndLeavesTableEmpty() { when(resourceService.getById(RESOURCE_ID)).thenThrow(new ServiceException("Ресурс не найден")); - loadPage(robot); + controller.setResourceId(RESOURCE_ID); - Label error = robot.lookup("#errorLabel").queryAs(Label.class); - assertTrue(error.isVisible()); - assertEquals("Ресурс не найден", error.getText()); - assertTrue(filesTable(robot).getItems().isEmpty()); + assertTrue(controller.errorLabel.isVisible()); + assertEquals("Ресурс не найден", controller.errorLabel.getText()); + assertTrue(controller.filesTable.getItems().isEmpty()); verifyNoInteractions(navigator); } @Test void downloadSavesFileAndShowsStatus(@TempDir Path tempDir) { when(resourceService.getById(RESOURCE_ID)).thenReturn(resourceWithFiles()); - loadPage(robot); + controller.setResourceId(RESOURCE_ID); + FileInfo toDownload = controller.filesTable.getItems().get(0); controller.setDirectory(tempDir.toFile()); Path saved = tempDir.resolve("report.pdf"); when(fileService.downloadFile(eq(RESOURCE_ID), eq(FILE_ID_1), any())).thenReturn(saved); - robot.clickOn("Скачать"); + controller.onDownload(toDownload); verify(fileService).downloadFile(RESOURCE_ID, FILE_ID_1, tempDir); - assertEquals("Сохранено: report.pdf", textOf(robot, "#statusLabel")); + assertEquals("Сохранено: report.pdf", controller.statusLabel.getText()); } @Test void downloadErrorShowsInErrorLabel(@TempDir Path tempDir) { when(resourceService.getById(RESOURCE_ID)).thenReturn(resourceWithFiles()); - loadPage(robot); + controller.setResourceId(RESOURCE_ID); + FileInfo toDownload = controller.filesTable.getItems().get(0); controller.setDirectory(tempDir.toFile()); when(fileService.downloadFile(eq(RESOURCE_ID), eq(FILE_ID_1), any())) .thenThrow(new ServiceException("Файл недоступен")); - robot.clickOn("Скачать"); + controller.onDownload(toDownload); - Label error = robot.lookup("#errorLabel").queryAs(Label.class); - assertTrue(error.isVisible()); - assertEquals("Файл недоступен", error.getText()); + assertTrue(controller.errorLabel.isVisible()); + assertEquals("Файл недоступен", controller.errorLabel.getText()); } @Test void cancelledDirectoryChooserDoesNotCallDownload() { when(resourceService.getById(RESOURCE_ID)).thenReturn(resourceWithFiles()); - loadPage(robot); + controller.setResourceId(RESOURCE_ID); + FileInfo toDownload = controller.filesTable.getItems().get(0); controller.setDirectory(null); - robot.clickOn("Скачать"); + + controller.onDownload(toDownload); verifyNoInteractions(fileService); } @Test void backNavigatesToResourceList() { - robot.clickOn("#backButton"); - + controller.onBack(new ActionEvent()); verify(navigator).goTo("/fxml/ResourceList.fxml"); } - private void loadPage(FxRobot robot) { - // Production ordering (see Navigator.goTo): FXML is loaded first with no id, and the id is - // set afterwards via withController. The controller must react to the late id itself; - // calling load() here would mask a controller that ignores post-load ids. - robot.interact(() -> controller.setResourceId(RESOURCE_ID)); - } - private Resource resourceWithFiles() { FileInfo first = new FileInfo(FILE_ID_1, "report.pdf", "application/pdf", 12345L); FileInfo second = new FileInfo(FILE_ID_2, "photo.png", "image/png", 678L); @@ -188,13 +188,4 @@ private Resource resourceWithFiles() { LocalDateTime.of(2024, 5, 7, 9, 10), List.of(first, second)); } - - private static String textOf(FxRobot robot, String query) { - return robot.lookup(query).queryAs(Label.class).getText(); - } - - @SuppressWarnings("unchecked") - private static TableView filesTable(FxRobot robot) { - return robot.lookup("#filesTable").queryAs(TableView.class); - } } diff --git a/src/test/java/com/DesktopApplicationClientJava/controllers/ResourceFormControllerTest.java b/src/test/java/com/DesktopApplicationClientJava/controllers/ResourceFormControllerTest.java new file mode 100644 index 0000000..6d99360 --- /dev/null +++ b/src/test/java/com/DesktopApplicationClientJava/controllers/ResourceFormControllerTest.java @@ -0,0 +1,192 @@ +package com.DesktopApplicationClientJava.controllers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.DesktopApplicationClientJava.entities.Resource; +import com.DesktopApplicationClientJava.navigation.Navigator; +import com.DesktopApplicationClientJava.services.ResourceService; +import com.DesktopApplicationClientJava.services.ServiceException; +import com.DesktopApplicationClientJava.services.ServiceRegistry; +import com.DesktopApplicationClientJava.session.Session; +import com.DesktopApplicationClientJava.utils.ControllerWiring; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ResourceFormControllerTest { + + private static final UUID RESOURCE_ID = UUID.randomUUID(); + private static final String TITLE = "Годовой отчёт"; + private static final String DESCRIPTION = "Описание документа"; + + private ResourceService resourceService; + private Navigator navigator; + private ResourceFormController controller; + + @BeforeAll + static void bootstrapFx() { + try { + Platform.startup(() -> {}); + Platform.setImplicitExit(false); + } catch (IllegalStateException alreadyStarted) { + // JVM-wide singleton; another test class already started the toolkit. + } + } + + @BeforeEach + void setUp() { + resourceService = mock(ResourceService.class); + navigator = mock(Navigator.class); + + ServiceRegistry services = mock(ServiceRegistry.class); + when(services.getResourceService()).thenReturn(resourceService); + + controller = new ResourceFormController(); + ControllerWiring.wire(controller, navigator, new Session(), services); + + controller.titleField = new TextField(); + controller.descriptionArea = new TextArea(); + controller.errorLabel = new Label(); + controller.formTitleLabel = new Label(); + + controller.initialize(); + } + + @Test + void createModeShowsNewDocumentTitle() { + assertEquals("Новый документ", controller.formTitleLabel.getText()); + assertFalse(controller.errorLabel.isVisible()); + } + + @Test + void blankTitleShowsErrorAndDoesNotSave() { + controller.onSave(new ActionEvent()); + + assertTrue(controller.errorLabel.isVisible()); + assertEquals("Название обязательно для заполнения", controller.errorLabel.getText()); + verify(resourceService, never()).create(anyString(), any()); + verify(navigator, never()).goTo(anyString()); + } + + @Test + void shortTitleShowsErrorAndDoesNotSave() { + controller.titleField.setText("ab"); + controller.onSave(new ActionEvent()); + + assertTrue(controller.errorLabel.isVisible()); + assertEquals("Название не может быть короче 3 символов", controller.errorLabel.getText()); + verify(resourceService, never()).create(anyString(), any()); + } + + @Test + void longTitleShowsErrorAndDoesNotSave() { + controller.titleField.setText("a".repeat(256)); + controller.onSave(new ActionEvent()); + + assertTrue(controller.errorLabel.isVisible()); + assertEquals("Название не может быть длиннее 255 символов", controller.errorLabel.getText()); + verify(resourceService, never()).create(anyString(), any()); + verify(navigator, never()).goTo(anyString()); + } + + @Test + void validInputCreatesAndNavigatesToList() { + controller.titleField.setText(TITLE); + controller.descriptionArea.setText(DESCRIPTION); + controller.onSave(new ActionEvent()); + + verify(resourceService).create(TITLE, DESCRIPTION); + verify(navigator).goTo("/fxml/ResourceList.fxml"); + assertFalse(controller.errorLabel.isVisible()); + } + + @Test + void blankDescriptionIsSavedAsNull() { + controller.titleField.setText(TITLE); + controller.onSave(new ActionEvent()); + + verify(resourceService).create(TITLE, null); + verify(navigator).goTo("/fxml/ResourceList.fxml"); + } + + @Test + void serviceErrorShowsInErrorLabelAndDoesNotNavigate() { + when(resourceService.create(anyString(), any())) + .thenThrow(new ServiceException("Не удалось сохранить")); + + controller.titleField.setText(TITLE); + controller.onSave(new ActionEvent()); + + assertTrue(controller.errorLabel.isVisible()); + assertEquals("Не удалось сохранить", controller.errorLabel.getText()); + verify(navigator, never()).goTo(anyString()); + } + + @Test + void cancelNavigatesToListWithoutSaving() { + controller.titleField.setText(TITLE); + controller.onCancel(new ActionEvent()); + + verify(navigator).goTo("/fxml/ResourceList.fxml"); + verify(resourceService, never()).create(anyString(), any()); + verify(resourceService, never()).update(any(), anyString(), any()); + } + + @Test + void editModePrefillsExistingValues() { + when(resourceService.getById(RESOURCE_ID)).thenReturn(existingResource()); + controller.setResourceId(RESOURCE_ID); + + assertEquals("Редактирование документа", controller.formTitleLabel.getText()); + assertEquals("Старое название", controller.titleField.getText()); + assertEquals("Старое описание", controller.descriptionArea.getText()); + } + + @Test + void editModeSaveUpdatesAndNavigatesToList() { + when(resourceService.getById(RESOURCE_ID)).thenReturn(existingResource()); + controller.setResourceId(RESOURCE_ID); + + controller.titleField.setText("Новое название"); + controller.onSave(new ActionEvent()); + + verify(resourceService).update(RESOURCE_ID, "Новое название", "Старое описание"); + verify(resourceService, never()).create(anyString(), any()); + verify(navigator).goTo("/fxml/ResourceList.fxml"); + } + + @Test + void editModeLoadErrorShowsInErrorLabel() { + when(resourceService.getById(RESOURCE_ID)).thenThrow(new ServiceException("Ресурс не найден")); + controller.setResourceId(RESOURCE_ID); + + assertTrue(controller.errorLabel.isVisible()); + assertEquals("Ресурс не найден", controller.errorLabel.getText()); + } + + private Resource existingResource() { + return new Resource( + RESOURCE_ID, + "Старое название", + "Старое описание", + LocalDateTime.of(2024, 5, 6, 7, 8), + LocalDateTime.of(2024, 5, 7, 9, 10), + List.of()); + } +} diff --git a/src/test/java/services/AuthServiceTest.java b/src/test/java/com/DesktopApplicationClientJava/services/AuthServiceTest.java similarity index 96% rename from src/test/java/services/AuthServiceTest.java rename to src/test/java/com/DesktopApplicationClientJava/services/AuthServiceTest.java index 9fb580d..0b48c66 100644 --- a/src/test/java/services/AuthServiceTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/services/AuthServiceTest.java @@ -1,4 +1,4 @@ -package services; +package com.DesktopApplicationClientJava.services; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -7,7 +7,6 @@ import com.DesktopApplicationClientJava.Api.ApiClient; import com.DesktopApplicationClientJava.entities.Role; import com.DesktopApplicationClientJava.entities.User; -import com.DesktopApplicationClientJava.services.AuthService; import com.DesktopApplicationClientJava.services.impl.AuthServiceImpl; import com.DesktopApplicationClientJava.session.Session; import java.util.UUID; diff --git a/src/test/java/services/mock/MockServicesTest.java b/src/test/java/com/DesktopApplicationClientJava/services/mock/MockServicesTest.java similarity index 91% rename from src/test/java/services/mock/MockServicesTest.java rename to src/test/java/com/DesktopApplicationClientJava/services/mock/MockServicesTest.java index 34519e1..c54e2df 100644 --- a/src/test/java/services/mock/MockServicesTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/services/mock/MockServicesTest.java @@ -1,4 +1,4 @@ -package services.mock; +package com.DesktopApplicationClientJava.services.mock; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -13,11 +13,6 @@ import com.DesktopApplicationClientJava.services.ServiceException; import com.DesktopApplicationClientJava.services.filter.ResourceFilter; import com.DesktopApplicationClientJava.services.filter.UserFilter; -import com.DesktopApplicationClientJava.services.mock.MockAuthService; -import com.DesktopApplicationClientJava.services.mock.MockFileService; -import com.DesktopApplicationClientJava.services.mock.MockResourceService; -import com.DesktopApplicationClientJava.services.mock.MockStore; -import com.DesktopApplicationClientJava.services.mock.MockUsersService; import com.DesktopApplicationClientJava.session.Session; import java.nio.file.Files; import java.nio.file.Path; diff --git a/src/test/java/ui/FxTestBase.java b/src/test/java/com/DesktopApplicationClientJava/ui/FxTestBase.java similarity index 97% rename from src/test/java/ui/FxTestBase.java rename to src/test/java/com/DesktopApplicationClientJava/ui/FxTestBase.java index f46c6cb..167ef27 100644 --- a/src/test/java/ui/FxTestBase.java +++ b/src/test/java/com/DesktopApplicationClientJava/ui/FxTestBase.java @@ -1,4 +1,4 @@ -package ui; +package com.DesktopApplicationClientJava.ui; import java.util.concurrent.TimeoutException; import javafx.scene.control.TextInputControl; diff --git a/src/test/java/ui/MockFlowTest.java b/src/test/java/com/DesktopApplicationClientJava/ui/MockFlowTest.java similarity index 98% rename from src/test/java/ui/MockFlowTest.java rename to src/test/java/com/DesktopApplicationClientJava/ui/MockFlowTest.java index 1df371c..03c582f 100644 --- a/src/test/java/ui/MockFlowTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/ui/MockFlowTest.java @@ -1,4 +1,4 @@ -package ui; +package com.DesktopApplicationClientJava.ui; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -25,7 +25,6 @@ * backend). Proves that MOCK_DATA mode renders actual data. */ class MockFlowTest extends FxTestBase { - private static final UUID SEEDED_RESOURCE_ID = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); diff --git a/src/test/java/ui/ResourceListControllerTest.java b/src/test/java/com/DesktopApplicationClientJava/ui/ResourceListControllerTest.java similarity index 99% rename from src/test/java/ui/ResourceListControllerTest.java rename to src/test/java/com/DesktopApplicationClientJava/ui/ResourceListControllerTest.java index af8fe27..71611f6 100644 --- a/src/test/java/ui/ResourceListControllerTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/ui/ResourceListControllerTest.java @@ -1,4 +1,4 @@ -package ui; +package com.DesktopApplicationClientJava.ui; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -158,7 +158,7 @@ void initialOpenLoadsFirstPageWithoutInteraction() { @Test void sortDefaultsToNewestFirst() { - + @SuppressWarnings("unchecked") ComboBox sort = robot.lookup("#sortCombo").queryAs(ComboBox.class); assertEquals( List.of("Сначала новые", "Сначала старые", "По названию (А-Я)", "По названию (Я-А)"), diff --git a/src/test/java/ui/integration/IntegrationTestCase.java b/src/test/java/com/DesktopApplicationClientJava/ui/integration/IntegrationTestCase.java similarity index 97% rename from src/test/java/ui/integration/IntegrationTestCase.java rename to src/test/java/com/DesktopApplicationClientJava/ui/integration/IntegrationTestCase.java index c3f0a22..824b411 100644 --- a/src/test/java/ui/integration/IntegrationTestCase.java +++ b/src/test/java/com/DesktopApplicationClientJava/ui/integration/IntegrationTestCase.java @@ -1,4 +1,4 @@ -package ui.integration; +package com.DesktopApplicationClientJava.ui.integration; import static org.junit.jupiter.api.Assumptions.assumeTrue; diff --git a/src/test/java/ui/integration/LoginIntegrationTest.java b/src/test/java/com/DesktopApplicationClientJava/ui/integration/LoginIntegrationTest.java similarity index 93% rename from src/test/java/ui/integration/LoginIntegrationTest.java rename to src/test/java/com/DesktopApplicationClientJava/ui/integration/LoginIntegrationTest.java index 280963a..9dc3c28 100644 --- a/src/test/java/ui/integration/LoginIntegrationTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/ui/integration/LoginIntegrationTest.java @@ -1,4 +1,4 @@ -package ui.integration; +package com.DesktopApplicationClientJava.ui.integration; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/src/test/java/ui/integration/ResourceDetailsIntegrationTest.java b/src/test/java/com/DesktopApplicationClientJava/ui/integration/ResourceDetailsIntegrationTest.java similarity index 96% rename from src/test/java/ui/integration/ResourceDetailsIntegrationTest.java rename to src/test/java/com/DesktopApplicationClientJava/ui/integration/ResourceDetailsIntegrationTest.java index 8080b51..2fa31ee 100644 --- a/src/test/java/ui/integration/ResourceDetailsIntegrationTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/ui/integration/ResourceDetailsIntegrationTest.java @@ -1,4 +1,4 @@ -package ui.integration; +package com.DesktopApplicationClientJava.ui.integration; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/src/test/java/ui/integration/ResourceFormIntegrationTest.java b/src/test/java/com/DesktopApplicationClientJava/ui/integration/ResourceFormIntegrationTest.java similarity index 98% rename from src/test/java/ui/integration/ResourceFormIntegrationTest.java rename to src/test/java/com/DesktopApplicationClientJava/ui/integration/ResourceFormIntegrationTest.java index 5c439cf..04944f0 100644 --- a/src/test/java/ui/integration/ResourceFormIntegrationTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/ui/integration/ResourceFormIntegrationTest.java @@ -1,4 +1,4 @@ -package ui.integration; +package com.DesktopApplicationClientJava.ui.integration; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -19,6 +19,7 @@ import com.DesktopApplicationClientJava.services.impl.AuthServiceImpl; import com.DesktopApplicationClientJava.services.impl.ResourceServiceImpl; import com.DesktopApplicationClientJava.session.Session; +import com.DesktopApplicationClientJava.ui.FxTestBase; import com.DesktopApplicationClientJava.utils.ControllerWiring; import java.util.UUID; import javafx.event.ActionEvent; @@ -37,7 +38,6 @@ import org.junit.jupiter.api.Test; import org.testfx.api.FxRobot; import org.testfx.api.FxToolkit; -import ui.FxTestBase; /** * Integration tests for the resource create/edit flow: real services with a real ApiClient against @@ -58,7 +58,6 @@ class ResourceFormIntegrationTest extends IntegrationTestCase { private Navigator navigator; private Stage stage; private Session session; - private ServiceRegistry services; @BeforeAll static void initToolkit() throws Exception { @@ -97,7 +96,6 @@ void setUpServices() { ApiClient apiClient = new ApiClient(session); authService = new AuthServiceImpl(apiClient, session); resourceService = new ResourceServiceImpl(apiClient); - services = new ServiceRegistry(apiClient, session); } @Test diff --git a/src/test/java/ui/integration/ResourceListIntegrationTest.java b/src/test/java/com/DesktopApplicationClientJava/ui/integration/ResourceListIntegrationTest.java similarity index 98% rename from src/test/java/ui/integration/ResourceListIntegrationTest.java rename to src/test/java/com/DesktopApplicationClientJava/ui/integration/ResourceListIntegrationTest.java index 5ddb7b8..ce8fabf 100644 --- a/src/test/java/ui/integration/ResourceListIntegrationTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/ui/integration/ResourceListIntegrationTest.java @@ -1,4 +1,4 @@ -package ui.integration; +package com.DesktopApplicationClientJava.ui.integration; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -15,6 +15,7 @@ import com.DesktopApplicationClientJava.services.impl.AuthServiceImpl; import com.DesktopApplicationClientJava.services.impl.ResourceServiceImpl; import com.DesktopApplicationClientJava.session.Session; +import com.DesktopApplicationClientJava.ui.FxTestBase; import com.DesktopApplicationClientJava.utils.ControllerWiring; import java.util.List; import java.util.UUID; @@ -37,7 +38,6 @@ import org.testfx.api.FxRobot; import org.testfx.api.FxToolkit; import org.testfx.util.WaitForAsyncUtils; -import ui.FxTestBase; /** * UI-level integration tests for the resource list page: real services with a real ApiClient @@ -245,6 +245,7 @@ private static void searchFor(FxRobot robot, String query) { * Monocle. Firing the search action afterwards triggers the same {@code loadAsync} path with the * newly selected sort, which is what the test needs. */ + @SuppressWarnings("unchecked") private static void selectSortAndReload(FxRobot robot, String option) { robot.interact( () -> { diff --git a/src/test/java/validation/EmailValidatorTest.java b/src/test/java/com/DesktopApplicationClientJava/validation/EmailValidatorTest.java similarity index 93% rename from src/test/java/validation/EmailValidatorTest.java rename to src/test/java/com/DesktopApplicationClientJava/validation/EmailValidatorTest.java index 6869b47..1993ad4 100644 --- a/src/test/java/validation/EmailValidatorTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/validation/EmailValidatorTest.java @@ -1,9 +1,8 @@ -package validation; +package com.DesktopApplicationClientJava.validation; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import com.DesktopApplicationClientJava.validation.EmailValidator; import com.DesktopApplicationClientJava.validation.utils.ErrorMessages; import org.junit.jupiter.api.Test; diff --git a/src/test/java/validation/FirstNameValidatorTest.java b/src/test/java/com/DesktopApplicationClientJava/validation/FirstNameValidatorTest.java similarity index 89% rename from src/test/java/validation/FirstNameValidatorTest.java rename to src/test/java/com/DesktopApplicationClientJava/validation/FirstNameValidatorTest.java index 047afa7..4a1aef5 100644 --- a/src/test/java/validation/FirstNameValidatorTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/validation/FirstNameValidatorTest.java @@ -1,9 +1,8 @@ -package validation; +package com.DesktopApplicationClientJava.validation; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import com.DesktopApplicationClientJava.validation.FirstNameValidator; import com.DesktopApplicationClientJava.validation.utils.ErrorMessages; import org.junit.jupiter.api.Test; diff --git a/src/test/java/validation/LastNameValidatorTest.java b/src/test/java/com/DesktopApplicationClientJava/validation/LastNameValidatorTest.java similarity index 89% rename from src/test/java/validation/LastNameValidatorTest.java rename to src/test/java/com/DesktopApplicationClientJava/validation/LastNameValidatorTest.java index 6158c2f..cef7cd8 100644 --- a/src/test/java/validation/LastNameValidatorTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/validation/LastNameValidatorTest.java @@ -1,9 +1,8 @@ -package validation; +package com.DesktopApplicationClientJava.validation; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import com.DesktopApplicationClientJava.validation.LastNameValidator; import com.DesktopApplicationClientJava.validation.utils.ErrorMessages; import org.junit.jupiter.api.Test; diff --git a/src/test/java/validation/PasswordValidatorTest.java b/src/test/java/com/DesktopApplicationClientJava/validation/PasswordValidatorTest.java similarity index 89% rename from src/test/java/validation/PasswordValidatorTest.java rename to src/test/java/com/DesktopApplicationClientJava/validation/PasswordValidatorTest.java index 6ae4acc..d2f0009 100644 --- a/src/test/java/validation/PasswordValidatorTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/validation/PasswordValidatorTest.java @@ -1,9 +1,8 @@ -package validation; +package com.DesktopApplicationClientJava.validation; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import com.DesktopApplicationClientJava.validation.PasswordValidator; import com.DesktopApplicationClientJava.validation.utils.ErrorMessages; import org.junit.jupiter.api.Test; diff --git a/src/test/java/validation/ThirdNameValidatorTest.java b/src/test/java/com/DesktopApplicationClientJava/validation/ThirdNameValidatorTest.java similarity index 90% rename from src/test/java/validation/ThirdNameValidatorTest.java rename to src/test/java/com/DesktopApplicationClientJava/validation/ThirdNameValidatorTest.java index 7e97020..447383f 100644 --- a/src/test/java/validation/ThirdNameValidatorTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/validation/ThirdNameValidatorTest.java @@ -1,9 +1,8 @@ -package validation; +package com.DesktopApplicationClientJava.validation; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import com.DesktopApplicationClientJava.validation.ThirdNameValidator; import com.DesktopApplicationClientJava.validation.utils.ErrorMessages; import org.junit.jupiter.api.Test; diff --git a/src/test/java/ui/ResourceFormControllerTest.java b/src/test/java/ui/ResourceFormControllerTest.java deleted file mode 100644 index 008a423..0000000 --- a/src/test/java/ui/ResourceFormControllerTest.java +++ /dev/null @@ -1,231 +0,0 @@ -package ui; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.DesktopApplicationClientJava.controllers.ResourceFormController; -import com.DesktopApplicationClientJava.entities.Resource; -import com.DesktopApplicationClientJava.navigation.Navigator; -import com.DesktopApplicationClientJava.services.ResourceService; -import com.DesktopApplicationClientJava.services.ServiceException; -import com.DesktopApplicationClientJava.services.ServiceRegistry; -import com.DesktopApplicationClientJava.session.Session; -import com.DesktopApplicationClientJava.utils.ControllerWiring; -import java.time.LocalDateTime; -import java.util.List; -import java.util.UUID; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; -import javafx.scene.Scene; -import javafx.scene.control.Label; -import javafx.scene.control.TextArea; -import javafx.scene.control.TextField; -import javafx.stage.Stage; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.testfx.api.FxRobot; -import org.testfx.api.FxToolkit; - -class ResourceFormControllerTest extends FxTestBase { - - private static final UUID RESOURCE_ID = UUID.randomUUID(); - private static final String TITLE = "Годовой отчёт"; - private static final String DESCRIPTION = "Описание документа"; - - private ResourceService resourceService; - private ServiceRegistry services; - private Navigator navigator; - private ResourceFormController controller; - private Stage stage; - - @BeforeEach - void resetScene() throws Exception { - FxToolkit.setupStage( - stage -> { - try { - start(stage); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - void start(Stage stage) throws Exception { - resourceService = mock(ResourceService.class); - services = mock(ServiceRegistry.class); - when(services.getResourceService()).thenReturn(resourceService); - navigator = mock(Navigator.class); - this.stage = stage; - loadForm(null); - } - - /** Loads the form as {@code initialize()} would see it (resourceId set before FXML load). */ - private void loadForm(UUID resourceId) throws Exception { - controller = new ResourceFormController(); - controller.setResourceId(resourceId); - FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/ResourceForm.fxml")); - loader.setControllerFactory( - clazz -> { - ControllerWiring.wire(controller, navigator, new Session(), services); - return controller; - }); - Parent root = loader.load(); - stage.setScene(new Scene(root)); - stage.show(); - } - - /** - * Production ordering (see Navigator.goTo): the form is loaded with no id, and the id is set - * afterwards via withController. Setting it here post-load must drive the controller into edit - * mode; pre-setting it before load would mask a controller that ignores late ids. - */ - private void openEditForm(FxRobot robot, UUID resourceId) { - robot.interact(() -> controller.setResourceId(resourceId)); - } - - @Test - void createModeShowsNewDocumentTitle() { - assertEquals("Новый документ", textOf(robot, "#formTitleLabel")); - assertFalse(robot.lookup("#errorLabel").queryAs(Label.class).isVisible()); - } - - @Test - void blankTitleShowsErrorAndDoesNotSave() { - robot.clickOn("#saveButton"); - - assertError(robot, "Название обязательно для заполнения"); - verify(resourceService, never()).create(anyString(), any()); - verify(resourceService, never()).update(any(), anyString(), any()); - verify(navigator, never()).goTo(anyString()); - } - - @Test - void shortTitleShowsErrorAndDoesNotSave() { - fillText(robot, "#titleField", "ab"); - robot.clickOn("#saveButton"); - - assertError(robot, "Название не может быть короче 3 символов"); - verify(resourceService, never()).create(anyString(), any()); - verify(navigator, never()).goTo(anyString()); - } - - @Test - void longTitleShowsErrorAndDoesNotSave() { - fillText(robot, "#titleField", "a".repeat(256)); - robot.clickOn("#saveButton"); - - assertError(robot, "Название не может быть длиннее 255 символов"); - verify(resourceService, never()).create(anyString(), any()); - verify(resourceService, never()).update(any(), anyString(), any()); - verify(navigator, never()).goTo(anyString()); - } - - @Test - void validInputCreatesAndNavigatesToList() { - fillText(robot, "#titleField", TITLE); - fillText(robot, "#descriptionArea", DESCRIPTION); - robot.clickOn("#saveButton"); - - verify(resourceService).create(TITLE, DESCRIPTION); - verify(navigator).goTo("/fxml/ResourceList.fxml"); - assertFalse(robot.lookup("#errorLabel").queryAs(Label.class).isVisible()); - } - - @Test - void blankDescriptionIsSavedAsNull() { - fillText(robot, "#titleField", TITLE); - robot.clickOn("#saveButton"); - - verify(resourceService).create(TITLE, null); - verify(navigator).goTo("/fxml/ResourceList.fxml"); - } - - @Test - void serviceErrorShowsInErrorLabelAndDoesNotNavigate() { - when(resourceService.create(anyString(), any())) - .thenThrow(new ServiceException("Не удалось сохранить")); - - fillText(robot, "#titleField", TITLE); - robot.clickOn("#saveButton"); - - assertError(robot, "Не удалось сохранить"); - verify(navigator, never()).goTo(anyString()); - } - - @Test - void cancelNavigatesToListWithoutSaving() { - fillText(robot, "#titleField", TITLE); - robot.clickOn("#cancelButton"); - - verify(navigator).goTo("/fxml/ResourceList.fxml"); - verify(resourceService, never()).create(anyString(), any()); - verify(resourceService, never()).update(any(), anyString(), any()); - } - - @Test - void editModePrefillsExistingValues() { - when(resourceService.getById(RESOURCE_ID)).thenReturn(existingResource()); - openEditForm(robot, RESOURCE_ID); - - assertEquals("Редактирование документа", textOf(robot, "#formTitleLabel")); - assertEquals("Старое название", fieldText(robot, "#titleField")); - assertEquals("Старое описание", areaText(robot, "#descriptionArea")); - } - - @Test - void editModeSaveUpdatesAndNavigatesToList() { - when(resourceService.getById(RESOURCE_ID)).thenReturn(existingResource()); - openEditForm(robot, RESOURCE_ID); - - robot.interact( - () -> robot.lookup("#titleField").queryAs(TextField.class).setText("Новое название")); - robot.clickOn("#saveButton"); - - verify(resourceService).update(RESOURCE_ID, "Новое название", "Старое описание"); - verify(resourceService, never()).create(anyString(), any()); - verify(navigator).goTo("/fxml/ResourceList.fxml"); - } - - @Test - void editModeLoadErrorShowsInErrorLabel() { - when(resourceService.getById(RESOURCE_ID)).thenThrow(new ServiceException("Ресурс не найден")); - openEditForm(robot, RESOURCE_ID); - - assertError(robot, "Ресурс не найден"); - } - - private Resource existingResource() { - return new Resource( - RESOURCE_ID, - "Старое название", - "Старое описание", - LocalDateTime.of(2024, 5, 6, 7, 8), - LocalDateTime.of(2024, 5, 7, 9, 10), - List.of()); - } - - private static void assertError(FxRobot robot, String expected) { - Label error = robot.lookup("#errorLabel").queryAs(Label.class); - assertTrue(error.isVisible()); - assertEquals(expected, error.getText()); - } - - private static String textOf(FxRobot robot, String query) { - return robot.lookup(query).queryAs(Label.class).getText(); - } - - private static String fieldText(FxRobot robot, String query) { - return robot.lookup(query).queryAs(TextField.class).getText(); - } - - private static String areaText(FxRobot robot, String query) { - return robot.lookup(query).queryAs(TextArea.class).getText(); - } -} From 1bd09b9a32810895750ea05ca10ea9720057c83f Mon Sep 17 00:00:00 2001 From: Vladimir Valekzhanin Date: Wed, 14 Oct 2026 22:32:28 +0300 Subject: [PATCH 3/3] chore: don't ignore settings.json --- .gitignore | 2 +- .vscode/settings.json | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index b7bbdd2..4b9ba18 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,7 @@ buildNumber.properties # JDT-specific (Eclipse Java Development Tools) .classpath -.vscode +.vscode/launch.json .idea tools/scenicview/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c3631ee --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,24 @@ +{ + "[java]": { + "editor.detectIndentation": false, + }, + "java.project.sourcePaths": [ + "src/main/java/com/DesktopApplicationClientJava", + ], + "java.format.settings.url": ".vscode/eclipse-java-google-style.xml", + "java.format.settings.profile": "GoogleStyle", + "java.configuration.updateBuildConfiguration": "automatic", + "xml.validation.noGrammar": "ignore", + "java.checkstyle.configuration": "${workspaceFolder}/checkstyle.xml", + "java.checkstyle.version": "14.3.0", + "java.autobuild.enabled": false, + "files.watcherExclude": { + "**/target/**": true + }, + "files.exclude": { + "**/target": true + }, + "java.import.exclusions": [ + "**/target/**" + ] +} \ No newline at end of file