mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-10-11 12:19:03 +00:00
feat: Add the implementation of Onion-Architecture (#3532)
* Add the implementation of Onion-Architecture * Execute maven command: mvn spotless:apply
This commit is contained in:
1 parent
b55fc2bcb5
commit
688095a4c7
32 files changed
+3182
-1
No files matched your search
@@ -0,0 +1,292 @@
|
|||||||
|
---
|
||||||
|
title: "Onion Architecture in Java: A Layered Approach to Building Maintainable and Testable Applications"
|
||||||
|
shortTitle: Onion Architecture
|
||||||
|
description: "Learn how the Onion Architecture pattern promotes maintainability, testability, and separation of concerns in Java applications. Explore examples, benefits, and best practices."
|
||||||
|
category: Architectural
|
||||||
|
language: en
|
||||||
|
tag:
|
||||||
|
- Decoupling
|
||||||
|
- Enterprise patterns
|
||||||
|
- Integration
|
||||||
|
- Microservices
|
||||||
|
- Scalability
|
||||||
|
- Security
|
||||||
|
---
|
||||||
|
## Intent of Microservices API Gateway Design Pattern
|
||||||
|
|
||||||
|
In this project, the implementation demonstrates **Onion Architecture** with clear dependency direction: infrastructure and application depend on domain, but domain is independent.
|
||||||
|
|
||||||
|
The central intent is to keep business rules in `domain`, orchestrate use cases in `application`, and isolate delivery/persistence/API details in `infrastructure`.
|
||||||
|
|
||||||
|
Current implementation highlights:
|
||||||
|
|
||||||
|
* `domain`: `Person`, `Category`, `PersonRepository`, `DomainException`
|
||||||
|
* `application`: `SavePersonUseCase`, `GetPersonUseCase`, DTO records
|
||||||
|
* `infrastructure`: Spring Boot REST controller, JPA entities, repository adapter, bean wiring
|
||||||
|
|
||||||
|
## Also known as
|
||||||
|
|
||||||
|
* Ports and Adapters Architecture
|
||||||
|
* Hexagonal-style layering (conceptually related)
|
||||||
|
* Dependency-rule-first architecture
|
||||||
|
|
||||||
|
## Detailed Explanation of Onion Architecture Pattern with Real-World Examples
|
||||||
|
|
||||||
|
Real-world example
|
||||||
|
|
||||||
|
> Imagine a people-management service where business validation must stay consistent no matter how data is stored or exposed. In this codebase, `Person` and `Category` enforce invariants (e.g., age >= 18, required email/category), use cases coordinate behavior, and infrastructure adapts HTTP + JPA concerns. This allows the persistence or web layer to evolve without changing core domain rules.
|
||||||
|
|
||||||
|
In plain words
|
||||||
|
|
||||||
|
> The project keeps business logic in the center and treats frameworks as replaceable details around it.
|
||||||
|
|
||||||
|
Wikipedia says
|
||||||
|
|
||||||
|
> Onion Architecture is a software architecture pattern that emphasizes separation of concerns and dependency inversion by organizing code in concentric layers, with the domain model at the center.
|
||||||
|
|
||||||
|
Sequence diagram
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Request flow in this implementation:
|
||||||
|
|
||||||
|
1. Client calls REST endpoint in `PersonController` (`/api/persons`, `/api/persons/{id}`)
|
||||||
|
2. Controller delegates to `SavePersonUseCase` or `GetPersonUseCase`
|
||||||
|
3. Use case interacts with `PersonRepository` abstraction from `domain`
|
||||||
|
4. `PersonRepositoryAdapter` maps domain <-> JPA and delegates to `SpringDataPersonRepository`
|
||||||
|
5. Response DTO (`PersonResponse`) is returned to the client
|
||||||
|
|
||||||
|
## Programmatic Example of Onion Architecture in Java
|
||||||
|
|
||||||
|
This repository exposes a simple person API backed by use cases and domain models.
|
||||||
|
|
||||||
|
Controller (infrastructure layer):
|
||||||
|
|
||||||
|
```java
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api")
|
||||||
|
public class PersonController {
|
||||||
|
|
||||||
|
@GetMapping("/persons/{id}")
|
||||||
|
public ResponseEntity<PersonResponse> getPerson(@PathVariable Long id) {
|
||||||
|
var person = getPersonUseCase.execute(id);
|
||||||
|
return ResponseEntity.ok(person);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/persons")
|
||||||
|
public ResponseEntity<List<PersonResponse>> getAllPersons() {
|
||||||
|
var persons = getPersonUseCase.executeAll();
|
||||||
|
return ResponseEntity.ok(persons);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/persons")
|
||||||
|
public ResponseEntity<PersonResponse> savePerson(@RequestBody SavePersonCommand command) {
|
||||||
|
try {
|
||||||
|
var savedPerson = savePersonUseCase.execute(command);
|
||||||
|
return ResponseEntity.status(HttpStatus.OK).body(savedPerson);
|
||||||
|
} catch (DomainException e) {
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use case (application layer):
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class SavePersonUseCase {
|
||||||
|
|
||||||
|
private final PersonRepository repository;
|
||||||
|
|
||||||
|
public PersonResponse execute(SavePersonCommand command) {
|
||||||
|
var category = new Category(command.categoryId(), command.categoryType());
|
||||||
|
var person = new Person(
|
||||||
|
null,
|
||||||
|
command.firstName(),
|
||||||
|
command.lastName(),
|
||||||
|
command.age(),
|
||||||
|
command.phoneNumber(),
|
||||||
|
command.email(),
|
||||||
|
category);
|
||||||
|
|
||||||
|
var savedPerson = repository.save(person);
|
||||||
|
return new PersonResponse(
|
||||||
|
savedPerson.getId(),
|
||||||
|
savedPerson.getFirstName(),
|
||||||
|
savedPerson.getLastName(),
|
||||||
|
savedPerson.getAge(),
|
||||||
|
savedPerson.getPhoneNumber(),
|
||||||
|
savedPerson.getEmail(),
|
||||||
|
savedPerson.getCategory().getId(),
|
||||||
|
savedPerson.getCategory().getType()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Domain validation (domain layer):
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Person {
|
||||||
|
public Person(Long id, String firstName, String lastName, int age,
|
||||||
|
String phoneNumber, String email, Category category) {
|
||||||
|
validateNames(firstName, lastName);
|
||||||
|
validateAge(age);
|
||||||
|
validatePhone(phoneNumber);
|
||||||
|
validateEmail(email);
|
||||||
|
validateCategory(category);
|
||||||
|
// assign fields...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Repository adapter (infrastructure -> domain port):
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Repository
|
||||||
|
public class PersonRepositoryAdapter implements PersonRepository {
|
||||||
|
|
||||||
|
private final SpringDataPersonRepository repository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Person> findById(Long id) {
|
||||||
|
return repository.findById(id).map(this::mapToDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Person save(Person person) {
|
||||||
|
JpaPersonEntity savedEntity = repository.save(mapToEntity(person));
|
||||||
|
return mapToDomain(savedEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Maven 3.6.0** or higher
|
||||||
|
|
||||||
|
### Build Steps
|
||||||
|
|
||||||
|
1. **Navigate to the onion-architecture module directory:**
|
||||||
|
```bash
|
||||||
|
cd java-design-patterns/onion-architecture
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build all modules:**
|
||||||
|
```bash
|
||||||
|
mvn clean package
|
||||||
|
```
|
||||||
|
This will compile the `domain`, `application`, and `infrastructure` modules and package them into a Spring Boot executable JAR.
|
||||||
|
|
||||||
|
3. **Run the Spring Boot application:**
|
||||||
|
```bash
|
||||||
|
mvn -pl infrastructure spring-boot:run
|
||||||
|
```
|
||||||
|
Alternatively, after building, run the JAR directly:
|
||||||
|
```bash
|
||||||
|
java -jar infrastructure/target/infrastructure-1.26.0-SNAPSHOT.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accessing the API
|
||||||
|
|
||||||
|
The application exposes REST endpoints at `http://localhost:8080/api`:
|
||||||
|
There is a Postman collection available in the `etc/postman` folder for testing the API.
|
||||||
|
|
||||||
|
- **Get all persons:**
|
||||||
|
```bash
|
||||||
|
GET http://localhost:8080/api/persons
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Get person by ID:**
|
||||||
|
```bash
|
||||||
|
GET http://localhost:8080/api/persons/{id}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Create a new person:**
|
||||||
|
```bash
|
||||||
|
POST http://localhost:8080/api/persons
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"age": 30,
|
||||||
|
"phoneNumber": "555-1234",
|
||||||
|
"email": "john.doe@example.com",
|
||||||
|
"address": "123 Main St",
|
||||||
|
"categoryId": 1,
|
||||||
|
"categoryType": "individual"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Tests
|
||||||
|
|
||||||
|
To execute unit tests across all modules:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn clean test
|
||||||
|
```
|
||||||
|
|
||||||
|
To run tests for a specific module:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn -pl domain test
|
||||||
|
mvn -pl application test
|
||||||
|
mvn -pl infrastructure test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
The application uses an **H2 in-memory database** for demonstration purposes. Configuration is in `infrastructure/src/main/resources/application.properties`:
|
||||||
|
|
||||||
|
- **JDBC URL:** `jdbc:h2:mem:testdb`
|
||||||
|
- **Username:** `sa`
|
||||||
|
- **Password:** `password`
|
||||||
|
|
||||||
|
Sample data is initialized from `infrastructure/src/main/resources/data.sql` on application startup.
|
||||||
|
|
||||||
|
## When to Use the Onion Architecture Pattern in Java
|
||||||
|
|
||||||
|
* When domain rules must be stable and independent from frameworks.
|
||||||
|
* When you want use cases to be testable without HTTP or database setup.
|
||||||
|
* When infrastructure details (web, JPA, database) should be replaceable.
|
||||||
|
* When dependency direction must be enforced from outer layers toward the domain core.
|
||||||
|
|
||||||
|
## Onion Architecture Pattern Java Tutorials
|
||||||
|
|
||||||
|
* [Clean Architecture with Spring Boot (Baeldung)](https://www.baeldung.com/spring-boot-clean-architecture)
|
||||||
|
* [Hexagonal Architecture Explained (Cockburn)](https://alistair.cockburn.us/hexagonal-architecture)
|
||||||
|
* [Spring Data JPA Reference](https://docs.spring.io/spring-data/jpa/reference/)
|
||||||
|
|
||||||
|
## Benefits and Trade-offs of Microservices API Gateway Pattern
|
||||||
|
|
||||||
|
Benefits:
|
||||||
|
|
||||||
|
* Business validations are centralized in domain constructors (`Person`, `Category`).
|
||||||
|
* Use cases stay independent from Spring, JPA, and transport concerns.
|
||||||
|
* Repository abstraction (`PersonRepository`) keeps application logic persistence-agnostic.
|
||||||
|
* Testability is strong across layers (domain, use case, adapter, controller tests).
|
||||||
|
|
||||||
|
Trade-offs:
|
||||||
|
|
||||||
|
* Additional mapping code between domain models, DTOs, and JPA entities.
|
||||||
|
* More classes and modules than a simple CRUD-by-controller approach.
|
||||||
|
* Requires discipline to avoid leaking infrastructure concerns into domain/application.
|
||||||
|
|
||||||
|
## Real-World Applications of Microservices API Gateway Pattern in Java
|
||||||
|
|
||||||
|
* People/contact management services with strict data validation.
|
||||||
|
* Internal platforms where multiple delivery mechanisms (REST, batch, messaging) can share the same core domain.
|
||||||
|
* Systems that need incremental infrastructure evolution while preserving business logic.
|
||||||
|
|
||||||
|
## Related Java Design Patterns
|
||||||
|
|
||||||
|
* [Repository](https://martinfowler.com/eaaCatalog/repository.html) - `PersonRepository` defines the domain-facing persistence contract.
|
||||||
|
* [Adapter](https://refactoring.guru/design-patterns/adapter) - `PersonRepositoryAdapter` bridges domain model and Spring Data JPA.
|
||||||
|
* [Dependency Injection](https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html) - `ApplicationConfig` wires use case beans with repository implementations.
|
||||||
|
|
||||||
|
## References and Credits
|
||||||
|
|
||||||
|
* Project modules: `domain`, `application`, `infrastructure`
|
||||||
|
* Java 21 + Maven multi-module setup
|
||||||
|
* Spring Boot 3.3 (`spring-boot-starter-web`, `spring-boot-starter-data-jpa`, H2)
|
||||||
|
* Layer-focused tests in each module validating domain invariants and use case behavior
|
||||||
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
|
||||||
|
The MIT License
|
||||||
|
Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.iluwatar</groupId>
|
||||||
|
<artifactId>onion-architecture</artifactId>
|
||||||
|
<version>1.26.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<artifactId>application</artifactId>
|
||||||
|
<name>Application</name>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.iluwatar</groupId>
|
||||||
|
<artifactId>domain</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JUnit 5 for testing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Mockito for mocking -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-junit-jupiter</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.application.dto;
|
||||||
|
|
||||||
|
public record PersonResponse(
|
||||||
|
Long id,
|
||||||
|
String firstName,
|
||||||
|
String lastName,
|
||||||
|
int age,
|
||||||
|
String phoneNumber,
|
||||||
|
String email,
|
||||||
|
Long categoryId,
|
||||||
|
String categoryType) {}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.application.dto;
|
||||||
|
|
||||||
|
public record SavePersonCommand(
|
||||||
|
String firstName,
|
||||||
|
String lastName,
|
||||||
|
int age,
|
||||||
|
String phoneNumber,
|
||||||
|
String email,
|
||||||
|
String address,
|
||||||
|
Long categoryId,
|
||||||
|
String categoryType) {}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.application.usecase;
|
||||||
|
|
||||||
|
import com.iluwatar.onion.application.dto.PersonResponse;
|
||||||
|
import com.iluwatar.onion.domain.repository.PersonRepository;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class GetPersonUseCase {
|
||||||
|
|
||||||
|
private final PersonRepository repository;
|
||||||
|
|
||||||
|
public GetPersonUseCase(PersonRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PersonResponse execute(Long id) {
|
||||||
|
var person =
|
||||||
|
repository
|
||||||
|
.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Person not found with id: " + id));
|
||||||
|
|
||||||
|
return new PersonResponse(
|
||||||
|
person.getId(),
|
||||||
|
person.getFirstName(),
|
||||||
|
person.getLastName(),
|
||||||
|
person.getAge(),
|
||||||
|
person.getPhoneNumber(),
|
||||||
|
person.getEmail(),
|
||||||
|
person.getCategory().getId(),
|
||||||
|
person.getCategory().getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PersonResponse> executeAll() {
|
||||||
|
return repository.findAll().stream()
|
||||||
|
.flatMap(Collection::stream)
|
||||||
|
.map(
|
||||||
|
person ->
|
||||||
|
new PersonResponse(
|
||||||
|
person.getId(),
|
||||||
|
person.getFirstName(),
|
||||||
|
person.getLastName(),
|
||||||
|
person.getAge(),
|
||||||
|
person.getPhoneNumber(),
|
||||||
|
person.getEmail(),
|
||||||
|
person.getCategory().getId(),
|
||||||
|
person.getCategory().getType()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.application.usecase;
|
||||||
|
|
||||||
|
import com.iluwatar.onion.application.dto.PersonResponse;
|
||||||
|
import com.iluwatar.onion.application.dto.SavePersonCommand;
|
||||||
|
import com.iluwatar.onion.domain.model.Category;
|
||||||
|
import com.iluwatar.onion.domain.model.Person;
|
||||||
|
import com.iluwatar.onion.domain.repository.PersonRepository;
|
||||||
|
|
||||||
|
public class SavePersonUseCase {
|
||||||
|
|
||||||
|
private final PersonRepository repository;
|
||||||
|
|
||||||
|
public SavePersonUseCase(PersonRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PersonResponse execute(SavePersonCommand command) {
|
||||||
|
var category = new Category(command.categoryId(), command.categoryType());
|
||||||
|
var person =
|
||||||
|
new Person(
|
||||||
|
null,
|
||||||
|
command.firstName(),
|
||||||
|
command.lastName(),
|
||||||
|
command.age(),
|
||||||
|
command.phoneNumber(),
|
||||||
|
command.email(),
|
||||||
|
category);
|
||||||
|
|
||||||
|
var savedPerson = repository.save(person);
|
||||||
|
|
||||||
|
return new PersonResponse(
|
||||||
|
savedPerson.getId(),
|
||||||
|
savedPerson.getFirstName(),
|
||||||
|
savedPerson.getLastName(),
|
||||||
|
savedPerson.getAge(),
|
||||||
|
savedPerson.getPhoneNumber(),
|
||||||
|
savedPerson.getEmail(),
|
||||||
|
savedPerson.getCategory().getId(),
|
||||||
|
savedPerson.getCategory().getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
+193
@@ -0,0 +1,193 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.application.usecase;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import com.iluwatar.onion.application.dto.PersonResponse;
|
||||||
|
import com.iluwatar.onion.domain.model.Category;
|
||||||
|
import com.iluwatar.onion.domain.model.Person;
|
||||||
|
import com.iluwatar.onion.domain.repository.PersonRepository;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class GetPersonUseCaseTest {
|
||||||
|
|
||||||
|
@Mock private PersonRepository personRepository;
|
||||||
|
|
||||||
|
private GetPersonUseCase getPersonUseCase;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
getPersonUseCase = new GetPersonUseCase(personRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Execute by ID - Happy Path")
|
||||||
|
class ExecuteByIdHappyPath {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return PersonResponse when person exists")
|
||||||
|
void shouldReturnPersonResponseWhenPersonExists() {
|
||||||
|
// Arrange
|
||||||
|
var person =
|
||||||
|
new Person(
|
||||||
|
1L,
|
||||||
|
"John",
|
||||||
|
"Doe",
|
||||||
|
25,
|
||||||
|
"+1234567890",
|
||||||
|
"john.doe@example.com",
|
||||||
|
new Category(1L, "Professional"));
|
||||||
|
|
||||||
|
when(personRepository.findById(1L)).thenReturn(Optional.of(person));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = getPersonUseCase.execute(1L);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(response);
|
||||||
|
assertEquals(1L, response.id());
|
||||||
|
assertEquals("John", response.firstName());
|
||||||
|
assertEquals("Doe", response.lastName());
|
||||||
|
assertEquals(25, response.age());
|
||||||
|
assertEquals("+1234567890", response.phoneNumber());
|
||||||
|
assertEquals("john.doe@example.com", response.email());
|
||||||
|
assertEquals(1L, response.categoryId());
|
||||||
|
assertEquals("Professional", response.categoryType());
|
||||||
|
|
||||||
|
verify(personRepository, times(1)).findById(1L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Execute by ID - Error Cases")
|
||||||
|
class ExecuteByIdErrorCases {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw RuntimeException when person not found")
|
||||||
|
void shouldThrowExceptionWhenPersonNotFound() {
|
||||||
|
// Arrange
|
||||||
|
when(personRepository.findById(999L)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = assertThrows(RuntimeException.class, () -> getPersonUseCase.execute(999L));
|
||||||
|
assertTrue(exception.getMessage().contains("Person not found with id: 999"));
|
||||||
|
|
||||||
|
verify(personRepository, times(1)).findById(999L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should propagate exception when repository throws exception")
|
||||||
|
void shouldPropagateExceptionWhenRepositoryFails() {
|
||||||
|
// Arrange
|
||||||
|
when(personRepository.findById(anyLong())).thenThrow(new RuntimeException("Database error"));
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = assertThrows(RuntimeException.class, () -> getPersonUseCase.execute(1L));
|
||||||
|
assertTrue(exception.getMessage().contains("Database error"));
|
||||||
|
|
||||||
|
verify(personRepository, times(1)).findById(1L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Execute All - Happy Path")
|
||||||
|
class ExecuteAllHappyPath {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return list of PersonResponses")
|
||||||
|
void shouldReturnListOfPersonResponses() {
|
||||||
|
// Arrange
|
||||||
|
var person1 =
|
||||||
|
new Person(
|
||||||
|
1L,
|
||||||
|
"John",
|
||||||
|
"Doe",
|
||||||
|
30,
|
||||||
|
"+9876543255",
|
||||||
|
"john.doe@example.com",
|
||||||
|
new Category(2L, "Personal"));
|
||||||
|
|
||||||
|
var person2 =
|
||||||
|
new Person(
|
||||||
|
2L,
|
||||||
|
"Jane",
|
||||||
|
"Smith",
|
||||||
|
25,
|
||||||
|
"+9876543210",
|
||||||
|
"jane.smith@example.com",
|
||||||
|
new Category(2L, "Personal"));
|
||||||
|
|
||||||
|
when(personRepository.findAll()).thenReturn(Optional.of(List.of(person1, person2)));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var responses = getPersonUseCase.executeAll();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(responses);
|
||||||
|
assertEquals(2, responses.size());
|
||||||
|
|
||||||
|
PersonResponse response1 = responses.get(0);
|
||||||
|
assertEquals(1L, response1.id());
|
||||||
|
assertEquals("John", response1.firstName());
|
||||||
|
assertEquals("Doe", response1.lastName());
|
||||||
|
|
||||||
|
PersonResponse response2 = responses.get(1);
|
||||||
|
assertEquals(2L, response2.id());
|
||||||
|
assertEquals("Jane", response2.firstName());
|
||||||
|
assertEquals("Smith", response2.lastName());
|
||||||
|
|
||||||
|
verify(personRepository, times(1)).findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return empty list when no persons found")
|
||||||
|
void shouldReturnEmptyListWhenNoPersonsFound() {
|
||||||
|
// Arrange
|
||||||
|
when(personRepository.findAll()).thenReturn(Optional.of(List.of()));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
List<PersonResponse> responses = getPersonUseCase.executeAll();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(responses);
|
||||||
|
assertTrue(responses.isEmpty());
|
||||||
|
|
||||||
|
verify(personRepository, times(1)).findAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+253
@@ -0,0 +1,253 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.application.usecase;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import com.iluwatar.onion.application.dto.SavePersonCommand;
|
||||||
|
import com.iluwatar.onion.domain.exception.DomainException;
|
||||||
|
import com.iluwatar.onion.domain.model.Category;
|
||||||
|
import com.iluwatar.onion.domain.model.Person;
|
||||||
|
import com.iluwatar.onion.domain.repository.PersonRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class SavePersonUseCaseTest {
|
||||||
|
|
||||||
|
@Mock private PersonRepository personRepository;
|
||||||
|
|
||||||
|
@Captor private ArgumentCaptor<Person> personCaptor;
|
||||||
|
|
||||||
|
private SavePersonUseCase savePersonUseCase;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
savePersonUseCase = new SavePersonUseCase(personRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Execute - Happy Path")
|
||||||
|
class ExecuteHappyPath {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should save person and return PersonResponse")
|
||||||
|
void shouldSavePersonAndReturnResponse() {
|
||||||
|
// Arrange
|
||||||
|
var command =
|
||||||
|
new SavePersonCommand(
|
||||||
|
"John",
|
||||||
|
"Doe",
|
||||||
|
25,
|
||||||
|
"+1234567890",
|
||||||
|
"john.doe@example.com",
|
||||||
|
"123 Main St",
|
||||||
|
1L,
|
||||||
|
"Professional");
|
||||||
|
|
||||||
|
var category = new Category(1L, "Professional");
|
||||||
|
var savedPerson =
|
||||||
|
new Person(
|
||||||
|
100L, // ID generated by database
|
||||||
|
"John",
|
||||||
|
"Doe",
|
||||||
|
25,
|
||||||
|
"+1234567890",
|
||||||
|
"john.doe@example.com",
|
||||||
|
category);
|
||||||
|
|
||||||
|
when(personRepository.save(any(Person.class))).thenReturn(savedPerson);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = savePersonUseCase.execute(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(response);
|
||||||
|
assertEquals(100L, response.id());
|
||||||
|
assertEquals("John", response.firstName());
|
||||||
|
assertEquals("Doe", response.lastName());
|
||||||
|
assertEquals(25, response.age());
|
||||||
|
assertEquals("+1234567890", response.phoneNumber());
|
||||||
|
assertEquals("john.doe@example.com", response.email());
|
||||||
|
assertEquals(1L, response.categoryId());
|
||||||
|
assertEquals("Professional", response.categoryType());
|
||||||
|
|
||||||
|
// Verify repository interaction
|
||||||
|
verify(personRepository, times(1)).save(any(Person.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should pass correct Person object to repository")
|
||||||
|
void shouldPassCorrectPersonToRepository() {
|
||||||
|
// Arrange
|
||||||
|
var command =
|
||||||
|
new SavePersonCommand(
|
||||||
|
"Jane",
|
||||||
|
"Smith",
|
||||||
|
30,
|
||||||
|
"+9876543210",
|
||||||
|
"jane.smith@example.com",
|
||||||
|
"456 Oak Ave",
|
||||||
|
2L,
|
||||||
|
"Personal");
|
||||||
|
|
||||||
|
var category = new Category(2L, "Personal");
|
||||||
|
var savedPerson =
|
||||||
|
new Person(200L, "Jane", "Smith", 30, "+9876543210", "jane.smith@example.com", category);
|
||||||
|
|
||||||
|
when(personRepository.save(any(Person.class))).thenReturn(savedPerson);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
savePersonUseCase.execute(command);
|
||||||
|
|
||||||
|
// Assert - Verify what was passed to repository
|
||||||
|
verify(personRepository).save(personCaptor.capture());
|
||||||
|
var capturedPerson = personCaptor.getValue();
|
||||||
|
|
||||||
|
assertNull(capturedPerson.getId()); // New person should have null ID
|
||||||
|
assertEquals("Jane", capturedPerson.getFirstName());
|
||||||
|
assertEquals("Smith", capturedPerson.getLastName());
|
||||||
|
assertEquals(30, capturedPerson.getAge());
|
||||||
|
assertEquals("+9876543210", capturedPerson.getPhoneNumber());
|
||||||
|
assertEquals("jane.smith@example.com", capturedPerson.getEmail());
|
||||||
|
assertEquals("Personal", capturedPerson.getCategory().getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Execute - Validation Failures")
|
||||||
|
class ExecuteValidationFailures {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw DomainException when age is less than 18")
|
||||||
|
void shouldThrowExceptionWhenAgeIsInvalid() {
|
||||||
|
// Arrange
|
||||||
|
var command =
|
||||||
|
new SavePersonCommand(
|
||||||
|
"Young",
|
||||||
|
"Person",
|
||||||
|
17, // Invalid age
|
||||||
|
"+1234567890",
|
||||||
|
"young@example.com",
|
||||||
|
"123 Main St",
|
||||||
|
1L,
|
||||||
|
"Student");
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = assertThrows(DomainException.class, () -> savePersonUseCase.execute(command));
|
||||||
|
assertTrue(exception.getMessage().contains("Age cannot be less than 18"));
|
||||||
|
|
||||||
|
// Verify repository was never called
|
||||||
|
verify(personRepository, never()).save(any(Person.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw DomainException when email is empty")
|
||||||
|
void shouldThrowExceptionWhenEmailIsEmpty() {
|
||||||
|
// Arrange
|
||||||
|
var command =
|
||||||
|
new SavePersonCommand(
|
||||||
|
"John",
|
||||||
|
"Doe",
|
||||||
|
25,
|
||||||
|
"+1234567890",
|
||||||
|
"", // Empty email
|
||||||
|
"123 Main St",
|
||||||
|
1L,
|
||||||
|
"Professional");
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = assertThrows(DomainException.class, () -> savePersonUseCase.execute(command));
|
||||||
|
assertTrue(exception.getMessage().contains("Email cannot be null or empty"));
|
||||||
|
|
||||||
|
verify(personRepository, never()).save(any(Person.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw DomainException when category type is invalid")
|
||||||
|
void shouldThrowExceptionWhenCategoryTypeIsInvalid() {
|
||||||
|
// Arrange
|
||||||
|
var command =
|
||||||
|
new SavePersonCommand(
|
||||||
|
"John",
|
||||||
|
"Doe",
|
||||||
|
25,
|
||||||
|
"+1234567890",
|
||||||
|
"john@example.com",
|
||||||
|
"123 Main St",
|
||||||
|
1L,
|
||||||
|
"" // Empty category type
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = assertThrows(DomainException.class, () -> savePersonUseCase.execute(command));
|
||||||
|
assertTrue(exception.getMessage().contains("Type is null or empty"));
|
||||||
|
|
||||||
|
verify(personRepository, never()).save(any(Person.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Execute - Repository Exceptions")
|
||||||
|
class ExecuteRepositoryExceptions {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should propagate exception when repository fails")
|
||||||
|
void shouldPropagateExceptionWhenRepositoryFails() {
|
||||||
|
// Arrange
|
||||||
|
var command =
|
||||||
|
new SavePersonCommand(
|
||||||
|
"John",
|
||||||
|
"Doe",
|
||||||
|
25,
|
||||||
|
"+1234567890",
|
||||||
|
"john.doe@example.com",
|
||||||
|
"123 Main St",
|
||||||
|
1L,
|
||||||
|
"Professional");
|
||||||
|
|
||||||
|
when(personRepository.save(any(Person.class)))
|
||||||
|
.thenThrow(new RuntimeException("Database connection failed"));
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception =
|
||||||
|
assertThrows(RuntimeException.class, () -> savePersonUseCase.execute(command));
|
||||||
|
assertTrue(exception.getMessage().contains("Database connection failed"));
|
||||||
|
|
||||||
|
verify(personRepository, times(1)).save(any(Person.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
|
||||||
|
The MIT License
|
||||||
|
Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.iluwatar</groupId>
|
||||||
|
<artifactId>onion-architecture</artifactId>
|
||||||
|
<version>1.26.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<artifactId>domain</artifactId>
|
||||||
|
<name>Domain</name>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- JUnit 5 for testing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.domain.exception;
|
||||||
|
|
||||||
|
public class DomainException extends RuntimeException {
|
||||||
|
|
||||||
|
public DomainException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.domain.model;
|
||||||
|
|
||||||
|
import com.iluwatar.onion.domain.exception.DomainException;
|
||||||
|
|
||||||
|
public class Category {
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final String type;
|
||||||
|
|
||||||
|
public Category(Long id, String type) {
|
||||||
|
if (type == null || type.isEmpty()) {
|
||||||
|
throw new DomainException("Type is null or empty. Category type is required.");
|
||||||
|
}
|
||||||
|
this.id = id;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.domain.model;
|
||||||
|
|
||||||
|
import com.iluwatar.onion.domain.exception.DomainException;
|
||||||
|
|
||||||
|
public class Person {
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final String firstName;
|
||||||
|
private final String lastName;
|
||||||
|
private final int age;
|
||||||
|
private final String phoneNumber;
|
||||||
|
private final String email;
|
||||||
|
private final Category category;
|
||||||
|
|
||||||
|
public Person(
|
||||||
|
Long id,
|
||||||
|
String firstName,
|
||||||
|
String lastName,
|
||||||
|
int age,
|
||||||
|
String phoneNumber,
|
||||||
|
String email,
|
||||||
|
Category category) {
|
||||||
|
validateNames(firstName, lastName);
|
||||||
|
validateAge(age);
|
||||||
|
validatePhone(phoneNumber);
|
||||||
|
validateEmail(email);
|
||||||
|
validateCategory(category);
|
||||||
|
|
||||||
|
this.id = id;
|
||||||
|
this.firstName = firstName;
|
||||||
|
this.lastName = lastName;
|
||||||
|
this.age = age;
|
||||||
|
this.phoneNumber = phoneNumber;
|
||||||
|
this.email = email;
|
||||||
|
this.category = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateNames(String firstName, String lastName) {
|
||||||
|
if (firstName == null || lastName == null) {
|
||||||
|
throw new DomainException("First name and last name cannot be null.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateAge(int age) {
|
||||||
|
if (age < 18) {
|
||||||
|
throw new DomainException("Age cannot be less than 18.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validatePhone(String phone) {
|
||||||
|
if (phone == null || phone.isEmpty()) {
|
||||||
|
throw new DomainException("Phone number cannot be null or empty.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateEmail(String email) {
|
||||||
|
if (email == null || email.isEmpty()) {
|
||||||
|
throw new DomainException("Email cannot be null or empty.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateCategory(Category category) {
|
||||||
|
if (category == null || category.getType().isEmpty()) {
|
||||||
|
throw new DomainException("Category cannot be null or empty.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFirstName() {
|
||||||
|
return firstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastName() {
|
||||||
|
return lastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAge() {
|
||||||
|
return age;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhoneNumber() {
|
||||||
|
return phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Category getCategory() {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.domain.repository;
|
||||||
|
|
||||||
|
import com.iluwatar.onion.domain.model.Person;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface PersonRepository {
|
||||||
|
Optional<Person> findById(Long id);
|
||||||
|
|
||||||
|
Optional<Person> findByFirstName(String firstName);
|
||||||
|
|
||||||
|
Optional<Person> findByLastName(String lastName);
|
||||||
|
|
||||||
|
Optional<List<Person>> findAll();
|
||||||
|
|
||||||
|
Person save(Person person);
|
||||||
|
|
||||||
|
boolean deleteById(Long id);
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.domain.model;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import com.iluwatar.onion.domain.exception.DomainException;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class CategoryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create category with valid data")
|
||||||
|
void shouldCreateCategoryWithValidData() {
|
||||||
|
// Arrange & Act
|
||||||
|
var category = new Category(1L, "Professional");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(category);
|
||||||
|
assertEquals(1L, category.getId());
|
||||||
|
assertEquals("Professional", category.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create category with null id")
|
||||||
|
void shouldCreateCategoryWithNullId() {
|
||||||
|
// Arrange & Act
|
||||||
|
var category = new Category(null, "Personal");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNull(category.getId());
|
||||||
|
assertEquals("Personal", category.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when type is null")
|
||||||
|
void shouldThrowExceptionWhenTypeIsNull() {
|
||||||
|
// Act & Assert
|
||||||
|
var exception = assertThrows(DomainException.class, () -> new Category(1L, null));
|
||||||
|
assertTrue(exception.getMessage().contains("Type is null or empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when type is empty")
|
||||||
|
void shouldThrowExceptionWhenTypeIsEmpty() {
|
||||||
|
// Act & Assert
|
||||||
|
var exception = assertThrows(DomainException.class, () -> new Category(1L, ""));
|
||||||
|
assertTrue(exception.getMessage().contains("Type is null or empty"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.domain.model;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import com.iluwatar.onion.domain.exception.DomainException;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class PersonTest {
|
||||||
|
|
||||||
|
private final Category validCategory = new Category(1L, "Professional");
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Person Creation - Valid Cases")
|
||||||
|
class ValidPersonCreation {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create person with valid data")
|
||||||
|
void shouldCreatePersonWithValidData() {
|
||||||
|
// Arrange & Act
|
||||||
|
var person =
|
||||||
|
new Person(1L, "John", "Doe", 25, "+1234567890", "john.doe@example.com", validCategory);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(person);
|
||||||
|
assertEquals(1L, person.getId());
|
||||||
|
assertEquals("John", person.getFirstName());
|
||||||
|
assertEquals("Doe", person.getLastName());
|
||||||
|
assertEquals(25, person.getAge());
|
||||||
|
assertEquals("+1234567890", person.getPhoneNumber());
|
||||||
|
assertEquals("john.doe@example.com", person.getEmail());
|
||||||
|
assertEquals(validCategory, person.getCategory());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create person with minimum valid age (18)")
|
||||||
|
void shouldCreatePersonWithMinimumValidAge() {
|
||||||
|
// Arrange & Act
|
||||||
|
var person =
|
||||||
|
new Person(
|
||||||
|
null,
|
||||||
|
"Jane",
|
||||||
|
"Smith",
|
||||||
|
18, // minimum valid age
|
||||||
|
"+9876543210",
|
||||||
|
"jane@example.com",
|
||||||
|
validCategory);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(18, person.getAge());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Person Creation - Invalid Cases")
|
||||||
|
class InvalidPersonCreation {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when first name is null")
|
||||||
|
void shouldThrowExceptionWhenFirstNameIsNull() {
|
||||||
|
// Act & Assert
|
||||||
|
var exception =
|
||||||
|
assertThrows(
|
||||||
|
DomainException.class,
|
||||||
|
() ->
|
||||||
|
new Person(
|
||||||
|
1L, null, "Doe", 25, "+1234567890", "john@example.com", validCategory));
|
||||||
|
assertTrue(exception.getMessage().contains("First name and last name cannot be null"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when last name is null")
|
||||||
|
void shouldThrowExceptionWhenLastNameIsNull() {
|
||||||
|
// Act & Assert
|
||||||
|
var exception =
|
||||||
|
assertThrows(
|
||||||
|
DomainException.class,
|
||||||
|
() ->
|
||||||
|
new Person(
|
||||||
|
1L, "John", null, 25, "+1234567890", "john@example.com", validCategory));
|
||||||
|
assertTrue(exception.getMessage().contains("First name and last name cannot be null"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when age is less than 18")
|
||||||
|
void shouldThrowExceptionWhenAgeIsLessThan18() {
|
||||||
|
// Act & Assert
|
||||||
|
var exception =
|
||||||
|
assertThrows(
|
||||||
|
DomainException.class,
|
||||||
|
() ->
|
||||||
|
new Person(
|
||||||
|
1L, "John", "Doe", 17, "+1234567890", "john@example.com", validCategory));
|
||||||
|
assertTrue(exception.getMessage().contains("Age cannot be less than 18"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when phone number is null")
|
||||||
|
void shouldThrowExceptionWhenPhoneIsNull() {
|
||||||
|
// Act & Assert
|
||||||
|
var exception =
|
||||||
|
assertThrows(
|
||||||
|
DomainException.class,
|
||||||
|
() -> new Person(1L, "John", "Doe", 25, null, "john@example.com", validCategory));
|
||||||
|
assertTrue(exception.getMessage().contains("Phone number cannot be null or empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when phone number is empty")
|
||||||
|
void shouldThrowExceptionWhenPhoneIsEmpty() {
|
||||||
|
// Act & Assert
|
||||||
|
var exception =
|
||||||
|
assertThrows(
|
||||||
|
DomainException.class,
|
||||||
|
() -> new Person(1L, "John", "Doe", 25, "", "john@example.com", validCategory));
|
||||||
|
assertTrue(exception.getMessage().contains("Phone number cannot be null or empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when email is null")
|
||||||
|
void shouldThrowExceptionWhenEmailIsNull() {
|
||||||
|
// Act & Assert
|
||||||
|
var exception =
|
||||||
|
assertThrows(
|
||||||
|
DomainException.class,
|
||||||
|
() -> new Person(1L, "John", "Doe", 25, "+1234567890", null, validCategory));
|
||||||
|
assertTrue(exception.getMessage().contains("Email cannot be null or empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when email is empty")
|
||||||
|
void shouldThrowExceptionWhenEmailIsEmpty() {
|
||||||
|
// Act & Assert
|
||||||
|
var exception =
|
||||||
|
assertThrows(
|
||||||
|
DomainException.class,
|
||||||
|
() -> new Person(1L, "John", "Doe", 25, "+1234567890", "", validCategory));
|
||||||
|
assertTrue(exception.getMessage().contains("Email cannot be null or empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when category is null")
|
||||||
|
void shouldThrowExceptionWhenCategoryIsNull() {
|
||||||
|
// Act & Assert
|
||||||
|
var exception =
|
||||||
|
assertThrows(
|
||||||
|
DomainException.class,
|
||||||
|
() -> new Person(1L, "John", "Doe", 25, "+1234567890", "john@example.com", null));
|
||||||
|
assertTrue(exception.getMessage().contains("Category cannot be null or empty"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 253 KiB |
@@ -0,0 +1,237 @@
|
|||||||
|
@startuml onion-architecture
|
||||||
|
|
||||||
|
skinparam packageStyle rectangle
|
||||||
|
skinparam shadowing false
|
||||||
|
skinparam defaultFontName Arial
|
||||||
|
skinparam defaultFontSize 11
|
||||||
|
skinparam classAttributeIconSize 0
|
||||||
|
skinparam ArrowColor #444444
|
||||||
|
skinparam ArrowThickness 1.2
|
||||||
|
|
||||||
|
skinparam package {
|
||||||
|
BackgroundColor<<domain>> #FFF9C4
|
||||||
|
BorderColor<<domain>> #F9A825
|
||||||
|
FontColor<<domain>> #5D4037
|
||||||
|
FontStyle<<domain>> bold
|
||||||
|
}
|
||||||
|
|
||||||
|
skinparam package {
|
||||||
|
BackgroundColor<<application>> #C8E6C9
|
||||||
|
BorderColor<<application>> #388E3C
|
||||||
|
FontColor<<application>> #1B5E20
|
||||||
|
FontStyle<<application>> bold
|
||||||
|
}
|
||||||
|
|
||||||
|
skinparam package {
|
||||||
|
BackgroundColor<<infrastructure>> #BBDEFB
|
||||||
|
BorderColor<<infrastructure>> #1976D2
|
||||||
|
FontColor<<infrastructure>> #0D47A1
|
||||||
|
FontStyle<<infrastructure>> bold
|
||||||
|
}
|
||||||
|
|
||||||
|
' ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
' DOMAIN LAYER (innermost)
|
||||||
|
' ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
package "Domain Layer" <<domain>> {
|
||||||
|
|
||||||
|
package "model" {
|
||||||
|
class Person {
|
||||||
|
- id : Long
|
||||||
|
- firstName : String
|
||||||
|
- lastName : String
|
||||||
|
- age : int
|
||||||
|
- phoneNumber : String
|
||||||
|
- email : String
|
||||||
|
- category : Category
|
||||||
|
+ getId() : Long
|
||||||
|
+ getFirstName() : String
|
||||||
|
+ getLastName() : String
|
||||||
|
+ getAge() : int
|
||||||
|
+ getPhoneNumber() : String
|
||||||
|
+ getEmail() : String
|
||||||
|
+ getCategory() : Category
|
||||||
|
}
|
||||||
|
|
||||||
|
class Category {
|
||||||
|
- id : Long
|
||||||
|
- type : String
|
||||||
|
+ getId() : Long
|
||||||
|
+ getType() : String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "repository" {
|
||||||
|
interface PersonRepository {
|
||||||
|
+ findById(id : Long) : Optional<Person>
|
||||||
|
+ findByFirstName(firstName : String) : Optional<Person>
|
||||||
|
+ findByLastName(lastName : String) : Optional<Person>
|
||||||
|
+ findAll() : Optional<List<Person>>
|
||||||
|
+ save(person : Person) : Person
|
||||||
|
+ deleteById(id : Long) : boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "exception" {
|
||||||
|
class DomainException {
|
||||||
|
+ DomainException(message : String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
' ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
' APPLICATION LAYER
|
||||||
|
' ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
package "Application Layer" <<application>> {
|
||||||
|
|
||||||
|
package "usecase" {
|
||||||
|
class SavePersonUseCase {
|
||||||
|
- repository : PersonRepository
|
||||||
|
+ SavePersonUseCase(repository : PersonRepository)
|
||||||
|
+ execute(command : SavePersonCommand) : PersonResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetPersonUseCase {
|
||||||
|
- repository : PersonRepository
|
||||||
|
+ GetPersonUseCase(repository : PersonRepository)
|
||||||
|
+ execute(id : Long) : PersonResponse
|
||||||
|
+ executeAll() : List<PersonResponse>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "dto" {
|
||||||
|
class SavePersonCommand <<record>> {
|
||||||
|
firstName : String
|
||||||
|
lastName : String
|
||||||
|
age : int
|
||||||
|
phoneNumber : String
|
||||||
|
email : String
|
||||||
|
address : String
|
||||||
|
categoryId : Long
|
||||||
|
categoryType : String
|
||||||
|
}
|
||||||
|
|
||||||
|
class PersonResponse <<record>> {
|
||||||
|
id : Long
|
||||||
|
firstName : String
|
||||||
|
lastName : String
|
||||||
|
age : int
|
||||||
|
phoneNumber : String
|
||||||
|
email : String
|
||||||
|
categoryId : Long
|
||||||
|
categoryType : String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
' ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
' INFRASTRUCTURE LAYER (outermost)
|
||||||
|
' ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
package "Infrastructure Layer" <<infrastructure>> {
|
||||||
|
|
||||||
|
package "web" {
|
||||||
|
class PersonController {
|
||||||
|
- savePersonUseCase : SavePersonUseCase
|
||||||
|
- getPersonUseCase : GetPersonUseCase
|
||||||
|
+ getPerson(id : Long) : ResponseEntity<PersonResponse>
|
||||||
|
+ getAllPersons() : ResponseEntity<List<PersonResponse>>
|
||||||
|
+ savePerson(command : SavePersonCommand) : ResponseEntity<PersonResponse>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "persistence" {
|
||||||
|
class PersonRepositoryAdapter {
|
||||||
|
- repository : SpringDataPersonRepository
|
||||||
|
+ findById(id : Long) : Optional<Person>
|
||||||
|
+ findByFirstName(firstName : String) : Optional<Person>
|
||||||
|
+ findByLastName(lastName : String) : Optional<Person>
|
||||||
|
+ findAll() : Optional<List<Person>>
|
||||||
|
+ save(person : Person) : Person
|
||||||
|
+ deleteById(id : Long) : boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SpringDataPersonRepository {
|
||||||
|
+ findByFirstName(firstName : String) : Optional<JpaPersonEntity>
|
||||||
|
+ findByLastName(lastName : String) : Optional<JpaPersonEntity>
|
||||||
|
}
|
||||||
|
|
||||||
|
class JpaPersonEntity {
|
||||||
|
- id : Long
|
||||||
|
- firstName : String
|
||||||
|
- lastName : String
|
||||||
|
- age : int
|
||||||
|
- phoneNumber : String
|
||||||
|
- email : String
|
||||||
|
- category : JpaCategoryEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
class JpaCategoryEntity {
|
||||||
|
- id : Long
|
||||||
|
- type : String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "config" {
|
||||||
|
class ApplicationConfig <<@Configuration>> {
|
||||||
|
+ savePersonUseCase(repo : PersonRepository) : SavePersonUseCase
|
||||||
|
+ getPersonUseCase(repo : PersonRepository) : GetPersonUseCase
|
||||||
|
}
|
||||||
|
|
||||||
|
class Application <<@SpringBootApplication>> {
|
||||||
|
+ main(args : String[])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
' ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
' DOMAIN: internal relationships
|
||||||
|
' ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Person "1" *-- "1" Category : has
|
||||||
|
Person ..> DomainException : <<throws>>
|
||||||
|
Category ..> DomainException : <<throws>>
|
||||||
|
|
||||||
|
' ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
' APPLICATION → DOMAIN
|
||||||
|
' ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
SavePersonUseCase --> PersonRepository : uses
|
||||||
|
SavePersonUseCase ..> Person : creates
|
||||||
|
SavePersonUseCase ..> Category : creates
|
||||||
|
SavePersonUseCase ..> PersonResponse : returns
|
||||||
|
SavePersonUseCase ..> SavePersonCommand : receives
|
||||||
|
|
||||||
|
GetPersonUseCase --> PersonRepository : uses
|
||||||
|
GetPersonUseCase ..> PersonResponse : returns
|
||||||
|
|
||||||
|
' ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
' INFRASTRUCTURE → APPLICATION / DOMAIN
|
||||||
|
' ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
PersonController --> SavePersonUseCase : delegates to
|
||||||
|
PersonController --> GetPersonUseCase : delegates to
|
||||||
|
PersonController ..> DomainException : catches
|
||||||
|
|
||||||
|
PersonRepositoryAdapter ..|> PersonRepository : implements
|
||||||
|
PersonRepositoryAdapter --> SpringDataPersonRepository : uses
|
||||||
|
PersonRepositoryAdapter ..> JpaPersonEntity : maps
|
||||||
|
PersonRepositoryAdapter ..> JpaCategoryEntity : maps
|
||||||
|
PersonRepositoryAdapter ..> Person : maps to / from
|
||||||
|
PersonRepositoryAdapter ..> Category : maps to / from
|
||||||
|
|
||||||
|
SpringDataPersonRepository --> JpaPersonEntity : manages
|
||||||
|
JpaPersonEntity "1" *-- "1" JpaCategoryEntity : contains
|
||||||
|
|
||||||
|
ApplicationConfig ..> SavePersonUseCase : <<@Bean>>
|
||||||
|
ApplicationConfig ..> GetPersonUseCase : <<@Bean>>
|
||||||
|
ApplicationConfig --> PersonRepository : injects
|
||||||
|
|
||||||
|
legend right
|
||||||
|
<b>Layer dependency rule</b>
|
||||||
|
Infrastructure → Application → Domain
|
||||||
|
Outer layers depend on inner layers.
|
||||||
|
Domain has NO external dependencies.
|
||||||
|
----
|
||||||
|
<color:#F9A825>■</color> Domain Layer
|
||||||
|
<color:#388E3C>■</color> Application Layer
|
||||||
|
<color:#1976D2>■</color> Infrastructure Layer
|
||||||
|
endlegend
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "79aca374-7080-46c1-938f-89b327a84d6a",
|
||||||
|
"name": "onion-architecture",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||||
|
"_exporter_id": "51221021",
|
||||||
|
"_collection_link": "https://go.postman.co/collection/51221021-79aca374-7080-46c1-938f-89b327a84d6a?source=collection_link"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "savePerson",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\r\n \"number1\": 5,\r\n \"number2\": 3,\r\n \"operation\": \"multiply\"\r\n }"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "http://localhost:8080/api/persons",
|
||||||
|
"protocol": "http",
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
],
|
||||||
|
"port": "8080",
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"persons"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "getPersons",
|
||||||
|
"protocolProfileBehavior": {
|
||||||
|
"disableBodyPruning": true
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\r\n \"number1\": 5,\r\n \"number2\": 3,\r\n \"operation\": \"multiply\"\r\n }"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "http://localhost:8080/api/persons",
|
||||||
|
"protocol": "http",
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
],
|
||||||
|
"port": "8080",
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"persons"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "getPersonById",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "http://localhost:8080/api/persons/1",
|
||||||
|
"protocol": "http",
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
],
|
||||||
|
"port": "8080",
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"persons",
|
||||||
|
"1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
|
||||||
|
The MIT License
|
||||||
|
Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.iluwatar</groupId>
|
||||||
|
<artifactId>onion-architecture</artifactId>
|
||||||
|
<version>1.26.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<artifactId>infrastructure</artifactId>
|
||||||
|
<name>Infrastructure</name>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.iluwatar</groupId>
|
||||||
|
<artifactId>domain</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.iluwatar</groupId>
|
||||||
|
<artifactId>application</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- REST Controller -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JPA + Spring Data JPA-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- H2 Database -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Boot Test -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.infrastructure;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class Application {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(Application.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.infrastructure.config;
|
||||||
|
|
||||||
|
import com.iluwatar.onion.application.usecase.GetPersonUseCase;
|
||||||
|
import com.iluwatar.onion.application.usecase.SavePersonUseCase;
|
||||||
|
import com.iluwatar.onion.domain.repository.PersonRepository;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class ApplicationConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SavePersonUseCase savePersonUseCase(PersonRepository repository) {
|
||||||
|
return new SavePersonUseCase(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GetPersonUseCase getPersonUseCase(PersonRepository repository) {
|
||||||
|
return new GetPersonUseCase(repository);
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.infrastructure.persistence;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "category")
|
||||||
|
public class JpaCategoryEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
public JpaCategoryEntity() {}
|
||||||
|
|
||||||
|
public JpaCategoryEntity(Long id, String type) {
|
||||||
|
this.id = id;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.infrastructure.persistence;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "person")
|
||||||
|
public class JpaPersonEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String firstName;
|
||||||
|
private String lastName;
|
||||||
|
private int age;
|
||||||
|
private String phoneNumber;
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@ManyToOne private JpaCategoryEntity category;
|
||||||
|
|
||||||
|
public JpaPersonEntity() {}
|
||||||
|
|
||||||
|
public JpaPersonEntity(
|
||||||
|
Long id,
|
||||||
|
String firstName,
|
||||||
|
String lastName,
|
||||||
|
int age,
|
||||||
|
String phoneNumber,
|
||||||
|
String email,
|
||||||
|
JpaCategoryEntity category) {
|
||||||
|
this.id = id;
|
||||||
|
this.firstName = firstName;
|
||||||
|
this.lastName = lastName;
|
||||||
|
this.age = age;
|
||||||
|
this.phoneNumber = phoneNumber;
|
||||||
|
this.email = email;
|
||||||
|
this.category = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFirstName() {
|
||||||
|
return firstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastName() {
|
||||||
|
return lastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAge() {
|
||||||
|
return age;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhoneNumber() {
|
||||||
|
return phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JpaCategoryEntity getCategory() {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.infrastructure.persistence;
|
||||||
|
|
||||||
|
import com.iluwatar.onion.domain.model.Category;
|
||||||
|
import com.iluwatar.onion.domain.model.Person;
|
||||||
|
import com.iluwatar.onion.domain.repository.PersonRepository;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public class PersonRepositoryAdapter implements PersonRepository {
|
||||||
|
|
||||||
|
private final SpringDataPersonRepository repository;
|
||||||
|
|
||||||
|
public PersonRepositoryAdapter(SpringDataPersonRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Person> findById(Long id) {
|
||||||
|
return repository.findById(id).map(this::mapToDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Person> findByFirstName(String firstName) {
|
||||||
|
return repository.findByFirstName(firstName).map(this::mapToDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Person> findByLastName(String lastName) {
|
||||||
|
return repository.findByLastName(lastName).map(this::mapToDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<List<Person>> findAll() {
|
||||||
|
return repository.findAll().stream()
|
||||||
|
.map(this::mapToDomain)
|
||||||
|
.collect(Collectors.collectingAndThen(Collectors.toList(), Optional::of));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Person save(Person person) {
|
||||||
|
JpaPersonEntity entity = mapToEntity(person);
|
||||||
|
JpaPersonEntity savedEntity = repository.save(entity);
|
||||||
|
return mapToDomain(savedEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deleteById(Long id) {
|
||||||
|
repository.deleteById(id);
|
||||||
|
return repository.findById(id).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JpaPersonEntity mapToEntity(Person person) {
|
||||||
|
return new JpaPersonEntity(
|
||||||
|
person.getId(),
|
||||||
|
person.getFirstName(),
|
||||||
|
person.getLastName(),
|
||||||
|
person.getAge(),
|
||||||
|
person.getPhoneNumber(),
|
||||||
|
person.getEmail(),
|
||||||
|
new JpaCategoryEntity(person.getCategory().getId(), person.getCategory().getType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Person mapToDomain(JpaPersonEntity entity) {
|
||||||
|
return new Person(
|
||||||
|
entity.getId(),
|
||||||
|
entity.getFirstName(),
|
||||||
|
entity.getLastName(),
|
||||||
|
entity.getAge(),
|
||||||
|
entity.getPhoneNumber(),
|
||||||
|
entity.getEmail(),
|
||||||
|
new Category(entity.getCategory().getId(), entity.getCategory().getType()));
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.infrastructure.persistence;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface SpringDataPersonRepository extends JpaRepository<JpaPersonEntity, Long> {
|
||||||
|
|
||||||
|
Optional<JpaPersonEntity> findByFirstName(String firstName);
|
||||||
|
|
||||||
|
Optional<JpaPersonEntity> findByLastName(String lastName);
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.infrastructure.web;
|
||||||
|
|
||||||
|
import com.iluwatar.onion.application.dto.PersonResponse;
|
||||||
|
import com.iluwatar.onion.application.dto.SavePersonCommand;
|
||||||
|
import com.iluwatar.onion.application.usecase.GetPersonUseCase;
|
||||||
|
import com.iluwatar.onion.application.usecase.SavePersonUseCase;
|
||||||
|
import com.iluwatar.onion.domain.exception.DomainException;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api")
|
||||||
|
public class PersonController {
|
||||||
|
|
||||||
|
private final SavePersonUseCase savePersonUseCase;
|
||||||
|
private final GetPersonUseCase getPersonUseCase;
|
||||||
|
|
||||||
|
public PersonController(SavePersonUseCase savePersonUseCase, GetPersonUseCase getPersonUseCase) {
|
||||||
|
this.savePersonUseCase = savePersonUseCase;
|
||||||
|
this.getPersonUseCase = getPersonUseCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/persons/{id}")
|
||||||
|
public ResponseEntity<PersonResponse> getPerson(@PathVariable("id") Long id) {
|
||||||
|
var person = getPersonUseCase.execute(id);
|
||||||
|
return ResponseEntity.ok(person);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/persons")
|
||||||
|
public ResponseEntity<List<PersonResponse>> getAllPersons() {
|
||||||
|
var persons = getPersonUseCase.executeAll();
|
||||||
|
return ResponseEntity.ok(persons);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/persons")
|
||||||
|
public ResponseEntity<PersonResponse> savePerson(@RequestBody SavePersonCommand command) {
|
||||||
|
try {
|
||||||
|
var savedPerson = savePersonUseCase.execute(command);
|
||||||
|
return ResponseEntity.status(HttpStatus.OK).body(savedPerson);
|
||||||
|
} catch (DomainException e) {
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# You can also specify the server port in the application.properties file.
|
||||||
|
# Uncomment the following line and set the desired port number to change
|
||||||
|
# the default port (8080) to a different value.
|
||||||
|
#server.port=8080
|
||||||
|
|
||||||
|
spring.datasource.url=jdbc:h2:mem:testdb
|
||||||
|
spring.datasource.driverClassName=org.h2.Driver
|
||||||
|
spring.datasource.username=sa
|
||||||
|
spring.datasource.password=password
|
||||||
|
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||||
|
|
||||||
|
# Force Spring Boot to always run SQL initialization scripts on startup
|
||||||
|
spring.sql.init.mode=always
|
||||||
|
|
||||||
|
# If using Spring Data JPA / Hibernate, defer data insertion until AFTER entities are generated
|
||||||
|
spring.jpa.defer-datasource-initialization=true
|
||||||
|
|
||||||
|
# Keep Hibernate's DDL generation active (or set to 'update')
|
||||||
|
spring.jpa.hibernate.ddl-auto=create-drop
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
INSERT INTO category (id, type) VALUES (1, 'Teenage');
|
||||||
|
INSERT INTO category (id, type) VALUES (2, 'Young Adult');
|
||||||
|
INSERT INTO category (id, type) VALUES (3, 'Adult');
|
||||||
|
INSERT INTO category (id, type) VALUES (4, 'Senior');
|
||||||
+332
@@ -0,0 +1,332 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.infrastructure.persistence;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import com.iluwatar.onion.domain.model.Category;
|
||||||
|
import com.iluwatar.onion.domain.model.Person;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class PersonRepositoryAdapterTest {
|
||||||
|
|
||||||
|
@Mock private SpringDataPersonRepository springDataRepository;
|
||||||
|
|
||||||
|
@Captor private ArgumentCaptor<JpaPersonEntity> entityCaptor;
|
||||||
|
|
||||||
|
private PersonRepositoryAdapter personRepositoryAdapter;
|
||||||
|
private JpaPersonEntity jpaPersonEntity;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
personRepositoryAdapter = new PersonRepositoryAdapter(springDataRepository);
|
||||||
|
|
||||||
|
// Setup test data
|
||||||
|
var jpaCategory = new JpaCategoryEntity(1L, "Professional");
|
||||||
|
jpaPersonEntity =
|
||||||
|
new JpaPersonEntity(
|
||||||
|
1L, "John", "Doe", 25, "+1234567890", "john.doe@example.com", jpaCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Find By ID")
|
||||||
|
class FindById {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return Person when entity exists")
|
||||||
|
void shouldReturnPersonWhenEntityExists() {
|
||||||
|
// Arrange
|
||||||
|
when(springDataRepository.findById(1L)).thenReturn(Optional.of(jpaPersonEntity));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = personRepositoryAdapter.findById(1L);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(result.isPresent());
|
||||||
|
Person person = result.get();
|
||||||
|
assertEquals(1L, person.getId());
|
||||||
|
assertEquals("John", person.getFirstName());
|
||||||
|
assertEquals("Doe", person.getLastName());
|
||||||
|
assertEquals(25, person.getAge());
|
||||||
|
assertEquals("+1234567890", person.getPhoneNumber());
|
||||||
|
assertEquals("john.doe@example.com", person.getEmail());
|
||||||
|
assertEquals(1L, person.getCategory().getId());
|
||||||
|
assertEquals("Professional", person.getCategory().getType());
|
||||||
|
|
||||||
|
verify(springDataRepository, times(1)).findById(1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return empty Optional when entity not found")
|
||||||
|
void shouldReturnEmptyWhenEntityNotFound() {
|
||||||
|
// Arrange
|
||||||
|
when(springDataRepository.findById(999L)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = personRepositoryAdapter.findById(999L);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(result.isEmpty());
|
||||||
|
verify(springDataRepository, times(1)).findById(999L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Find By First Name")
|
||||||
|
class FindByFirstName {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return Person when entity with first name exists")
|
||||||
|
void shouldReturnPersonWhenEntityExists() {
|
||||||
|
// Arrange
|
||||||
|
when(springDataRepository.findByFirstName("John")).thenReturn(Optional.of(jpaPersonEntity));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = personRepositoryAdapter.findByFirstName("John");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(result.isPresent());
|
||||||
|
assertEquals("John", result.get().getFirstName());
|
||||||
|
verify(springDataRepository, times(1)).findByFirstName("John");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return empty Optional when no entity found")
|
||||||
|
void shouldReturnEmptyWhenNotFound() {
|
||||||
|
// Arrange
|
||||||
|
when(springDataRepository.findByFirstName("Unknown")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = personRepositoryAdapter.findByFirstName("Unknown");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(result.isEmpty());
|
||||||
|
verify(springDataRepository, times(1)).findByFirstName("Unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Find By Last Name")
|
||||||
|
class FindByLastName {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return Person when entity with last name exists")
|
||||||
|
void shouldReturnPersonWhenEntityExists() {
|
||||||
|
// Arrange
|
||||||
|
when(springDataRepository.findByLastName("Doe")).thenReturn(Optional.of(jpaPersonEntity));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = personRepositoryAdapter.findByLastName("Doe");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(result.isPresent());
|
||||||
|
assertEquals("Doe", result.get().getLastName());
|
||||||
|
verify(springDataRepository, times(1)).findByLastName("Doe");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Find All")
|
||||||
|
class FindAll {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return list of Persons when entities exist")
|
||||||
|
void shouldReturnListOfPersons() {
|
||||||
|
// Arrange
|
||||||
|
var category2 = new JpaCategoryEntity(2L, "Personal");
|
||||||
|
var person2 =
|
||||||
|
new JpaPersonEntity(
|
||||||
|
2L, "Jane", "Smith", 30, "+9876543210", "jane@example.com", category2);
|
||||||
|
|
||||||
|
when(springDataRepository.findAll()).thenReturn(List.of(jpaPersonEntity, person2));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = personRepositoryAdapter.findAll();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(result.isPresent());
|
||||||
|
var persons = result.get();
|
||||||
|
assertEquals(2, persons.size());
|
||||||
|
assertEquals("John", persons.get(0).getFirstName());
|
||||||
|
assertEquals("Jane", persons.get(1).getFirstName());
|
||||||
|
|
||||||
|
verify(springDataRepository, times(1)).findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return empty list when no entities exist")
|
||||||
|
void shouldReturnEmptyListWhenNoEntities() {
|
||||||
|
// Arrange
|
||||||
|
when(springDataRepository.findAll()).thenReturn(List.of());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = personRepositoryAdapter.findAll();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(result.isPresent());
|
||||||
|
assertTrue(result.get().isEmpty());
|
||||||
|
verify(springDataRepository, times(1)).findAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Save")
|
||||||
|
class Save {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should save person and return domain model")
|
||||||
|
void shouldSavePersonAndReturnDomainModel() {
|
||||||
|
// Arrange
|
||||||
|
var personToSave =
|
||||||
|
new Person(
|
||||||
|
null, // New person without ID
|
||||||
|
"Jane",
|
||||||
|
"Smith",
|
||||||
|
28,
|
||||||
|
"+1111111111",
|
||||||
|
"jane@example.com",
|
||||||
|
new Category(2L, "Personal"));
|
||||||
|
|
||||||
|
var savedCategory = new JpaCategoryEntity(2L, "Personal");
|
||||||
|
var savedEntity =
|
||||||
|
new JpaPersonEntity(
|
||||||
|
100L, // ID assigned by database
|
||||||
|
"Jane",
|
||||||
|
"Smith",
|
||||||
|
28,
|
||||||
|
"+1111111111",
|
||||||
|
"jane@example.com",
|
||||||
|
savedCategory);
|
||||||
|
|
||||||
|
when(springDataRepository.save(any(JpaPersonEntity.class))).thenReturn(savedEntity);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = personRepositoryAdapter.save(personToSave);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(100L, result.getId());
|
||||||
|
assertEquals("Jane", result.getFirstName());
|
||||||
|
assertEquals("Smith", result.getLastName());
|
||||||
|
assertEquals(28, result.getAge());
|
||||||
|
|
||||||
|
verify(springDataRepository, times(1)).save(any(JpaPersonEntity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should correctly map domain model to JPA entity")
|
||||||
|
void shouldCorrectlyMapDomainToEntity() {
|
||||||
|
// Arrange
|
||||||
|
var personToSave =
|
||||||
|
new Person(
|
||||||
|
null,
|
||||||
|
"Bob",
|
||||||
|
"Johnson",
|
||||||
|
35,
|
||||||
|
"+2222222222",
|
||||||
|
"bob@example.com",
|
||||||
|
new Category(3L, "Business"));
|
||||||
|
|
||||||
|
var savedEntity =
|
||||||
|
new JpaPersonEntity(
|
||||||
|
200L,
|
||||||
|
"Bob",
|
||||||
|
"Johnson",
|
||||||
|
35,
|
||||||
|
"+2222222222",
|
||||||
|
"bob@example.com",
|
||||||
|
new JpaCategoryEntity(3L, "Business"));
|
||||||
|
|
||||||
|
when(springDataRepository.save(any(JpaPersonEntity.class))).thenReturn(savedEntity);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
personRepositoryAdapter.save(personToSave);
|
||||||
|
|
||||||
|
// Assert - Verify what was passed to Spring Data repository
|
||||||
|
verify(springDataRepository).save(entityCaptor.capture());
|
||||||
|
JpaPersonEntity capturedEntity = entityCaptor.getValue();
|
||||||
|
|
||||||
|
assertEquals("Bob", capturedEntity.getFirstName());
|
||||||
|
assertEquals("Johnson", capturedEntity.getLastName());
|
||||||
|
assertEquals(35, capturedEntity.getAge());
|
||||||
|
assertEquals("+2222222222", capturedEntity.getPhoneNumber());
|
||||||
|
assertEquals("bob@example.com", capturedEntity.getEmail());
|
||||||
|
assertEquals(3L, capturedEntity.getCategory().getId());
|
||||||
|
assertEquals("Business", capturedEntity.getCategory().getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Delete By ID")
|
||||||
|
class DeleteById {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return true when person is successfully deleted")
|
||||||
|
void shouldReturnTrueWhenDeleted() {
|
||||||
|
// Arrange
|
||||||
|
doNothing().when(springDataRepository).deleteById(1L);
|
||||||
|
when(springDataRepository.findById(1L)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
boolean result = personRepositoryAdapter.deleteById(1L);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(result);
|
||||||
|
verify(springDataRepository, times(1)).deleteById(1L);
|
||||||
|
verify(springDataRepository, times(1)).findById(1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return false when person still exists after deletion")
|
||||||
|
void shouldReturnFalseWhenStillExists() {
|
||||||
|
// Arrange
|
||||||
|
doNothing().when(springDataRepository).deleteById(1L);
|
||||||
|
when(springDataRepository.findById(1L)).thenReturn(Optional.of(jpaPersonEntity));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
boolean result = personRepositoryAdapter.deleteById(1L);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertFalse(result);
|
||||||
|
verify(springDataRepository, times(1)).deleteById(1L);
|
||||||
|
verify(springDataRepository, times(1)).findById(1L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+348
@@ -0,0 +1,348 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
* *
|
||||||
|
* * The MIT License
|
||||||
|
* * Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
* *
|
||||||
|
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* * of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* * in the Software without restriction, including without limitation the rights
|
||||||
|
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* * copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* * furnished to do so, subject to the following conditions:
|
||||||
|
* *
|
||||||
|
* * The above copyright notice and this permission notice shall be included in
|
||||||
|
* * all copies or substantial portions of the Software.
|
||||||
|
* *
|
||||||
|
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* * THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.iluwatar.onion.infrastructure.web;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.iluwatar.onion.application.dto.PersonResponse;
|
||||||
|
import com.iluwatar.onion.application.dto.SavePersonCommand;
|
||||||
|
import com.iluwatar.onion.application.usecase.GetPersonUseCase;
|
||||||
|
import com.iluwatar.onion.application.usecase.SavePersonUseCase;
|
||||||
|
import com.iluwatar.onion.domain.exception.DomainException;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
@WebMvcTest(PersonController.class)
|
||||||
|
class PersonControllerTest {
|
||||||
|
|
||||||
|
@Autowired private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Autowired private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@MockitoBean private SavePersonUseCase savePersonUseCase;
|
||||||
|
|
||||||
|
@MockitoBean private GetPersonUseCase getPersonUseCase;
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("GET /api/persons/{id}")
|
||||||
|
class GetPersonById {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return person when person exists")
|
||||||
|
void shouldReturnPersonWhenExists() throws Exception {
|
||||||
|
// Arrange
|
||||||
|
var response =
|
||||||
|
new PersonResponse(
|
||||||
|
1L, "John", "Doe", 25, "+1234567890", "john.doe@example.com", 1L, "Professional");
|
||||||
|
|
||||||
|
when(getPersonUseCase.execute(1L)).thenReturn(response);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
mockMvc
|
||||||
|
.perform(get("/api/persons/1").contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(jsonPath("$.id").value(1))
|
||||||
|
.andExpect(jsonPath("$.firstName").value("John"))
|
||||||
|
.andExpect(jsonPath("$.lastName").value("Doe"))
|
||||||
|
.andExpect(jsonPath("$.age").value(25))
|
||||||
|
.andExpect(jsonPath("$.phoneNumber").value("+1234567890"))
|
||||||
|
.andExpect(jsonPath("$.email").value("john.doe@example.com"))
|
||||||
|
.andExpect(jsonPath("$.categoryId").value(1))
|
||||||
|
.andExpect(jsonPath("$.categoryType").value("Professional"));
|
||||||
|
|
||||||
|
verify(getPersonUseCase, times(1)).execute(1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle different person IDs")
|
||||||
|
void shouldHandleDifferentPersonIds() throws Exception {
|
||||||
|
// Arrange
|
||||||
|
var response =
|
||||||
|
new PersonResponse(
|
||||||
|
42L, "Jane", "Smith", 30, "+9876543210", "jane@example.com", 2L, "Personal");
|
||||||
|
|
||||||
|
when(getPersonUseCase.execute(42L)).thenReturn(response);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
mockMvc
|
||||||
|
.perform(get("/api/persons/42").contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.id").value(42))
|
||||||
|
.andExpect(jsonPath("$.firstName").value("Jane"));
|
||||||
|
|
||||||
|
verify(getPersonUseCase, times(1)).execute(42L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("GET /api/persons")
|
||||||
|
class GetAllPersons {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return list of persons")
|
||||||
|
void shouldReturnListOfPersons() throws Exception {
|
||||||
|
// Arrange
|
||||||
|
var person1 =
|
||||||
|
new PersonResponse(
|
||||||
|
1L, "John", "Doe", 25, "+1234567890", "john@example.com", 1L, "Professional");
|
||||||
|
var person2 =
|
||||||
|
new PersonResponse(
|
||||||
|
2L, "Jane", "Smith", 30, "+9876543210", "jane@example.com", 2L, "Personal");
|
||||||
|
|
||||||
|
var persons = List.of(person1, person2);
|
||||||
|
when(getPersonUseCase.executeAll()).thenReturn(persons);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
mockMvc
|
||||||
|
.perform(get("/api/persons").contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(jsonPath("$", hasSize(2)))
|
||||||
|
.andExpect(jsonPath("$[0].id").value(1))
|
||||||
|
.andExpect(jsonPath("$[0].firstName").value("John"))
|
||||||
|
.andExpect(jsonPath("$[1].id").value(2))
|
||||||
|
.andExpect(jsonPath("$[1].firstName").value("Jane"));
|
||||||
|
|
||||||
|
verify(getPersonUseCase, times(1)).executeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return empty list when no persons exist")
|
||||||
|
void shouldReturnEmptyListWhenNoPersons() throws Exception {
|
||||||
|
// Arrange
|
||||||
|
when(getPersonUseCase.executeAll()).thenReturn(List.of());
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
mockMvc
|
||||||
|
.perform(get("/api/persons").contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(jsonPath("$", hasSize(0)));
|
||||||
|
|
||||||
|
verify(getPersonUseCase, times(1)).executeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle large list of persons")
|
||||||
|
void shouldHandleLargeListOfPersons() throws Exception {
|
||||||
|
// Arrange
|
||||||
|
var persons =
|
||||||
|
List.of(
|
||||||
|
new PersonResponse(1L, "Person1", "Last1", 25, "+1", "p1@example.com", 1L, "Cat1"),
|
||||||
|
new PersonResponse(2L, "Person2", "Last2", 26, "+2", "p2@example.com", 1L, "Cat1"),
|
||||||
|
new PersonResponse(3L, "Person3", "Last3", 27, "+3", "p3@example.com", 1L, "Cat1"));
|
||||||
|
|
||||||
|
when(getPersonUseCase.executeAll()).thenReturn(persons);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
mockMvc
|
||||||
|
.perform(get("/api/persons").contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$", hasSize(3)));
|
||||||
|
|
||||||
|
verify(getPersonUseCase, times(1)).executeAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("POST /api/persons")
|
||||||
|
class SavePerson {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create person with valid data")
|
||||||
|
void shouldCreatePersonWithValidData() throws Exception {
|
||||||
|
// Arrange
|
||||||
|
var command =
|
||||||
|
new SavePersonCommand(
|
||||||
|
"John",
|
||||||
|
"Doe",
|
||||||
|
25,
|
||||||
|
"+1234567890",
|
||||||
|
"john.doe@example.com",
|
||||||
|
"123 Main St",
|
||||||
|
1L,
|
||||||
|
"Professional");
|
||||||
|
|
||||||
|
var response =
|
||||||
|
new PersonResponse(
|
||||||
|
1L, "John", "Doe", 25, "+1234567890", "john.doe@example.com", 1L, "Professional");
|
||||||
|
|
||||||
|
when(savePersonUseCase.execute(any(SavePersonCommand.class))).thenReturn(response);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
mockMvc
|
||||||
|
.perform(
|
||||||
|
post("/api/persons")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(command)))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(jsonPath("$.id").value(1))
|
||||||
|
.andExpect(jsonPath("$.firstName").value("John"))
|
||||||
|
.andExpect(jsonPath("$.lastName").value("Doe"))
|
||||||
|
.andExpect(jsonPath("$.age").value(25))
|
||||||
|
.andExpect(jsonPath("$.phoneNumber").value("+1234567890"))
|
||||||
|
.andExpect(jsonPath("$.email").value("john.doe@example.com"))
|
||||||
|
.andExpect(jsonPath("$.categoryId").value(1))
|
||||||
|
.andExpect(jsonPath("$.categoryType").value("Professional"));
|
||||||
|
|
||||||
|
verify(savePersonUseCase, times(1)).execute(any(SavePersonCommand.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle validation errors from use case")
|
||||||
|
void shouldPropagateValidationErrors() throws Exception {
|
||||||
|
// Arrange
|
||||||
|
var command =
|
||||||
|
new SavePersonCommand(
|
||||||
|
"Young",
|
||||||
|
"Person",
|
||||||
|
17, // Invalid age
|
||||||
|
"+1234567890",
|
||||||
|
"young@example.com",
|
||||||
|
"123 Main St",
|
||||||
|
1L,
|
||||||
|
"Student");
|
||||||
|
|
||||||
|
when(savePersonUseCase.execute(any(SavePersonCommand.class)))
|
||||||
|
.thenThrow(new DomainException("Age cannot be less than 18"));
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
mockMvc
|
||||||
|
.perform(
|
||||||
|
post("/api/persons")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(command)))
|
||||||
|
.andExpect(status().is4xxClientError());
|
||||||
|
|
||||||
|
verify(savePersonUseCase, times(1)).execute(any(SavePersonCommand.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle malformed JSON")
|
||||||
|
void shouldHandleMalformedJson() throws Exception {
|
||||||
|
// Act & Assert
|
||||||
|
mockMvc
|
||||||
|
.perform(
|
||||||
|
post("/api/persons")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{invalid json}"))
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
|
|
||||||
|
verify(savePersonUseCase, never()).execute(any(SavePersonCommand.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should accept all required fields in command")
|
||||||
|
void shouldAcceptAllRequiredFields() throws Exception {
|
||||||
|
// Arrange
|
||||||
|
var command =
|
||||||
|
new SavePersonCommand(
|
||||||
|
"Complete",
|
||||||
|
"Person",
|
||||||
|
30,
|
||||||
|
"+1111111111",
|
||||||
|
"complete@example.com",
|
||||||
|
"456 Oak Ave",
|
||||||
|
5L,
|
||||||
|
"Business");
|
||||||
|
|
||||||
|
var response =
|
||||||
|
new PersonResponse(
|
||||||
|
10L, "Complete", "Person", 30, "+1111111111", "complete@example.com", 5L, "Business");
|
||||||
|
|
||||||
|
when(savePersonUseCase.execute(any(SavePersonCommand.class))).thenReturn(response);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
mockMvc
|
||||||
|
.perform(
|
||||||
|
post("/api/persons")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(command)))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.categoryType").value("Business"));
|
||||||
|
|
||||||
|
verify(savePersonUseCase, times(1)).execute(any(SavePersonCommand.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Controller Integration")
|
||||||
|
class ControllerIntegration {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle multiple requests sequentially")
|
||||||
|
void shouldHandleMultipleRequestsSequentially() throws Exception {
|
||||||
|
// Arrange
|
||||||
|
var getResponse =
|
||||||
|
new PersonResponse(
|
||||||
|
1L, "John", "Doe", 25, "+1234567890", "john@example.com", 1L, "Professional");
|
||||||
|
|
||||||
|
var savePersonCommand =
|
||||||
|
new SavePersonCommand(
|
||||||
|
"Jane", "Smith", 30, "+9876543210", "jane@example.com", "456 Oak", 2L, "Personal");
|
||||||
|
|
||||||
|
var savePersonResponse =
|
||||||
|
new PersonResponse(
|
||||||
|
2L, "Jane", "Smith", 30, "+9876543210", "jane@example.com", 2L, "Personal");
|
||||||
|
|
||||||
|
when(getPersonUseCase.execute(1L)).thenReturn(getResponse);
|
||||||
|
when(savePersonUseCase.execute(any(SavePersonCommand.class))).thenReturn(savePersonResponse);
|
||||||
|
|
||||||
|
// Act & Assert - GET request
|
||||||
|
mockMvc
|
||||||
|
.perform(get("/api/persons/1"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.firstName").value("John"));
|
||||||
|
|
||||||
|
// Act & Assert - POST request
|
||||||
|
mockMvc
|
||||||
|
.perform(
|
||||||
|
post("/api/persons")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(savePersonCommand)))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.firstName").value("Jane"));
|
||||||
|
|
||||||
|
verify(getPersonUseCase, times(1)).execute(1L);
|
||||||
|
verify(savePersonUseCase, times(1)).execute(any(SavePersonCommand.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<!--
|
||||||
|
|
||||||
|
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
|
||||||
|
The MIT License
|
||||||
|
Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.iluwatar</groupId>
|
||||||
|
<artifactId>java-design-patterns</artifactId>
|
||||||
|
<version>1.26.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<artifactId>onion-architecture</artifactId>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<junit-jupiter.version>5.10.0</junit-jupiter.version>
|
||||||
|
<mockito.version>5.23.0</mockito.version>
|
||||||
|
<assertj.version>3.24.2</assertj.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>domain</module>
|
||||||
|
<module>application</module>
|
||||||
|
<module>infrastructure</module>
|
||||||
|
</modules>
|
||||||
|
</project>
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
<module>converter</module>
|
<module>converter</module>
|
||||||
<module>curiously-recurring-template-pattern</module>
|
<module>curiously-recurring-template-pattern</module>
|
||||||
<module>currying</module>
|
<module>currying</module>
|
||||||
<module>dao-factory</module>
|
<module>dao-factory</module>
|
||||||
<module>data-access-object</module>
|
<module>data-access-object</module>
|
||||||
<module>data-bus</module>
|
<module>data-bus</module>
|
||||||
<module>data-locality</module>
|
<module>data-locality</module>
|
||||||
@@ -252,6 +252,7 @@
|
|||||||
<module>backpressure</module>
|
<module>backpressure</module>
|
||||||
<module>actor-model</module>
|
<module>actor-model</module>
|
||||||
<module>rate-limiting-pattern</module>
|
<module>rate-limiting-pattern</module>
|
||||||
|
<module>onion-architecture</module>
|
||||||
</modules>
|
</modules>
|
||||||
<repositories>
|
<repositories>
|
||||||
<repository>
|
<repository>
|
||||||
@@ -346,6 +347,11 @@
|
|||||||
<artifactId>mockito-core</artifactId>
|
<artifactId>mockito-core</artifactId>
|
||||||
<version>${mockito.version}</version>
|
<version>${mockito.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-junit-jupiter</artifactId>
|
||||||
|
<version>${mockito.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mongodb</groupId>
|
<groupId>org.mongodb</groupId>
|
||||||
<artifactId>bson</artifactId>
|
<artifactId>bson</artifactId>
|
||||||
|
|||||||
Reference in new issue
Block a user