diff --git a/README.md b/README.md index 209a998..69bcc23 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # WPI Core -**WPI Core** is a Java library providing foundational abstractions for secure API communication, including authentication, endpoint access control, and request/response modeling, designed to be consumed by higher-level WPI client implementations. +**WPI Core** is a Java library providing foundational abstractions for secure API communication, including authentication, endpoint access control, request/response modeling, and application-level service contracts for WPI clients. Part of the **Waterflow Pixel (WP)** ecosystem developed by [VerbaTechTeam](https://github.com/VerbaTechTeam) — *We create words that devices understand.* @@ -12,7 +12,7 @@ Part of the **Waterflow Pixel (WP)** ecosystem developed by [VerbaTechTeam](http | Repository | Layer | Description | |---|---|---| -| **wpi-core** *(this repo)* | WPI | Core API abstractions in Java — auth, endpoints, request model | +| **wpi-core** *(this repo)* | WPI | Core API abstractions in Java — auth, endpoints, request model, service contracts | | [waterflow-pixel-unit](https://github.com/VerbaTechTeam/waterflow-pixel-unit) | WPU | MicroPython firmware for Raspberry Pi Pico 2W with built-in REST HTTP server for direct LED strip control | For a full overview of the system architecture and roadmap (including the planned **WPC** controller layer and **wpu-emulator**), see the [VerbaTechTeam organization profile](https://github.com/VerbaTechTeam). @@ -48,39 +48,35 @@ Then reference it in your project's `pom.xml`: ## Architecture -The library follows a clean, layered architecture: +The library follows a layered architecture: -``` +```text pl.vtt.wpi.core ├── application │ ├── config # AuthorizationHolder (thread-local auth state) -│ ├── exception # Domain exceptions (e.g. IncorrectUsernameOrPasswordException) -│ ├── service # Service interfaces (LoginService) -│ │ └── impl # Internal implementations (not exported) -│ └── util # RequestFactory, RequestHandler interfaces -└── domain - └── model - ├── Authorization.java - ├── Credentials.java - ├── Request.java - └── endpoint - ├── EndpointDescriptor.java - ├── Method.java - ├── RequestTarget.java - ├── Resource.java - └── UserGroup.java +│ ├── exception # Application-level exceptions +│ └── service +│ ├── ... # Service interfaces (LoginService, RuntimeDataService, etc.) +│ └── impl # Internal service implementations +├── domain +│ ├── dto +│ ├── model +│ └── port # Input/Output port contracts + endpoint-specific port implementations +└── infrastructure + ├── Request / Response + ├── RequestFactory / RequestHandler / RequestSender + └── factory # SynchronizedRequestFactory ``` ## Key Concepts ### Authentication -Login is handled by `LoginService`. Internally, `LoginServiceImpl` delegates authorization to a dedicated port (`AuthOutputPort`), so request execution is decoupled from service orchestration. +Login is handled by `LoginService`. Internally, `LoginServiceImpl` delegates authorization to a dedicated output port (`AuthOutputPort`), so request execution is decoupled from service orchestration. -On success, the resulting `Credentials` (username + token) are stored as a Basic Auth header in `AuthorizationHolder` — a thread-local holder used to attach authorization to outgoing requests. +On success, resulting `Credentials` (username + token) are stored in `AuthorizationHolder` — a thread-local holder used to attach authorization to outgoing requests. ```java -// Provided by a higher-level module LoginService loginService = ...; Credentials credentials = loginService.login("admin", "password"); @@ -108,29 +104,37 @@ boolean allowed = RequestTarget.DATA_UPDATE.allow(Method.PUT, userGroups); ### Request Model -A `Request` carries the target URL, authorization header, and an optional typed payload: +A `Request` carries timestamp, HTTP method, target URL, authorization header, and an optional typed payload: ```java -record Request(Method method, String url, Authorization authorization, T payload) {} +Request request = new Request<>( + null, // timestamp (null => now) + Method.GET, + "https://host/api/data", + authorization, + payload +); ``` -### Implementing a Client - -`wpi-core` exposes only the `LoginService` interface — the implementation is internal to the module. A higher-level module (e.g. `wpi-desktop`) is responsible for instantiating the service and exposing it to the client. +### Application Services -From the client's perspective, usage is limited to the public interface: +The package `pl.vtt.wpi.core.application.service` currently exposes interfaces for: -```java -// Provided by a higher-level module -LoginService loginService = ...; +- `LoginService` +- `AdminPasswordService` +- `UserManagementService` +- `RuntimeDataService` +- `PixelProgramService` +- `NetworkConfigurationService` +- `DeviceInfoService` +- `DebugService` +- `RebootService` -Credentials credentials = loginService.login("admin", "password"); -loginService.logout(); -``` +Concrete implementations are provided in `pl.vtt.wpi.core.application.service.impl`. ### Domain Ports -The package `pl.vtt.wpi.core.domain.port` contains concrete port implementations for domain operations: +The package `pl.vtt.wpi.core.domain.port` contains port contracts and endpoint-specific implementations: - **Output ports**: `AuthOutputPort`, `DeviceInfoOutputPort`, `RuntimeDataOutputPort`, `CurrentStateOutputPort`, `PixelProgramsOutputPort`, `UsersOutputPort` - **Input ports**: `RuntimeDataInputPort`, `WifiConfigInputPort`, `PixelProgramsInputPort`, `UserCreateInputPort`, `RestartInputPort`, `LogsDeleteInputPort` @@ -147,13 +151,7 @@ mvn test mvn package ``` -Unit tests are written with JUnit Jupiter 5 and currently verify: - -- successful login flow (`Credentials` returned + Authorization header updated), -- validation for null/blank username and password, -- incorrect credentials / null auth response handling, -- cleanup of `AuthorizationHolder` after failed login attempts (including runtime failures), -- logout behavior. +Unit tests are written with JUnit Jupiter 5 and currently include coverage for port-level behavior and application-service logic (including `UserCreateInputPort` / `UserCreateRequest`, login flow, and related service orchestration paths). ## CI diff --git a/src/main/java/pl/vtt/wpi/core/application/exception/AdminPasswordResetException.java b/src/main/java/pl/vtt/wpi/core/application/exception/AdminPasswordResetException.java new file mode 100644 index 0000000..953b990 --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/exception/AdminPasswordResetException.java @@ -0,0 +1,13 @@ +package pl.vtt.wpi.core.application.exception; + +public class AdminPasswordResetException extends ApplicationServiceException { + private static final long serialVersionUID = 1L; + + public AdminPasswordResetException(String message) { + super(message); + } + + public AdminPasswordResetException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/exception/ApplicationServiceException.java b/src/main/java/pl/vtt/wpi/core/application/exception/ApplicationServiceException.java new file mode 100644 index 0000000..c113a74 --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/exception/ApplicationServiceException.java @@ -0,0 +1,13 @@ +package pl.vtt.wpi.core.application.exception; + +public abstract class ApplicationServiceException extends Exception { + private static final long serialVersionUID = 1L; + + protected ApplicationServiceException(String message) { + super(message); + } + + protected ApplicationServiceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/exception/AuthenticationServiceUnavailableException.java b/src/main/java/pl/vtt/wpi/core/application/exception/AuthenticationServiceUnavailableException.java new file mode 100644 index 0000000..e5a2f4c --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/exception/AuthenticationServiceUnavailableException.java @@ -0,0 +1,13 @@ +package pl.vtt.wpi.core.application.exception; + +public class AuthenticationServiceUnavailableException extends ApplicationServiceException { + private static final long serialVersionUID = 1L; + + public AuthenticationServiceUnavailableException(String message) { + super(message); + } + + public AuthenticationServiceUnavailableException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/exception/DebugDataAccessException.java b/src/main/java/pl/vtt/wpi/core/application/exception/DebugDataAccessException.java new file mode 100644 index 0000000..8dde42d --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/exception/DebugDataAccessException.java @@ -0,0 +1,13 @@ +package pl.vtt.wpi.core.application.exception; + +public class DebugDataAccessException extends ApplicationServiceException { + private static final long serialVersionUID = 1L; + + public DebugDataAccessException(String message) { + super(message); + } + + public DebugDataAccessException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/exception/DeviceInfoReadException.java b/src/main/java/pl/vtt/wpi/core/application/exception/DeviceInfoReadException.java new file mode 100644 index 0000000..8ba7378 --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/exception/DeviceInfoReadException.java @@ -0,0 +1,13 @@ +package pl.vtt.wpi.core.application.exception; + +public class DeviceInfoReadException extends ApplicationServiceException { + private static final long serialVersionUID = 1L; + + public DeviceInfoReadException(String message) { + super(message); + } + + public DeviceInfoReadException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/exception/DeviceRebootException.java b/src/main/java/pl/vtt/wpi/core/application/exception/DeviceRebootException.java new file mode 100644 index 0000000..e7b0462 --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/exception/DeviceRebootException.java @@ -0,0 +1,13 @@ +package pl.vtt.wpi.core.application.exception; + +public class DeviceRebootException extends ApplicationServiceException { + private static final long serialVersionUID = 1L; + + public DeviceRebootException(String message) { + super(message); + } + + public DeviceRebootException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/exception/NetworkConfigurationException.java b/src/main/java/pl/vtt/wpi/core/application/exception/NetworkConfigurationException.java new file mode 100644 index 0000000..8c43598 --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/exception/NetworkConfigurationException.java @@ -0,0 +1,13 @@ +package pl.vtt.wpi.core.application.exception; + +public class NetworkConfigurationException extends ApplicationServiceException { + private static final long serialVersionUID = 1L; + + public NetworkConfigurationException(String message) { + super(message); + } + + public NetworkConfigurationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/exception/PixelProgramOperationException.java b/src/main/java/pl/vtt/wpi/core/application/exception/PixelProgramOperationException.java new file mode 100644 index 0000000..7085a3f --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/exception/PixelProgramOperationException.java @@ -0,0 +1,13 @@ +package pl.vtt.wpi.core.application.exception; + +public class PixelProgramOperationException extends ApplicationServiceException { + private static final long serialVersionUID = 1L; + + public PixelProgramOperationException(String message) { + super(message); + } + + public PixelProgramOperationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/exception/RuntimeDataOperationException.java b/src/main/java/pl/vtt/wpi/core/application/exception/RuntimeDataOperationException.java new file mode 100644 index 0000000..129750c --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/exception/RuntimeDataOperationException.java @@ -0,0 +1,13 @@ +package pl.vtt.wpi.core.application.exception; + +public class RuntimeDataOperationException extends ApplicationServiceException { + private static final long serialVersionUID = 1L; + + public RuntimeDataOperationException(String message) { + super(message); + } + + public RuntimeDataOperationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/exception/UserManagementOperationException.java b/src/main/java/pl/vtt/wpi/core/application/exception/UserManagementOperationException.java new file mode 100644 index 0000000..d600467 --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/exception/UserManagementOperationException.java @@ -0,0 +1,13 @@ +package pl.vtt.wpi.core.application.exception; + +public class UserManagementOperationException extends ApplicationServiceException { + private static final long serialVersionUID = 1L; + + public UserManagementOperationException(String message) { + super(message); + } + + public UserManagementOperationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/service/AdminPasswordService.java b/src/main/java/pl/vtt/wpi/core/application/service/AdminPasswordService.java index 36c51d8..596e320 100644 --- a/src/main/java/pl/vtt/wpi/core/application/service/AdminPasswordService.java +++ b/src/main/java/pl/vtt/wpi/core/application/service/AdminPasswordService.java @@ -1,7 +1,8 @@ package pl.vtt.wpi.core.application.service; import pl.vtt.wpi.core.domain.dto.PasswordDto; +import pl.vtt.wpi.core.application.exception.AdminPasswordResetException; public interface AdminPasswordService { - void resetPassword(String secureKey, PasswordDto passwordDto); + void resetPassword(String secureKey, PasswordDto passwordDto) throws AdminPasswordResetException; } diff --git a/src/main/java/pl/vtt/wpi/core/application/service/DebugService.java b/src/main/java/pl/vtt/wpi/core/application/service/DebugService.java index 2e46182..9081857 100644 --- a/src/main/java/pl/vtt/wpi/core/application/service/DebugService.java +++ b/src/main/java/pl/vtt/wpi/core/application/service/DebugService.java @@ -1,11 +1,12 @@ package pl.vtt.wpi.core.application.service; import pl.vtt.wpi.core.domain.model.device.CurrentState; +import pl.vtt.wpi.core.application.exception.DebugDataAccessException; import java.util.List; public interface DebugService { - List pollLogs(); - List peekLogs(); - CurrentState getCurrentState(); + List pollLogs() throws DebugDataAccessException; + List peekLogs() throws DebugDataAccessException; + CurrentState getCurrentState() throws DebugDataAccessException; } diff --git a/src/main/java/pl/vtt/wpi/core/application/service/DeviceInfoService.java b/src/main/java/pl/vtt/wpi/core/application/service/DeviceInfoService.java index ec6e918..ee82319 100644 --- a/src/main/java/pl/vtt/wpi/core/application/service/DeviceInfoService.java +++ b/src/main/java/pl/vtt/wpi/core/application/service/DeviceInfoService.java @@ -1,7 +1,8 @@ package pl.vtt.wpi.core.application.service; import pl.vtt.wpi.core.domain.model.device.DeviceInfo; +import pl.vtt.wpi.core.application.exception.DeviceInfoReadException; public interface DeviceInfoService { - DeviceInfo read(); + DeviceInfo read() throws DeviceInfoReadException; } diff --git a/src/main/java/pl/vtt/wpi/core/application/service/LoginService.java b/src/main/java/pl/vtt/wpi/core/application/service/LoginService.java index 4cc9f06..1729e97 100644 --- a/src/main/java/pl/vtt/wpi/core/application/service/LoginService.java +++ b/src/main/java/pl/vtt/wpi/core/application/service/LoginService.java @@ -2,11 +2,12 @@ import pl.vtt.wpi.core.application.config.AuthorizationHolder; import pl.vtt.wpi.core.application.exception.IncorrectUsernameOrPasswordException; +import pl.vtt.wpi.core.application.exception.AuthenticationServiceUnavailableException; import pl.vtt.wpi.core.domain.model.Credentials; public interface LoginService { Credentials login(String username, String password) - throws IncorrectUsernameOrPasswordException; + throws IncorrectUsernameOrPasswordException, AuthenticationServiceUnavailableException; default void logout() { AuthorizationHolder.clear(); diff --git a/src/main/java/pl/vtt/wpi/core/application/service/NetworkConfigurationService.java b/src/main/java/pl/vtt/wpi/core/application/service/NetworkConfigurationService.java index 36ff65c..e2a71d3 100644 --- a/src/main/java/pl/vtt/wpi/core/application/service/NetworkConfigurationService.java +++ b/src/main/java/pl/vtt/wpi/core/application/service/NetworkConfigurationService.java @@ -1,9 +1,10 @@ package pl.vtt.wpi.core.application.service; +import pl.vtt.wpi.core.application.exception.NetworkConfigurationException; import pl.vtt.wpi.core.domain.model.device.AccessPointConfig; import pl.vtt.wpi.core.domain.model.device.WifiConfig; public interface NetworkConfigurationService { - void config(WifiConfig config); - void config(AccessPointConfig config); + void config(WifiConfig config) throws NetworkConfigurationException; + void config(AccessPointConfig config) throws NetworkConfigurationException; } diff --git a/src/main/java/pl/vtt/wpi/core/application/service/PixelProgramService.java b/src/main/java/pl/vtt/wpi/core/application/service/PixelProgramService.java index b63bb88..6f72b7e 100644 --- a/src/main/java/pl/vtt/wpi/core/application/service/PixelProgramService.java +++ b/src/main/java/pl/vtt/wpi/core/application/service/PixelProgramService.java @@ -2,23 +2,24 @@ import pl.vtt.wpi.core.application.exception.DataInconsistencyException; import pl.vtt.wpi.core.application.exception.PixelProgramNotFoundException; +import pl.vtt.wpi.core.application.exception.PixelProgramOperationException; import pl.vtt.wpi.core.domain.model.color.RgbColor; import pl.vtt.wpi.core.domain.model.device.PixelProgram; import java.util.List; public interface PixelProgramService { - List getAll(); - void insert(List pixelPrograms); + List getAll() throws PixelProgramOperationException; + void insert(List pixelPrograms) throws PixelProgramOperationException; void set(List pixelPrograms) throws DataInconsistencyException; PixelProgram get(int index) - throws PixelProgramNotFoundException; + throws PixelProgramNotFoundException, PixelProgramOperationException; PixelProgram singleton(List pixelProgram); - PixelProgram save(List pixelProgram); + PixelProgram save(List pixelProgram) throws PixelProgramOperationException; PixelProgram update(int index, List pixelProgram) - throws PixelProgramNotFoundException; + throws PixelProgramNotFoundException, PixelProgramOperationException; PixelProgram remove(int index) - throws PixelProgramNotFoundException, DataInconsistencyException; + throws PixelProgramNotFoundException, DataInconsistencyException, PixelProgramOperationException; } diff --git a/src/main/java/pl/vtt/wpi/core/application/service/RebootService.java b/src/main/java/pl/vtt/wpi/core/application/service/RebootService.java index e69255e..34449ff 100644 --- a/src/main/java/pl/vtt/wpi/core/application/service/RebootService.java +++ b/src/main/java/pl/vtt/wpi/core/application/service/RebootService.java @@ -1,5 +1,7 @@ package pl.vtt.wpi.core.application.service; +import pl.vtt.wpi.core.application.exception.DeviceRebootException; + public interface RebootService { - void reboot(); + void reboot() throws DeviceRebootException; } diff --git a/src/main/java/pl/vtt/wpi/core/application/service/RuntimeDataService.java b/src/main/java/pl/vtt/wpi/core/application/service/RuntimeDataService.java index c61f0c9..0b1f20b 100644 --- a/src/main/java/pl/vtt/wpi/core/application/service/RuntimeDataService.java +++ b/src/main/java/pl/vtt/wpi/core/application/service/RuntimeDataService.java @@ -1,10 +1,10 @@ package pl.vtt.wpi.core.application.service; import pl.vtt.wpi.core.application.exception.DataInconsistencyException; +import pl.vtt.wpi.core.application.exception.RuntimeDataOperationException; import pl.vtt.wpi.core.domain.model.device.RuntimeData; public interface RuntimeDataService { - RuntimeData read(); - void set(RuntimeData runtimeData) throws DataInconsistencyException; - void update(RuntimeData runtimeData) throws DataInconsistencyException; + RuntimeData read() throws RuntimeDataOperationException; + void set(RuntimeData runtimeData) throws DataInconsistencyException, RuntimeDataOperationException; } diff --git a/src/main/java/pl/vtt/wpi/core/application/service/UserManagementService.java b/src/main/java/pl/vtt/wpi/core/application/service/UserManagementService.java index 29391c8..10c3d6e 100644 --- a/src/main/java/pl/vtt/wpi/core/application/service/UserManagementService.java +++ b/src/main/java/pl/vtt/wpi/core/application/service/UserManagementService.java @@ -3,16 +3,17 @@ import pl.vtt.wpi.core.application.exception.InvalidPasswordException; import pl.vtt.wpi.core.application.exception.UserAlreadyExistsException; import pl.vtt.wpi.core.application.exception.UserNotExistsException; +import pl.vtt.wpi.core.application.exception.UserManagementOperationException; import pl.vtt.wpi.core.domain.dto.PasswordDto; import pl.vtt.wpi.core.domain.model.User; public interface UserManagementService { void createUser(User user, PasswordDto passwordDto) - throws UserAlreadyExistsException, InvalidPasswordException; + throws UserAlreadyExistsException, InvalidPasswordException, UserManagementOperationException; void changePassword(PasswordDto passwordDto) - throws InvalidPasswordException; + throws InvalidPasswordException, UserManagementOperationException; void removeUser(User user) - throws UserNotExistsException; + throws UserNotExistsException, UserManagementOperationException; } diff --git a/src/main/java/pl/vtt/wpi/core/application/service/impl/AdminPasswordServiceImpl.java b/src/main/java/pl/vtt/wpi/core/application/service/impl/AdminPasswordServiceImpl.java new file mode 100644 index 0000000..2c742c3 --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/service/impl/AdminPasswordServiceImpl.java @@ -0,0 +1,40 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.Objects; +import pl.vtt.wpi.core.application.exception.AdminPasswordResetException; +import pl.vtt.wpi.core.application.service.AdminPasswordService; +import pl.vtt.wpi.core.domain.dto.AdminPasswordResetRequest; +import pl.vtt.wpi.core.domain.dto.PasswordDto; +import pl.vtt.wpi.core.domain.port.InputPort; +import pl.vtt.wpi.core.domain.port.exception.InputPortException; + +public class AdminPasswordServiceImpl implements AdminPasswordService { + private final InputPort adminPasswordResetInputPort; + + public AdminPasswordServiceImpl(InputPort adminPasswordResetInputPort) { + this.adminPasswordResetInputPort = Objects.requireNonNull(adminPasswordResetInputPort, + "adminPasswordResetInputPort cannot be null"); + } + + @Override + public void resetPassword(String secureKey, PasswordDto passwordDto) throws AdminPasswordResetException { + if (secureKey == null || secureKey.isBlank()) { + throw new AdminPasswordResetException("Secure key cannot be null or blank"); + } + if (passwordDto == null || passwordDto.password() == null || passwordDto.passwordConfirmation() == null) { + throw new AdminPasswordResetException("Password data cannot be null"); + } + if (passwordDto.password().isBlank() || passwordDto.passwordConfirmation().isBlank()) { + throw new AdminPasswordResetException("Password and password confirmation cannot be blank"); + } + if (!passwordDto.password().equals(passwordDto.passwordConfirmation())) { + throw new AdminPasswordResetException("Password and password confirmation do not match"); + } + try { + adminPasswordResetInputPort.send(new AdminPasswordResetRequest(secureKey, passwordDto)); + } catch (InputPortException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new AdminPasswordResetException("Cannot reset admin password", cause); + } + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/service/impl/DebugServiceImpl.java b/src/main/java/pl/vtt/wpi/core/application/service/impl/DebugServiceImpl.java new file mode 100644 index 0000000..4b1aed1 --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/service/impl/DebugServiceImpl.java @@ -0,0 +1,59 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.List; +import java.util.Objects; +import pl.vtt.wpi.core.application.exception.DebugDataAccessException; +import pl.vtt.wpi.core.application.service.DebugService; +import pl.vtt.wpi.core.domain.model.device.CurrentState; +import pl.vtt.wpi.core.domain.port.InputPort; +import pl.vtt.wpi.core.domain.port.OutputPort; +import pl.vtt.wpi.core.domain.port.exception.InputPortException; +import pl.vtt.wpi.core.domain.port.exception.OutputPortException; + +public class DebugServiceImpl implements DebugService { + private final OutputPort> logsOutputPort; + private final InputPort logsDeleteInputPort; + private final OutputPort currentStateOutputPort; + + public DebugServiceImpl(OutputPort> logsOutputPort, + InputPort logsDeleteInputPort, + OutputPort currentStateOutputPort) { + this.logsOutputPort = Objects.requireNonNull(logsOutputPort, "logsOutputPort cannot be null"); + this.logsDeleteInputPort = Objects.requireNonNull(logsDeleteInputPort, + "logsDeleteInputPort cannot be null"); + this.currentStateOutputPort = Objects.requireNonNull(currentStateOutputPort, + "currentStateOutputPort cannot be null"); + } + + @Override + public List pollLogs() throws DebugDataAccessException { + List logs = peekLogs(); + try { + logsDeleteInputPort.send(null); + } catch (InputPortException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new DebugDataAccessException("Cannot clear logs", cause); + } + return logs; + } + + @Override + public List peekLogs() throws DebugDataAccessException { + try { + return logsOutputPort.load(); + } catch (OutputPortException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new DebugDataAccessException("Cannot read logs", cause); + } + } + + @Override + public CurrentState getCurrentState() throws DebugDataAccessException { + try { + return currentStateOutputPort.load(); + } catch (OutputPortException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new DebugDataAccessException("Cannot read current state", cause); + } + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/service/impl/DeviceInfoServiceImpl.java b/src/main/java/pl/vtt/wpi/core/application/service/impl/DeviceInfoServiceImpl.java new file mode 100644 index 0000000..ae1d23b --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/service/impl/DeviceInfoServiceImpl.java @@ -0,0 +1,27 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.Objects; +import pl.vtt.wpi.core.application.exception.DeviceInfoReadException; +import pl.vtt.wpi.core.application.service.DeviceInfoService; +import pl.vtt.wpi.core.domain.model.device.DeviceInfo; +import pl.vtt.wpi.core.domain.port.OutputPort; +import pl.vtt.wpi.core.domain.port.exception.OutputPortException; + +public class DeviceInfoServiceImpl implements DeviceInfoService { + private final OutputPort deviceInfoOutputPort; + + public DeviceInfoServiceImpl(OutputPort deviceInfoOutputPort) { + this.deviceInfoOutputPort = Objects.requireNonNull(deviceInfoOutputPort, + "deviceInfoOutputPort cannot be null"); + } + + @Override + public DeviceInfo read() throws DeviceInfoReadException { + try { + return deviceInfoOutputPort.load(); + } catch (OutputPortException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new DeviceInfoReadException("Cannot read device info", cause); + } + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImpl.java b/src/main/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImpl.java index f4b0912..240ec12 100644 --- a/src/main/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImpl.java +++ b/src/main/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImpl.java @@ -2,6 +2,7 @@ import pl.vtt.wpi.core.application.config.AuthorizationHolder; import pl.vtt.wpi.core.application.exception.IncorrectUsernameOrPasswordException; +import pl.vtt.wpi.core.application.exception.AuthenticationServiceUnavailableException; import pl.vtt.wpi.core.application.service.LoginService; import pl.vtt.wpi.core.domain.port.OutputPort; import pl.vtt.wpi.core.domain.port.exception.OutputPortException; @@ -16,7 +17,7 @@ public LoginServiceImpl(OutputPort authPort) { @Override public Credentials login(String username, String password) - throws IncorrectUsernameOrPasswordException { + throws IncorrectUsernameOrPasswordException, AuthenticationServiceUnavailableException { if (username == null || username.isBlank() || password == null || password.isBlank()) { throw new IncorrectUsernameOrPasswordException(); } @@ -25,14 +26,16 @@ public Credentials login(String username, String password) Credentials credentials = getCredentials(); AuthorizationHolder.authorize(credentials); return credentials; - } catch (IncorrectUsernameOrPasswordException | RuntimeException e) { + } catch (IncorrectUsernameOrPasswordException + | AuthenticationServiceUnavailableException + | RuntimeException e) { AuthorizationHolder.clear(); throw e; } } private Credentials getCredentials() - throws IncorrectUsernameOrPasswordException { + throws IncorrectUsernameOrPasswordException, AuthenticationServiceUnavailableException { final Credentials responseBody; try { responseBody = authPort.load(); @@ -41,7 +44,7 @@ private Credentials getCredentials() throw incorrect; } Throwable cause = e.getCause() == null ? e : e.getCause(); - throw new RuntimeException("Login failed", cause); + throw new AuthenticationServiceUnavailableException("Login failed", cause); } if (responseBody == null) { throw new IncorrectUsernameOrPasswordException(); diff --git a/src/main/java/pl/vtt/wpi/core/application/service/impl/NetworkConfigurationServiceImpl.java b/src/main/java/pl/vtt/wpi/core/application/service/impl/NetworkConfigurationServiceImpl.java new file mode 100644 index 0000000..b8317bb --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/service/impl/NetworkConfigurationServiceImpl.java @@ -0,0 +1,43 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.Objects; +import pl.vtt.wpi.core.application.exception.NetworkConfigurationException; +import pl.vtt.wpi.core.application.service.NetworkConfigurationService; +import pl.vtt.wpi.core.domain.model.device.AccessPointConfig; +import pl.vtt.wpi.core.domain.model.device.WifiConfig; +import pl.vtt.wpi.core.domain.port.InputPort; +import pl.vtt.wpi.core.domain.port.exception.InputPortException; + +public class NetworkConfigurationServiceImpl implements NetworkConfigurationService { + private final InputPort wifiConfigInputPort; + + public NetworkConfigurationServiceImpl(InputPort wifiConfigInputPort) { + this.wifiConfigInputPort = Objects.requireNonNull(wifiConfigInputPort, + "wifiConfigInputPort cannot be null"); + } + + @Override + public void config(WifiConfig config) throws NetworkConfigurationException { + send(config); + } + + @Override + public void config(AccessPointConfig config) throws NetworkConfigurationException { + if (config == null) { + throw new NetworkConfigurationException("Access point config cannot be null"); + } + send(new WifiConfig(config.ssid(), config.password())); + } + + private void send(WifiConfig config) throws NetworkConfigurationException { + if (config == null) { + throw new NetworkConfigurationException("WiFi config cannot be null"); + } + try { + wifiConfigInputPort.send(config); + } catch (InputPortException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new NetworkConfigurationException("Cannot update network configuration", cause); + } + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/service/impl/PixelProgramServiceImpl.java b/src/main/java/pl/vtt/wpi/core/application/service/impl/PixelProgramServiceImpl.java new file mode 100644 index 0000000..1558dba --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/service/impl/PixelProgramServiceImpl.java @@ -0,0 +1,165 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import pl.vtt.wpi.core.application.exception.DataInconsistencyException; +import pl.vtt.wpi.core.application.exception.PixelProgramNotFoundException; +import pl.vtt.wpi.core.application.exception.PixelProgramOperationException; +import pl.vtt.wpi.core.application.service.PixelProgramService; +import pl.vtt.wpi.core.domain.model.color.RgbColor; +import pl.vtt.wpi.core.domain.model.device.PixelProgram; +import pl.vtt.wpi.core.domain.port.InputPort; +import pl.vtt.wpi.core.domain.port.OutputPort; +import pl.vtt.wpi.core.domain.port.exception.InputPortException; +import pl.vtt.wpi.core.domain.port.exception.OutputPortException; + +public class PixelProgramServiceImpl implements PixelProgramService { + private final OutputPort> pixelProgramsOutputPort; + private final InputPort> pixelProgramsInputPort; + private final Lock mutationLock = new ReentrantLock(); + + public PixelProgramServiceImpl(OutputPort> pixelProgramsOutputPort, + InputPort> pixelProgramsInputPort) { + this.pixelProgramsOutputPort = Objects.requireNonNull(pixelProgramsOutputPort, + "pixelProgramsOutputPort cannot be null"); + this.pixelProgramsInputPort = Objects.requireNonNull(pixelProgramsInputPort, + "pixelProgramsInputPort cannot be null"); + } + + @Override + public List getAll() throws PixelProgramOperationException { + return new ArrayList<>(loadPrograms()); + } + + @Override + public void insert(List pixelPrograms) throws PixelProgramOperationException { + if (pixelPrograms == null || pixelPrograms.isEmpty()) { + return; + } + mutationLock.lock(); + try { + List current = getAll(); + current.addAll(pixelPrograms); + set(current); + } catch (DataInconsistencyException e) { + throw new PixelProgramOperationException("Cannot insert pixel programs", e); + } finally { + mutationLock.unlock(); + } + } + + @Override + public void set(List pixelPrograms) throws DataInconsistencyException { + mutationLock.lock(); + try { + if (pixelPrograms == null) { + throw new DataInconsistencyException("Pixel programs cannot be null"); + } + if (pixelPrograms.stream().anyMatch(Objects::isNull)) { + throw new DataInconsistencyException("Pixel programs cannot contain null elements"); + } + try { + pixelProgramsInputPort.send(List.copyOf(pixelPrograms)); + } catch (InputPortException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new DataInconsistencyException("Cannot update pixel programs", cause); + } + } finally { + mutationLock.unlock(); + } + } + + @Override + public PixelProgram get(int index) throws PixelProgramNotFoundException, PixelProgramOperationException { + List programs = loadPrograms(); + return programs.get(findListIndex(programs, index)); + } + + @Override + public PixelProgram singleton(List pixelProgram) { + return new PixelProgram(0, pixelProgram == null ? List.of() : List.copyOf(pixelProgram)); + } + + @Override + public PixelProgram save(List pixelProgram) throws PixelProgramOperationException { + mutationLock.lock(); + try { + List programs = new ArrayList<>(getAll()); + int nextIndex = programs.stream() + .filter(Objects::nonNull) + .map(PixelProgram::index) + .max(Comparator.naturalOrder()) + .map(i -> i + 1) + .orElse(0); + PixelProgram saved = new PixelProgram(nextIndex, + pixelProgram == null ? List.of() : List.copyOf(pixelProgram)); + programs.add(saved); + set(programs); + return saved; + } catch (DataInconsistencyException e) { + throw new PixelProgramOperationException("Cannot save pixel program", e); + } finally { + mutationLock.unlock(); + } + } + + @Override + public PixelProgram update(int index, List pixelProgram) + throws PixelProgramNotFoundException, PixelProgramOperationException { + mutationLock.lock(); + try { + List programs = getAll(); + int replaceIndex = findListIndex(programs, index); + PixelProgram updated = new PixelProgram(index, + pixelProgram == null ? List.of() : List.copyOf(pixelProgram)); + programs.set(replaceIndex, updated); + set(programs); + return updated; + } catch (DataInconsistencyException e) { + throw new PixelProgramOperationException("Cannot update pixel program", e); + } finally { + mutationLock.unlock(); + } + } + + @Override + public PixelProgram remove(int index) + throws PixelProgramNotFoundException, DataInconsistencyException, PixelProgramOperationException { + mutationLock.lock(); + try { + List programs = getAll(); + int removeIndex = findListIndex(programs, index); + PixelProgram removed = programs.remove(removeIndex); + set(programs); + return removed; + } catch (PixelProgramOperationException e) { + throw e; + } finally { + mutationLock.unlock(); + } + } + + private List loadPrograms() throws PixelProgramOperationException { + try { + List pixelPrograms = pixelProgramsOutputPort.load(); + return pixelPrograms == null ? List.of() : pixelPrograms; + } catch (OutputPortException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new PixelProgramOperationException("Cannot read pixel programs", cause); + } + } + + private int findListIndex(List programs, int index) throws PixelProgramNotFoundException { + for (int i = 0; i < programs.size(); i++) { + PixelProgram program = programs.get(i); + if (program != null && program.index() == index) { + return i; + } + } + throw new PixelProgramNotFoundException("Pixel program %d not found".formatted(index)); + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/service/impl/RebootServiceImpl.java b/src/main/java/pl/vtt/wpi/core/application/service/impl/RebootServiceImpl.java new file mode 100644 index 0000000..f2ded5e --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/service/impl/RebootServiceImpl.java @@ -0,0 +1,25 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.Objects; +import pl.vtt.wpi.core.application.exception.DeviceRebootException; +import pl.vtt.wpi.core.application.service.RebootService; +import pl.vtt.wpi.core.domain.port.InputPort; +import pl.vtt.wpi.core.domain.port.exception.InputPortException; + +public class RebootServiceImpl implements RebootService { + private final InputPort rebootInputPort; + + public RebootServiceImpl(InputPort rebootInputPort) { + this.rebootInputPort = Objects.requireNonNull(rebootInputPort, + "rebootInputPort cannot be null"); + } + + @Override + public void reboot() throws DeviceRebootException { + try { + rebootInputPort.send(null); + } catch (InputPortException e) { + throw new DeviceRebootException("Cannot reboot device", e); + } + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/service/impl/RuntimeDataServiceImpl.java b/src/main/java/pl/vtt/wpi/core/application/service/impl/RuntimeDataServiceImpl.java new file mode 100644 index 0000000..b969aba --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/service/impl/RuntimeDataServiceImpl.java @@ -0,0 +1,82 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import pl.vtt.wpi.core.application.exception.DataInconsistencyException; +import pl.vtt.wpi.core.application.exception.RuntimeDataOperationException; +import pl.vtt.wpi.core.application.service.RuntimeDataService; +import pl.vtt.wpi.core.domain.model.device.PixelProgram; +import pl.vtt.wpi.core.domain.model.device.RuntimeData; +import pl.vtt.wpi.core.domain.port.InputPort; +import pl.vtt.wpi.core.domain.port.OutputPort; +import pl.vtt.wpi.core.domain.port.exception.InputPortException; +import pl.vtt.wpi.core.domain.port.exception.OutputPortException; + +public class RuntimeDataServiceImpl implements RuntimeDataService { + private final OutputPort runtimeDataOutputPort; + private final OutputPort> pixelProgramsOutputPort; + private final InputPort runtimeDataInputPort; + + public RuntimeDataServiceImpl(OutputPort runtimeDataOutputPort, + OutputPort> pixelProgramsOutputPort, + InputPort runtimeDataInputPort) { + this.runtimeDataOutputPort = Objects.requireNonNull(runtimeDataOutputPort, + "runtimeDataOutputPort cannot be null"); + this.pixelProgramsOutputPort = Objects.requireNonNull(pixelProgramsOutputPort, + "pixelProgramsOutputPort cannot be null"); + this.runtimeDataInputPort = Objects.requireNonNull(runtimeDataInputPort, + "runtimeDataInputPort cannot be null"); + } + + @Override + public RuntimeData read() throws RuntimeDataOperationException { + try { + return runtimeDataOutputPort.load(); + } catch (OutputPortException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new RuntimeDataOperationException("Cannot read runtime data", cause); + } + } + + @Override + public void set(RuntimeData runtimeData) throws DataInconsistencyException, RuntimeDataOperationException { + if (runtimeData == null) { + throw new RuntimeDataOperationException("Runtime data cannot be null"); + } + validatePixelProgramExists(runtimeData); + send(runtimeData); + } + + private void send(RuntimeData runtimeData) throws RuntimeDataOperationException { + try { + runtimeDataInputPort.send(runtimeData); + } catch (InputPortException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new RuntimeDataOperationException("Cannot set runtime data", cause); + } + } + + private void validatePixelProgramExists(RuntimeData runtimeData) + throws DataInconsistencyException, RuntimeDataOperationException { + if (runtimeData.pixelProgram() == null) { + return; + } + List pixelPrograms; + try { + pixelPrograms = pixelProgramsOutputPort.load(); + } catch (OutputPortException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new RuntimeDataOperationException("Cannot validate runtime data", cause); + } + + boolean exists = pixelPrograms != null + && pixelPrograms.stream().anyMatch(program -> + program != null && Objects.equals(program.index(), runtimeData.pixelProgram())); + + if (!exists) { + String message = "Pixel program %d does not exist".formatted(runtimeData.pixelProgram()); + throw new DataInconsistencyException(message, new NoSuchElementException(message)); + } + } +} diff --git a/src/main/java/pl/vtt/wpi/core/application/service/impl/UserManagementServiceImpl.java b/src/main/java/pl/vtt/wpi/core/application/service/impl/UserManagementServiceImpl.java new file mode 100644 index 0000000..5ffc1de --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/application/service/impl/UserManagementServiceImpl.java @@ -0,0 +1,122 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import pl.vtt.wpi.core.application.exception.InvalidPasswordException; +import pl.vtt.wpi.core.application.exception.UserAlreadyExistsException; +import pl.vtt.wpi.core.application.exception.UserManagementOperationException; +import pl.vtt.wpi.core.application.exception.UserNotExistsException; +import pl.vtt.wpi.core.application.service.UserManagementService; +import pl.vtt.wpi.core.domain.dto.PasswordDto; +import pl.vtt.wpi.core.domain.dto.UserCreateRequest; +import pl.vtt.wpi.core.domain.model.User; +import pl.vtt.wpi.core.domain.port.InputPort; +import pl.vtt.wpi.core.domain.port.OutputPort; +import pl.vtt.wpi.core.domain.port.exception.InputPortException; +import pl.vtt.wpi.core.domain.port.exception.OutputPortException; + +public class UserManagementServiceImpl implements UserManagementService { + private final OutputPort> usersOutputPort; + private final InputPort userCreateRequestInputPort; + private final InputPort changePasswordInputPort; + private final InputPort removeUserInputPort; + private final Lock userLock = new ReentrantLock(); + + public UserManagementServiceImpl(OutputPort> usersOutputPort, + InputPort userCreateRequestInputPort, + InputPort changePasswordInputPort, + InputPort removeUserInputPort) { + this.usersOutputPort = Objects.requireNonNull(usersOutputPort, "usersOutputPort cannot be null"); + this.userCreateRequestInputPort = Objects.requireNonNull(userCreateRequestInputPort, + "userCreateRequestInputPort cannot be null"); + this.changePasswordInputPort = Objects.requireNonNull(changePasswordInputPort, + "changePasswordInputPort cannot be null"); + this.removeUserInputPort = Objects.requireNonNull(removeUserInputPort, + "removeUserInputPort cannot be null"); + } + + @Override + public void createUser(User user, PasswordDto passwordDto) + throws UserAlreadyExistsException, InvalidPasswordException, UserManagementOperationException { + if (user == null) { + throw new UserManagementOperationException("User cannot be null"); + } + validatePassword(passwordDto); + userLock.lock(); + try { + boolean exists = loadUsers().stream().anyMatch(existingUser -> existingUser.username().equals(user.username())); + if (exists) { + throw new UserAlreadyExistsException(); + } + userCreateRequestInputPort.send(new UserCreateRequest(user, passwordDto)); + } catch (InputPortException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new UserManagementOperationException("Cannot create user", cause); + } finally { + userLock.unlock(); + } + } + + @Override + public void changePassword(PasswordDto passwordDto) + throws InvalidPasswordException, UserManagementOperationException { + validatePassword(passwordDto); + try { + changePasswordInputPort.send(passwordDto); + } catch (InputPortException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new UserManagementOperationException("Cannot change password", cause); + } + } + + @Override + public void removeUser(User user) + throws UserNotExistsException, UserManagementOperationException { + if (user == null) { + throw new UserManagementOperationException("User cannot be null"); + } + userLock.lock(); + try { + boolean exists = loadUsers().stream().anyMatch(existingUser -> existingUser.username().equals(user.username())); + if (!exists) { + throw new UserNotExistsException(); + } + removeUserInputPort.send(user); + } catch (InputPortException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new UserManagementOperationException("Cannot remove user", cause); + } finally { + userLock.unlock(); + } + } + + private List loadUsers() throws UserManagementOperationException { + try { + List users = usersOutputPort.load(); + if (users == null) { + return List.of(); + } + if (users.stream().anyMatch(Objects::isNull)) { + throw new UserManagementOperationException("Users cannot contain null elements"); + } + return users; + } catch (OutputPortException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new UserManagementOperationException("Cannot load users", cause); + } + } + + private void validatePassword(PasswordDto passwordDto) throws InvalidPasswordException { + if (passwordDto == null || passwordDto.password() == null || passwordDto.passwordConfirmation() == null) { + throw new InvalidPasswordException("Password data is required"); + } + if (passwordDto.password().isBlank()) { + throw new InvalidPasswordException("Password cannot be blank"); + } + if (!passwordDto.password().equals(passwordDto.passwordConfirmation())) { + throw new InvalidPasswordException("Password and password confirmation must be identical"); + } + } +} diff --git a/src/main/java/pl/vtt/wpi/core/domain/dto/AdminPasswordResetRequest.java b/src/main/java/pl/vtt/wpi/core/domain/dto/AdminPasswordResetRequest.java new file mode 100644 index 0000000..a4aef71 --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/domain/dto/AdminPasswordResetRequest.java @@ -0,0 +1,4 @@ +package pl.vtt.wpi.core.domain.dto; + +public record AdminPasswordResetRequest(String secureKey, PasswordDto passwordDto) { +} diff --git a/src/main/java/pl/vtt/wpi/core/domain/dto/UserCreateRequest.java b/src/main/java/pl/vtt/wpi/core/domain/dto/UserCreateRequest.java new file mode 100644 index 0000000..a2830da --- /dev/null +++ b/src/main/java/pl/vtt/wpi/core/domain/dto/UserCreateRequest.java @@ -0,0 +1,6 @@ +package pl.vtt.wpi.core.domain.dto; + +import pl.vtt.wpi.core.domain.model.User; + +public record UserCreateRequest(User user, PasswordDto passwordDto) { +} diff --git a/src/main/java/pl/vtt/wpi/core/domain/port/input/UserCreateInputPort.java b/src/main/java/pl/vtt/wpi/core/domain/port/input/UserCreateInputPort.java index 7f88db5..e0c185e 100644 --- a/src/main/java/pl/vtt/wpi/core/domain/port/input/UserCreateInputPort.java +++ b/src/main/java/pl/vtt/wpi/core/domain/port/input/UserCreateInputPort.java @@ -2,32 +2,35 @@ import pl.vtt.wpi.core.infrastructure.RequestFactory; import pl.vtt.wpi.core.infrastructure.RequestSender; +import pl.vtt.wpi.core.domain.dto.UserCreateRequest; import pl.vtt.wpi.core.domain.port.exception.InputPortException; import pl.vtt.wpi.core.infrastructure.Request; -import pl.vtt.wpi.core.domain.model.User; import pl.vtt.wpi.core.domain.model.endpoint.Method; import pl.vtt.wpi.core.domain.model.endpoint.RequestTarget; import pl.vtt.wpi.core.domain.port.InputPort; -public class UserCreateInputPort implements InputPort { - private final RequestFactory requestFactory; +public class UserCreateInputPort implements InputPort { + private final RequestFactory requestFactory; private final RequestSender requestSender; - public UserCreateInputPort(RequestFactory requestFactory, + public UserCreateInputPort(RequestFactory requestFactory, RequestSender requestSender) { this.requestFactory = requestFactory; this.requestSender = requestSender; } @Override - public void send(User obj) throws InputPortException { - if (obj == null) { - throw new InputPortException("User cannot be null"); + public void send(UserCreateRequest obj) throws InputPortException { + if (obj == null || obj.user() == null || obj.passwordDto() == null) { + throw new InputPortException("User and password data cannot be null"); } try { - Request request = requestFactory.create(obj, Method.POST, RequestTarget.USERS_CREATE); + Request request = requestFactory.create(obj, Method.POST, RequestTarget.USERS_CREATE); requestSender.send(request); } catch (Exception e) { + if (e instanceof InputPortException inputPortException) { + throw inputPortException; + } throw new InputPortException("Cannot create user", e); } } diff --git a/src/test/java/pl/vtt/wpi/core/application/service/impl/AdminPasswordServiceImplTest.java b/src/test/java/pl/vtt/wpi/core/application/service/impl/AdminPasswordServiceImplTest.java new file mode 100644 index 0000000..d35dd8e --- /dev/null +++ b/src/test/java/pl/vtt/wpi/core/application/service/impl/AdminPasswordServiceImplTest.java @@ -0,0 +1,63 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import pl.vtt.wpi.core.application.exception.AdminPasswordResetException; +import pl.vtt.wpi.core.domain.dto.AdminPasswordResetRequest; +import pl.vtt.wpi.core.domain.dto.PasswordDto; +import pl.vtt.wpi.core.domain.port.InputPort; +import pl.vtt.wpi.core.domain.port.exception.InputPortException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class AdminPasswordServiceImplTest { + + @Test + void resetPassword_validData_sendsRequest() throws Exception { + AtomicReference sent = new AtomicReference<>(); + InputPort inputPort = sent::set; + AdminPasswordServiceImpl service = new AdminPasswordServiceImpl(inputPort); + + PasswordDto passwordDto = new PasswordDto("secret", "secret"); + service.resetPassword("secure-key", passwordDto); + + assertEquals("secure-key", sent.get().secureKey()); + assertEquals(passwordDto, sent.get().passwordDto()); + } + + @Test + void resetPassword_blankPassword_throwsAdminPasswordResetException() { + AdminPasswordServiceImpl service = new AdminPasswordServiceImpl(_ -> {}); + + assertThrows(AdminPasswordResetException.class, + () -> service.resetPassword("secure-key", new PasswordDto(" ", " "))); + } + + @Test + void resetPassword_inputPortExceptionWithoutCause_isTranslated() { + AdminPasswordServiceImpl service = new AdminPasswordServiceImpl( + _ -> { throw new InputPortException("port failure"); } + ); + + AdminPasswordResetException exception = assertThrows(AdminPasswordResetException.class, + () -> service.resetPassword("secure-key", new PasswordDto("secret", "secret"))); + + assertEquals("Cannot reset admin password", exception.getMessage()); + assertEquals("port failure", exception.getCause().getMessage()); + } + + @Test + void resetPassword_inputPortExceptionWithCause_isTranslated() { + RuntimeException cause = new RuntimeException("cause"); + AdminPasswordServiceImpl service = new AdminPasswordServiceImpl( + _ -> { throw new InputPortException("port failure", cause); } + ); + + AdminPasswordResetException exception = assertThrows(AdminPasswordResetException.class, + () -> service.resetPassword("secure-key", new PasswordDto("secret", "secret"))); + + assertEquals("Cannot reset admin password", exception.getMessage()); + assertEquals("cause", exception.getCause().getMessage()); + } +} diff --git a/src/test/java/pl/vtt/wpi/core/application/service/impl/DebugServiceImplTest.java b/src/test/java/pl/vtt/wpi/core/application/service/impl/DebugServiceImplTest.java new file mode 100644 index 0000000..ee57478 --- /dev/null +++ b/src/test/java/pl/vtt/wpi/core/application/service/impl/DebugServiceImplTest.java @@ -0,0 +1,44 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; +import pl.vtt.wpi.core.application.exception.DebugDataAccessException; +import pl.vtt.wpi.core.domain.model.device.CurrentState; +import pl.vtt.wpi.core.domain.port.InputPort; +import pl.vtt.wpi.core.domain.port.OutputPort; +import pl.vtt.wpi.core.domain.port.exception.OutputPortException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DebugServiceImplTest { + + @Test + void pollLogs_readsAndClearsLogs() throws Exception { + OutputPort> logsOutputPort = () -> List.of("a", "b"); + AtomicBoolean cleared = new AtomicBoolean(false); + InputPort deleteInputPort = _ -> cleared.set(true); + + DebugServiceImpl service = new DebugServiceImpl(logsOutputPort, deleteInputPort, + () -> new CurrentState.Builder().build()); + + List logs = service.pollLogs(); + + assertEquals(List.of("a", "b"), logs); + assertTrue(cleared.get()); + } + + @Test + void peekLogs_outputFailure_wrapsException() { + DebugServiceImpl service = new DebugServiceImpl( + () -> { throw new OutputPortException("x", new IllegalStateException("boom")); }, + _ -> {}, + () -> new CurrentState.Builder().build() + ); + + DebugDataAccessException exception = assertThrows(DebugDataAccessException.class, service::peekLogs); + assertEquals("Cannot read logs", exception.getMessage()); + } +} diff --git a/src/test/java/pl/vtt/wpi/core/application/service/impl/DeviceInfoServiceImplTest.java b/src/test/java/pl/vtt/wpi/core/application/service/impl/DeviceInfoServiceImplTest.java new file mode 100644 index 0000000..bff7581 --- /dev/null +++ b/src/test/java/pl/vtt/wpi/core/application/service/impl/DeviceInfoServiceImplTest.java @@ -0,0 +1,31 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.UUID; +import org.junit.jupiter.api.Test; +import pl.vtt.wpi.core.application.exception.DeviceInfoReadException; +import pl.vtt.wpi.core.domain.model.device.DeviceInfo; +import pl.vtt.wpi.core.domain.port.exception.OutputPortException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class DeviceInfoServiceImplTest { + + @Test + void read_success_returnsDeviceInfo() throws Exception { + DeviceInfo expected = new DeviceInfo(UUID.randomUUID(), "m", "p", "a", "1", "auth", "mail"); + DeviceInfoServiceImpl service = new DeviceInfoServiceImpl(() -> expected); + + assertEquals(expected, service.read()); + } + + @Test + void read_portFailure_wrapsException() { + DeviceInfoServiceImpl service = new DeviceInfoServiceImpl( + () -> { throw new OutputPortException("x", new IllegalArgumentException("boom")); } + ); + + DeviceInfoReadException exception = assertThrows(DeviceInfoReadException.class, service::read); + assertEquals("Cannot read device info", exception.getMessage()); + } +} diff --git a/src/test/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImplTest.java b/src/test/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImplTest.java index 1445836..3664031 100644 --- a/src/test/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImplTest.java +++ b/src/test/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImplTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test; import pl.vtt.wpi.core.application.config.AuthorizationHolder; import pl.vtt.wpi.core.application.exception.IncorrectUsernameOrPasswordException; +import pl.vtt.wpi.core.application.exception.AuthenticationServiceUnavailableException; import pl.vtt.wpi.core.domain.port.OutputPort; import pl.vtt.wpi.core.domain.port.exception.OutputPortException; import pl.vtt.wpi.core.domain.model.Credentials; @@ -37,7 +38,7 @@ void casual_login_ok() { .encodeToString((username + ":" + token).getBytes(StandardCharsets.UTF_8)); assertEquals(expectedType, AuthorizationHolder.get().type()); assertEquals(expectedCredentials, AuthorizationHolder.get().credentials()); - } catch (IncorrectUsernameOrPasswordException e) { + } catch (IncorrectUsernameOrPasswordException | AuthenticationServiceUnavailableException e) { fail(e); } } @@ -56,8 +57,8 @@ void wrong_username_or_password_login_exception() { } @Test - @DisplayName("Tests cleanup when auth port throws runtime exception") - void runtime_exception_clears_authorization() { + @DisplayName("Tests cleanup when OutputPortException causes AuthenticationServiceUnavailableException") + void output_port_exception_clears_authorization() { String username = "test"; String password = "test123"; OutputPort port = () -> { @@ -65,7 +66,8 @@ void runtime_exception_clears_authorization() { }; LoginServiceImpl instance = new LoginServiceImpl(port); - RuntimeException exception = assertThrows(RuntimeException.class, () -> instance.login(username, password)); + AuthenticationServiceUnavailableException exception = assertThrows(AuthenticationServiceUnavailableException.class, + () -> instance.login(username, password)); assertEquals("Login failed", exception.getMessage()); assertNotNull(exception.getCause()); assertEquals("connection lost", exception.getCause().getMessage()); diff --git a/src/test/java/pl/vtt/wpi/core/application/service/impl/NetworkConfigurationServiceImplTest.java b/src/test/java/pl/vtt/wpi/core/application/service/impl/NetworkConfigurationServiceImplTest.java new file mode 100644 index 0000000..3b8319f --- /dev/null +++ b/src/test/java/pl/vtt/wpi/core/application/service/impl/NetworkConfigurationServiceImplTest.java @@ -0,0 +1,21 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import pl.vtt.wpi.core.domain.model.device.AccessPointConfig; +import pl.vtt.wpi.core.domain.model.device.WifiConfig; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class NetworkConfigurationServiceImplTest { + + @Test + void config_accessPointConfig_isMappedToWifiConfig() throws Exception { + AtomicReference sent = new AtomicReference<>(); + NetworkConfigurationServiceImpl service = new NetworkConfigurationServiceImpl(sent::set); + + service.config(new AccessPointConfig("ap", "pass")); + + assertEquals(new WifiConfig("ap", "pass"), sent.get()); + } +} diff --git a/src/test/java/pl/vtt/wpi/core/application/service/impl/PixelProgramServiceImplTest.java b/src/test/java/pl/vtt/wpi/core/application/service/impl/PixelProgramServiceImplTest.java new file mode 100644 index 0000000..e09f8fb --- /dev/null +++ b/src/test/java/pl/vtt/wpi/core/application/service/impl/PixelProgramServiceImplTest.java @@ -0,0 +1,61 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import pl.vtt.wpi.core.application.exception.DataInconsistencyException; +import pl.vtt.wpi.core.application.exception.PixelProgramNotFoundException; +import pl.vtt.wpi.core.application.exception.PixelProgramOperationException; +import pl.vtt.wpi.core.domain.model.device.PixelProgram; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class PixelProgramServiceImplTest { + + @Test + void set_nullElement_throwsDataInconsistencyException() { + PixelProgramServiceImpl service = new PixelProgramServiceImpl(() -> List.of(), _ -> {}); + + DataInconsistencyException exception = assertThrows(DataInconsistencyException.class, + () -> service.set(new ArrayList<>(Collections.singletonList(null)))); + + assertEquals("Pixel programs cannot contain null elements", exception.getMessage()); + } + + @Test + void save_addsProgramWithNextIndexAndSendsAll() throws Exception { + AtomicReference> sent = new AtomicReference<>(); + PixelProgramServiceImpl service = new PixelProgramServiceImpl( + () -> List.of(new PixelProgram(0, List.of())), + sent::set + ); + + PixelProgram saved = service.save(List.of()); + + assertEquals(1, saved.index()); + assertEquals(2, sent.get().size()); + } + + @Test + void save_loadedNullProgram_throwsPixelProgramOperationException() { + PixelProgramServiceImpl service = new PixelProgramServiceImpl( + () -> new ArrayList<>(Collections.singletonList(null)), + _ -> {} + ); + + PixelProgramOperationException exception = assertThrows(PixelProgramOperationException.class, + () -> service.save(List.of())); + + assertEquals("Cannot save pixel program", exception.getMessage()); + } + + @Test + void get_missingProgram_throwsPixelProgramNotFoundException() { + PixelProgramServiceImpl service = new PixelProgramServiceImpl(() -> List.of(), _ -> {}); + + assertThrows(PixelProgramNotFoundException.class, () -> service.get(7)); + } +} diff --git a/src/test/java/pl/vtt/wpi/core/application/service/impl/RebootServiceImplTest.java b/src/test/java/pl/vtt/wpi/core/application/service/impl/RebootServiceImplTest.java new file mode 100644 index 0000000..8344e3f --- /dev/null +++ b/src/test/java/pl/vtt/wpi/core/application/service/impl/RebootServiceImplTest.java @@ -0,0 +1,33 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; +import pl.vtt.wpi.core.application.exception.DeviceRebootException; +import pl.vtt.wpi.core.domain.port.exception.InputPortException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RebootServiceImplTest { + + @Test + void reboot_success_sendsNullPayload() throws Exception { + AtomicBoolean called = new AtomicBoolean(false); + RebootServiceImpl service = new RebootServiceImpl(_ -> called.set(true)); + + service.reboot(); + + assertTrue(called.get()); + } + + @Test + void reboot_portFailure_wrapsException() { + RebootServiceImpl service = new RebootServiceImpl( + _ -> { throw new InputPortException("x", new IllegalStateException("boom")); } + ); + + DeviceRebootException exception = assertThrows(DeviceRebootException.class, service::reboot); + assertEquals("Cannot reboot device", exception.getMessage()); + } +} diff --git a/src/test/java/pl/vtt/wpi/core/application/service/impl/RuntimeDataServiceImplTest.java b/src/test/java/pl/vtt/wpi/core/application/service/impl/RuntimeDataServiceImplTest.java new file mode 100644 index 0000000..1c4c417 --- /dev/null +++ b/src/test/java/pl/vtt/wpi/core/application/service/impl/RuntimeDataServiceImplTest.java @@ -0,0 +1,82 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import pl.vtt.wpi.core.application.exception.DataInconsistencyException; +import pl.vtt.wpi.core.application.exception.RuntimeDataOperationException; +import pl.vtt.wpi.core.domain.model.device.PixelProgram; +import pl.vtt.wpi.core.domain.model.device.RuntimeData; +import pl.vtt.wpi.core.domain.port.exception.OutputPortException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class RuntimeDataServiceImplTest { + + @Test + void read_success_returnsRuntimeData() throws Exception { + RuntimeData runtimeData = new RuntimeData.Builder().build(); + RuntimeDataServiceImpl service = new RuntimeDataServiceImpl(() -> runtimeData, () -> List.of(), _ -> {}); + + RuntimeData result = service.read(); + + assertEquals(runtimeData, result); + } + + @Test + void read_outputPortFailure_throwsRuntimeDataOperationException() { + RuntimeDataServiceImpl service = new RuntimeDataServiceImpl( + () -> { throw new OutputPortException("read failed"); }, + () -> List.of(), + _ -> {} + ); + + RuntimeDataOperationException exception = assertThrows(RuntimeDataOperationException.class, service::read); + + assertInstanceOf(OutputPortException.class, exception.getCause()); + } + + @Test + void set_nullRuntimeData_throwsRuntimeDataOperationException() { + RuntimeDataServiceImpl service = new RuntimeDataServiceImpl(() -> null, () -> List.of(), _ -> {}); + + RuntimeDataOperationException exception = assertThrows(RuntimeDataOperationException.class, + () -> service.set(null)); + + assertEquals("Runtime data cannot be null", exception.getMessage()); + } + + @Test + void set_missingPixelProgram_throwsDataInconsistencyExceptionWithNoSuchElementCause() { + RuntimeData runtimeData = new RuntimeData.Builder().pixelProgram(10).build(); + RuntimeDataServiceImpl service = new RuntimeDataServiceImpl( + () -> runtimeData, + () -> List.of(new PixelProgram(0, List.of())), + _ -> {} + ); + + DataInconsistencyException exception = assertThrows(DataInconsistencyException.class, + () -> service.set(runtimeData)); + + assertEquals("Pixel program 10 does not exist", exception.getMessage()); + assertInstanceOf(NoSuchElementException.class, exception.getCause()); + } + + @Test + void set_existingPixelProgram_sendsRuntimeData() throws Exception { + RuntimeData runtimeData = new RuntimeData.Builder().pixelProgram(1).build(); + AtomicReference sent = new AtomicReference<>(); + RuntimeDataServiceImpl service = new RuntimeDataServiceImpl( + () -> runtimeData, + () -> List.of(new PixelProgram(1, List.of())), + sent::set + ); + + service.set(runtimeData); + + assertEquals(runtimeData, sent.get()); + } +} diff --git a/src/test/java/pl/vtt/wpi/core/application/service/impl/UserManagementServiceImplTest.java b/src/test/java/pl/vtt/wpi/core/application/service/impl/UserManagementServiceImplTest.java new file mode 100644 index 0000000..f7cd50e --- /dev/null +++ b/src/test/java/pl/vtt/wpi/core/application/service/impl/UserManagementServiceImplTest.java @@ -0,0 +1,102 @@ +package pl.vtt.wpi.core.application.service.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import pl.vtt.wpi.core.application.exception.InvalidPasswordException; +import pl.vtt.wpi.core.application.exception.UserAlreadyExistsException; +import pl.vtt.wpi.core.application.exception.UserManagementOperationException; +import pl.vtt.wpi.core.domain.dto.PasswordDto; +import pl.vtt.wpi.core.domain.dto.UserCreateRequest; +import pl.vtt.wpi.core.domain.model.User; +import pl.vtt.wpi.core.domain.model.endpoint.UserGroup; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class UserManagementServiceImplTest { + + @Test + void createUser_success_sendsUserCreateRequest() throws Exception { + User user = new User("john", EnumSet.of(UserGroup.ADMIN)); + PasswordDto passwordDto = new PasswordDto("secret", "secret"); + AtomicReference sent = new AtomicReference<>(); + + UserManagementServiceImpl service = new UserManagementServiceImpl( + List::of, + sent::set, + _ -> {}, + _ -> {} + ); + + service.createUser(user, passwordDto); + + assertEquals(new UserCreateRequest(user, passwordDto), sent.get()); + } + + @Test + void createUser_existingUser_throwsUserAlreadyExistsException() throws Exception { + User user = new User("john", EnumSet.of(UserGroup.ADMIN)); + PasswordDto passwordDto = new PasswordDto("secret", "secret"); + + UserManagementServiceImpl service = new UserManagementServiceImpl( + () -> List.of(user), + _ -> {}, + _ -> {}, + _ -> {} + ); + + assertThrows(UserAlreadyExistsException.class, () -> service.createUser(user, passwordDto)); + } + + @Test + void createUser_loadedNullUser_throwsUserManagementOperationException() { + User user = new User("john", EnumSet.of(UserGroup.ADMIN)); + PasswordDto passwordDto = new PasswordDto("secret", "secret"); + + UserManagementServiceImpl service = new UserManagementServiceImpl( + () -> new ArrayList<>(Collections.singletonList(null)), + _ -> {}, + _ -> {}, + _ -> {} + ); + + UserManagementOperationException exception = assertThrows(UserManagementOperationException.class, + () -> service.createUser(user, passwordDto)); + + assertEquals("Users cannot contain null elements", exception.getMessage()); + } + + @Test + void removeUser_loadedNullUser_throwsUserManagementOperationException() { + User user = new User("john", EnumSet.of(UserGroup.ADMIN)); + + UserManagementServiceImpl service = new UserManagementServiceImpl( + () -> new ArrayList<>(Collections.singletonList(null)), + _ -> {}, + _ -> {}, + _ -> {} + ); + + UserManagementOperationException exception = assertThrows(UserManagementOperationException.class, + () -> service.removeUser(user)); + + assertEquals("Users cannot contain null elements", exception.getMessage()); + } + + @Test + void changePassword_blankPassword_throwsInvalidPasswordException() { + UserManagementServiceImpl service = new UserManagementServiceImpl( + List::of, + _ -> {}, + _ -> {}, + _ -> {} + ); + + assertThrows(InvalidPasswordException.class, + () -> service.changePassword(new PasswordDto(" ", " "))); + } +} diff --git a/src/test/java/pl/vtt/wpi/core/domain/port/UserCreateInputPortTest.java b/src/test/java/pl/vtt/wpi/core/domain/port/UserCreateInputPortTest.java index d7a098a..f87f27c 100644 --- a/src/test/java/pl/vtt/wpi/core/domain/port/UserCreateInputPortTest.java +++ b/src/test/java/pl/vtt/wpi/core/domain/port/UserCreateInputPortTest.java @@ -3,6 +3,8 @@ import java.util.EnumSet; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; +import pl.vtt.wpi.core.domain.dto.PasswordDto; +import pl.vtt.wpi.core.domain.dto.UserCreateRequest; import pl.vtt.wpi.core.infrastructure.RequestFactory; import pl.vtt.wpi.core.infrastructure.RequestSender; import pl.vtt.wpi.core.domain.port.exception.InputPortException; @@ -23,14 +25,16 @@ class UserCreateInputPortTest { @Test void send_success_invokesFactoryAndSender() throws Exception { User user = new User("admin", EnumSet.of(UserGroup.ADMIN)); + PasswordDto passwordDto = new PasswordDto("secret", "secret"); + UserCreateRequest userCreateRequest = new UserCreateRequest(user, passwordDto); AtomicReference usedMethod = new AtomicReference<>(); AtomicReference usedTarget = new AtomicReference<>(); - AtomicReference usedPayload = new AtomicReference<>(); + AtomicReference usedPayload = new AtomicReference<>(); AtomicReference> sentRequest = new AtomicReference<>(); - Request request = new Request<>(null, Method.POST, - RequestTarget.USERS_CREATE.url("http://localhost"), null, user); - RequestFactory requestFactory = (payload, method, target, _) -> { + Request request = new Request<>(null, Method.POST, + RequestTarget.USERS_CREATE.url("http://localhost"), null, userCreateRequest); + RequestFactory requestFactory = (payload, method, target, _) -> { usedMethod.set(method); usedTarget.set(target); usedPayload.set(payload); @@ -40,27 +44,47 @@ void send_success_invokesFactoryAndSender() throws Exception { UserCreateInputPort port = new UserCreateInputPort(requestFactory, requestSender); - port.send(user); + port.send(userCreateRequest); assertEquals(Method.POST, usedMethod.get()); assertEquals(RequestTarget.USERS_CREATE, usedTarget.get()); - assertSame(user, usedPayload.get()); + assertSame(userCreateRequest, usedPayload.get()); assertSame(request, sentRequest.get()); } @Test - void send_nullUser_throwsInputPortException() { + void send_nullRequest_throwsInputPortException() { UserCreateInputPort port = new UserCreateInputPort((_, _, _, _) -> null, _ -> {}); InputPortException exception = assertThrows(InputPortException.class, () -> port.send(null)); - assertTrue(exception.getMessage().contains("User cannot be null")); + assertTrue(exception.getMessage().contains("User and password data cannot be null")); + } + + @Test + void send_nullUserInRequest_throwsInputPortException() { + UserCreateInputPort port = new UserCreateInputPort((_, _, _, _) -> null, _ -> {}); + + InputPortException exception = assertThrows(InputPortException.class, () -> + port.send(new UserCreateRequest(null, new PasswordDto("secret", "secret")))); + + assertTrue(exception.getMessage().contains("User and password data cannot be null")); + } + + @Test + void send_nullPasswordInRequest_throwsInputPortException() { + UserCreateInputPort port = new UserCreateInputPort((_, _, _, _) -> null, _ -> {}); + + InputPortException exception = assertThrows(InputPortException.class, () -> + port.send(new UserCreateRequest(new User("admin", EnumSet.of(UserGroup.ADMIN)), null))); + + assertTrue(exception.getMessage().contains("User and password data cannot be null")); } @Test void send_factoryException_wrapsWithCause() { RuntimeException originalCause = new RuntimeException("factory failure"); - RequestFactory requestFactory = (_, _, _, _) -> { + RequestFactory requestFactory = (_, _, _, _) -> { throw originalCause; }; RequestSender requestSender = _ -> {}; @@ -68,7 +92,10 @@ void send_factoryException_wrapsWithCause() { UserCreateInputPort port = new UserCreateInputPort(requestFactory, requestSender); InputPortException exception = assertThrows(InputPortException.class, - () -> port.send(new User("admin", EnumSet.of(UserGroup.ADMIN)))); + () -> port.send(new UserCreateRequest( + new User("admin", EnumSet.of(UserGroup.ADMIN)), + new PasswordDto("secret", "secret") + ))); assertTrue(exception.getMessage().contains("Cannot create user")); assertSame(originalCause, exception.getCause()); @@ -77,9 +104,11 @@ void send_factoryException_wrapsWithCause() { @Test void send_senderException_wrapsWithCause() { User user = new User("admin", EnumSet.of(UserGroup.ADMIN)); + PasswordDto passwordDto = new PasswordDto("secret", "secret"); + UserCreateRequest userCreateRequest = new UserCreateRequest(user, passwordDto); RuntimeException originalCause = new RuntimeException("send failure"); - RequestFactory requestFactory = (payload, method, target, _) -> + RequestFactory requestFactory = (payload, method, target, _) -> new Request<>(null, method, target.url("http://localhost"), null, payload); RequestSender requestSender = _ -> { throw originalCause; @@ -87,7 +116,7 @@ void send_senderException_wrapsWithCause() { UserCreateInputPort port = new UserCreateInputPort(requestFactory, requestSender); - InputPortException exception = assertThrows(InputPortException.class, () -> port.send(user)); + InputPortException exception = assertThrows(InputPortException.class, () -> port.send(userCreateRequest)); assertTrue(exception.getMessage().contains("Cannot create user")); assertSame(originalCause, exception.getCause());