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:
Hattori Henzo authored and GitHub committed 2026-08-16 20:30:42 +03:00
1 parent b55fc2bcb5
commit 688095a4c7
32 files changed
+3182 -1

No files matched your search

+87
View File
@@ -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>
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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()));
}
}
@@ -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);
}
@@ -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');
@@ -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);
}
}
}
@@ -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));
}
}
}