From d2543621e1544524e56f58fcb03441bde99fa2e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Stra=C3=9Fer?= Date: Mon, 12 Apr 2021 20:52:00 +0200 Subject: [PATCH] add addresses & more tests --- README.md | 25 +++- .../AddressDoesNotExistExc.java | 10 -- .../addressvalidation/AddressValidator.java | 4 +- .../addressvalidation/InvalidAddressExc.java | 16 +++ .../hexagonal/persistence/CustomerDao.java | 45 +++--- .../errors/CustomerDoesNotExistExc.java | 11 ++ .../hexagonal/web/AddAddressController.java | 23 ++- .../web/dto/response/AddressResponse.java | 15 ++ .../web/dto/response/CustomerResponse.java | 4 +- .../web/AddAddressControllerTest.java | 51 +++++++ .../src/test/resources/valid_add_address.json | 7 + .../application/customer/domain/Address.java | 1 - .../application/customer/domain/Customer.java | 114 ++++++++------- .../exception/TooOldToDeactivateExc.java | 7 + ...serIsTooYoungExc.java => TooYoungExc.java} | 4 +- .../customer/domain/CustomerTest.java | 61 ++++++++ .../customer/service/AddressServiceTest.java | 135 ++++++++++++++++-- 17 files changed, 406 insertions(+), 127 deletions(-) delete mode 100644 adapter/addressvalidation/src/main/java/de/strasser/peter/hexagonal/addressvalidation/AddressDoesNotExistExc.java create mode 100644 adapter/addressvalidation/src/main/java/de/strasser/peter/hexagonal/addressvalidation/InvalidAddressExc.java create mode 100644 adapter/persistence/src/main/java/de/strasser/peter/hexagonal/persistence/errors/CustomerDoesNotExistExc.java create mode 100644 adapter/web/src/main/java/de/strasser/peter/hexagonal/web/dto/response/AddressResponse.java create mode 100644 adapter/web/src/test/java/de/strasser/peter/hexagonal/web/AddAddressControllerTest.java create mode 100644 adapter/web/src/test/resources/valid_add_address.json create mode 100644 application/src/main/java/de/strasser/peter/hexagonal/application/customer/exception/TooOldToDeactivateExc.java rename application/src/main/java/de/strasser/peter/hexagonal/application/customer/exception/{UserIsTooYoungExc.java => TooYoungExc.java} (67%) create mode 100644 application/src/test/java/de/strasser/peter/hexagonal/application/customer/domain/CustomerTest.java diff --git a/README.md b/README.md index 7a91c73..ac1e032 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,11 @@ To run this application you need to first run a mvn clean install ``` -in the root directory of this project. This will download all required dependecies and bundle your project properly. +in the root directory of this project. This will download all required dependecies and bundle your +project properly. -The database for this application is a mongo db. You either need to have one running on your system, or you can -alternatively start the MongoDB in docker with the included docker-compose file. +The database for this application is a mongo db. You either need to have one running on your system, +or you can alternatively start the MongoDB in docker with the included docker-compose file. ```cmd docker-compose -f mongodb-docker-compose up @@ -37,11 +38,13 @@ hexagonal-example\config\src\main\java\de\strasser\peter\hexagonal\HexagonalAppl ### What does this application do -This app can register customers, add addresses to these customers and retrieve a list of all customers. +This app can register customers, add addresses to these customers and retrieve a list of all +customers. The supported usecases in the business layer can be inspected in the applicationmodule. Under -application/src/main/java/de/strasser/peter/hexagonal/application/customer/port all the supported operations are clearly -visible, which is a major selling point to structure your application in this way. +application/src/main/java/de/strasser/peter/hexagonal/application/customer/port all the supported +operations are clearly visible, which is a major selling point to structure your application in this +way. ![img_1.png](documentation/ports.png) @@ -49,3 +52,13 @@ visible, which is a major selling point to structure your application in this wa ![img_3.jpg](documentation/dependency_diagram.jpg) +### Advantages of separated models on each layer in this example + +- The customer response can easily exclude the (hashed) password without much effort. This seperates + the concern in what way to display the data to the client. + + +- The customer can be persisted different from the domain models structure. The layout in this + example has no benefit, but assume structuring your data in this way would give you a much needed + performance boost. This way the concern on how to handle data persistence is independent from the + business layer and can be handled by the persistence module. diff --git a/adapter/addressvalidation/src/main/java/de/strasser/peter/hexagonal/addressvalidation/AddressDoesNotExistExc.java b/adapter/addressvalidation/src/main/java/de/strasser/peter/hexagonal/addressvalidation/AddressDoesNotExistExc.java deleted file mode 100644 index 52d0a70..0000000 --- a/adapter/addressvalidation/src/main/java/de/strasser/peter/hexagonal/addressvalidation/AddressDoesNotExistExc.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.strasser.peter.hexagonal.addressvalidation; - -import de.strasser.peter.hexagonal.application.customer.port.out.commands.ValidateAddressCommand; - - -class AddressDoesNotExistExc extends IllegalArgumentException { - AddressDoesNotExistExc(ValidateAddressCommand validateAddressCommand) { - super(String.format("Address %s does not exist", validateAddressCommand.toString())); - } -} diff --git a/adapter/addressvalidation/src/main/java/de/strasser/peter/hexagonal/addressvalidation/AddressValidator.java b/adapter/addressvalidation/src/main/java/de/strasser/peter/hexagonal/addressvalidation/AddressValidator.java index 6b2fef4..1b82a15 100644 --- a/adapter/addressvalidation/src/main/java/de/strasser/peter/hexagonal/addressvalidation/AddressValidator.java +++ b/adapter/addressvalidation/src/main/java/de/strasser/peter/hexagonal/addressvalidation/AddressValidator.java @@ -11,11 +11,11 @@ import org.springframework.stereotype.Component; class AddressValidator implements AddressValidatorAdapter { @Override - public Address validate(ValidateAddressCommand validateAddressCommand) { + public Address validate(ValidateAddressCommand validateAddressCommand) throws InvalidAddressExc { // This could be some call to a 3rd party to validate this address. if (validateAddressCommand.getStreet().equalsIgnoreCase("parkring")) { log.info("Address is made up."); - throw new AddressDoesNotExistExc(validateAddressCommand); + throw new InvalidAddressExc(validateAddressCommand); } return new Address( diff --git a/adapter/addressvalidation/src/main/java/de/strasser/peter/hexagonal/addressvalidation/InvalidAddressExc.java b/adapter/addressvalidation/src/main/java/de/strasser/peter/hexagonal/addressvalidation/InvalidAddressExc.java new file mode 100644 index 0000000..02855f1 --- /dev/null +++ b/adapter/addressvalidation/src/main/java/de/strasser/peter/hexagonal/addressvalidation/InvalidAddressExc.java @@ -0,0 +1,16 @@ +package de.strasser.peter.hexagonal.addressvalidation; + +import de.strasser.peter.hexagonal.application.customer.exception.BusinessException; +import de.strasser.peter.hexagonal.application.customer.port.out.commands.ValidateAddressCommand; + +public class InvalidAddressExc extends BusinessException { + public InvalidAddressExc(ValidateAddressCommand validateAddressCommand) { + super( + String.format( + "Address <%s, %d, %d, %s> does not exist", + validateAddressCommand.getStreet(), + validateAddressCommand.getHouseNumber(), + validateAddressCommand.getZipCode(), + validateAddressCommand.getCountry())); + } +} diff --git a/adapter/persistence/src/main/java/de/strasser/peter/hexagonal/persistence/CustomerDao.java b/adapter/persistence/src/main/java/de/strasser/peter/hexagonal/persistence/CustomerDao.java index de0e13a..e964d0b 100644 --- a/adapter/persistence/src/main/java/de/strasser/peter/hexagonal/persistence/CustomerDao.java +++ b/adapter/persistence/src/main/java/de/strasser/peter/hexagonal/persistence/CustomerDao.java @@ -4,6 +4,7 @@ import de.strasser.peter.hexagonal.application.customer.domain.Customer; import de.strasser.peter.hexagonal.application.customer.port.in.QueryAllCustomersCRUD; import de.strasser.peter.hexagonal.application.customer.port.out.LoadCustomerAdapter; import de.strasser.peter.hexagonal.application.customer.port.out.SaveCustomerAdapter; +import de.strasser.peter.hexagonal.persistence.errors.CustomerDoesNotExistExc; import de.strasser.peter.hexagonal.persistence.mapper.CustomerMapper; import de.strasser.peter.hexagonal.persistence.model.CustomerEntity; import de.strasser.peter.hexagonal.persistence.repository.CustomerRepository; @@ -12,37 +13,31 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Repository; import java.math.BigInteger; -import java.time.LocalDate; -import java.util.HashMap; import java.util.List; -import java.util.Optional; @Slf4j @Repository @RequiredArgsConstructor -public class CustomerDao implements SaveCustomerAdapter, LoadCustomerAdapter, QueryAllCustomersCRUD { - private final CustomerRepository customerRepository; - private final CustomerMapper customerMapper; +public class CustomerDao + implements SaveCustomerAdapter, LoadCustomerAdapter, QueryAllCustomersCRUD { + private final CustomerRepository customerRepository; + private final CustomerMapper customerMapper; - @Override - public void upsert(Customer customer) { - log.info("saving customer"); - customerRepository.save(customerMapper.toDbEntity(customer)); - } + @Override + public void upsert(Customer customer) { + customerRepository.save(customerMapper.toDbEntity(customer)); + } - @Override - public Customer findById(BigInteger id) { - Optional byId = customerRepository.findById(id); - return Customer.createCustomer(id, - "max", - "mustermann", - LocalDate.of(1980, 1, 1), - new HashMap<>(), - true); - } + @Override + public Customer findById(BigInteger id) { + final CustomerEntity customerEntity = + customerRepository.findById(id).orElseThrow(() -> new CustomerDoesNotExistExc(id)); - @Override - public List getAll() { - return customerMapper.toDomain(customerRepository.findAll()); - } + return customerMapper.toDomain(customerEntity); + } + + @Override + public List getAll() { + return customerMapper.toDomain(customerRepository.findAll()); + } } diff --git a/adapter/persistence/src/main/java/de/strasser/peter/hexagonal/persistence/errors/CustomerDoesNotExistExc.java b/adapter/persistence/src/main/java/de/strasser/peter/hexagonal/persistence/errors/CustomerDoesNotExistExc.java new file mode 100644 index 0000000..9884ef5 --- /dev/null +++ b/adapter/persistence/src/main/java/de/strasser/peter/hexagonal/persistence/errors/CustomerDoesNotExistExc.java @@ -0,0 +1,11 @@ +package de.strasser.peter.hexagonal.persistence.errors; + +import de.strasser.peter.hexagonal.application.customer.exception.BusinessException; + +import java.math.BigInteger; + +public class CustomerDoesNotExistExc extends BusinessException { + public CustomerDoesNotExistExc(BigInteger id) { + super("Customer with id " + id.toString() + " does not exist!"); + } +} diff --git a/adapter/web/src/main/java/de/strasser/peter/hexagonal/web/AddAddressController.java b/adapter/web/src/main/java/de/strasser/peter/hexagonal/web/AddAddressController.java index ac8889e..891d1cf 100644 --- a/adapter/web/src/main/java/de/strasser/peter/hexagonal/web/AddAddressController.java +++ b/adapter/web/src/main/java/de/strasser/peter/hexagonal/web/AddAddressController.java @@ -2,13 +2,10 @@ package de.strasser.peter.hexagonal.web; import de.strasser.peter.hexagonal.application.customer.port.in.AddAddressUseCase; import de.strasser.peter.hexagonal.application.customer.port.in.commands.AddAddressCommand; -import de.strasser.peter.hexagonal.web.mapper.AddAddressWebMapper; import de.strasser.peter.hexagonal.web.dto.request.AddAddressRequest; +import de.strasser.peter.hexagonal.web.mapper.AddAddressWebMapper; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.math.BigInteger; import java.util.List; @@ -16,12 +13,14 @@ import java.util.List; @RestController @RequiredArgsConstructor public class AddAddressController { - private final AddAddressUseCase addAddressUseCase; - private final AddAddressWebMapper addAddressMapper; + private final AddAddressUseCase addAddressUseCase; + private final AddAddressWebMapper addAddressMapper; - @PostMapping("/v1/address") - public void addAddress(@RequestParam BigInteger customerId, @RequestBody AddAddressRequest addAddressRequest) { - final List addAddressCmds = List.of(addAddressMapper.toCmd(addAddressRequest)); - addAddressUseCase.addAddresses(customerId, addAddressCmds); - } + @PostMapping("/v1/customer/address") + public void addAddress( + @RequestParam BigInteger customerId, @RequestBody AddAddressRequest addAddressRequest) { + final List addAddressCmds = + List.of(addAddressMapper.toCmd(addAddressRequest)); + addAddressUseCase.addAddresses(customerId, addAddressCmds); + } } diff --git a/adapter/web/src/main/java/de/strasser/peter/hexagonal/web/dto/response/AddressResponse.java b/adapter/web/src/main/java/de/strasser/peter/hexagonal/web/dto/response/AddressResponse.java new file mode 100644 index 0000000..073a90a --- /dev/null +++ b/adapter/web/src/main/java/de/strasser/peter/hexagonal/web/dto/response/AddressResponse.java @@ -0,0 +1,15 @@ +package de.strasser.peter.hexagonal.web.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AddressResponse { + private String street; + private Integer houseNumber; + private Integer zipCode; + private String country; +} diff --git a/adapter/web/src/main/java/de/strasser/peter/hexagonal/web/dto/response/CustomerResponse.java b/adapter/web/src/main/java/de/strasser/peter/hexagonal/web/dto/response/CustomerResponse.java index 8c2a992..6a116a4 100644 --- a/adapter/web/src/main/java/de/strasser/peter/hexagonal/web/dto/response/CustomerResponse.java +++ b/adapter/web/src/main/java/de/strasser/peter/hexagonal/web/dto/response/CustomerResponse.java @@ -1,11 +1,13 @@ package de.strasser.peter.hexagonal.web.dto.response; +import de.strasser.peter.hexagonal.application.customer.domain.Address; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.math.BigInteger; import java.time.LocalDate; +import java.util.Map; @Data @NoArgsConstructor @@ -13,7 +15,7 @@ import java.time.LocalDate; public class CustomerResponse { private BigInteger id; private String name; - private String hashedPassword; private LocalDate birthday; + private Map addresses; private int age; } diff --git a/adapter/web/src/test/java/de/strasser/peter/hexagonal/web/AddAddressControllerTest.java b/adapter/web/src/test/java/de/strasser/peter/hexagonal/web/AddAddressControllerTest.java new file mode 100644 index 0000000..b9c79ed --- /dev/null +++ b/adapter/web/src/test/java/de/strasser/peter/hexagonal/web/AddAddressControllerTest.java @@ -0,0 +1,51 @@ +package de.strasser.peter.hexagonal.web; + +import de.strasser.peter.hexagonal.application.customer.port.in.AddAddressUseCase; +import de.strasser.peter.hexagonal.application.customer.port.in.commands.AddAddressCommand; +import de.strasser.peter.hexagonal.common.validators.TestUtils; +import de.strasser.peter.hexagonal.web.mapper.AddAddressWebMapperImpl; +import lombok.extern.slf4j.Slf4j; +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.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.math.BigInteger; +import java.util.List; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.then; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +@WebMvcTest(controllers = AddAddressController.class) +@Import(AddAddressWebMapperImpl.class) +class AddAddressControllerTest { + @Autowired private MockMvc mockMvc; + + @MockBean private AddAddressUseCase addAddressUseCase; + + @Test + public void should_AddAddress() throws Exception { + final String body = TestUtils.readStringFromResource("valid_add_address.json"); + final int customerId = 1231231; + + mockMvc + .perform( + post("/v1/customer/address?customerId=" + customerId) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isOk()) + .andReturn(); + + var addAddressCmd = new AddAddressCommand("default", "street", 59, 85748, "Germany"); + + then(addAddressUseCase) + .should() + .addAddresses(eq(BigInteger.valueOf(customerId)), eq(List.of(addAddressCmd))); + } +} diff --git a/adapter/web/src/test/resources/valid_add_address.json b/adapter/web/src/test/resources/valid_add_address.json new file mode 100644 index 0000000..e28d9ef --- /dev/null +++ b/adapter/web/src/test/resources/valid_add_address.json @@ -0,0 +1,7 @@ +{ + "type": "default", + "street": "street", + "houseNumber": 59, + "zipCode": 85748, + "country": "Germany" +} diff --git a/application/src/main/java/de/strasser/peter/hexagonal/application/customer/domain/Address.java b/application/src/main/java/de/strasser/peter/hexagonal/application/customer/domain/Address.java index b8a0ce1..24edd08 100644 --- a/application/src/main/java/de/strasser/peter/hexagonal/application/customer/domain/Address.java +++ b/application/src/main/java/de/strasser/peter/hexagonal/application/customer/domain/Address.java @@ -10,7 +10,6 @@ public class Address { Integer zipCode; String country; - public enum AddressType { DEFAULT, SHIPPING, diff --git a/application/src/main/java/de/strasser/peter/hexagonal/application/customer/domain/Customer.java b/application/src/main/java/de/strasser/peter/hexagonal/application/customer/domain/Customer.java index 927ea05..07cb185 100644 --- a/application/src/main/java/de/strasser/peter/hexagonal/application/customer/domain/Customer.java +++ b/application/src/main/java/de/strasser/peter/hexagonal/application/customer/domain/Customer.java @@ -1,8 +1,10 @@ package de.strasser.peter.hexagonal.application.customer.domain; -import de.strasser.peter.hexagonal.application.customer.exception.DefaultAdressRequiredToActivateExc; -import de.strasser.peter.hexagonal.application.customer.exception.UserIsTooYoungExc; +import de.strasser.peter.hexagonal.application.customer.exception.TooOldToDeactivateExc; +import de.strasser.peter.hexagonal.application.customer.exception.TooYoungExc; +import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.ToString; import java.math.BigInteger; import java.time.LocalDate; @@ -11,67 +13,63 @@ import java.util.HashMap; import java.util.Map; @Getter +@EqualsAndHashCode +@ToString public class Customer { - private final BigInteger id; - private String name; - private String hashedPassword; - private LocalDate birthday; - private int age; - private Map addresses; - private boolean active; + private final BigInteger id; + private String name; + private String hashedPassword; + private LocalDate birthday; + private int age; + private Map addresses; + private boolean active; - private Customer(BigInteger id, String name, String hashedPassword, LocalDate birthDate, Map addresses, boolean active) { - this.id = id; - this.active = active; - this.age = Period.between(birthDate, LocalDate.now()).getYears(); - this.birthday = birthDate; - this.name = name; - this.hashedPassword = hashedPassword; - this.addresses = addresses == null ? addresses : new HashMap<>(); - if (this.age < 18) { - throw new UserIsTooYoungExc(age); - } + private Customer( + BigInteger id, + String name, + String hashedPassword, + LocalDate birthDate, + Map addresses, + boolean active) { + this.id = id; + this.active = active; + this.age = Period.between(birthDate, LocalDate.now()).getYears(); + this.birthday = birthDate; + this.name = name; + this.hashedPassword = hashedPassword; + this.addresses = addresses != null ? addresses : new HashMap<>(); + if (this.age < 18) { + throw new TooYoungExc(age); } + } - public static Customer newCustomer( - String name, - String hashedPassword, - LocalDate birthDate) { - return new Customer( - null, - name, - hashedPassword, - birthDate, - null, - true); + public static Customer newCustomer(String name, String hashedPassword, LocalDate birthDate) { + return new Customer(null, name, hashedPassword, birthDate, null, false); + } + + public static Customer createCustomer( + BigInteger id, + String name, + String hashedPassword, + LocalDate birthDate, + Map addresses, + boolean active) { + return new Customer(id, name, hashedPassword, birthDate, addresses, active); + } + + public void addAddresses(Map addresses) { + this.addresses.putAll(addresses); + + if (this.addresses.containsKey(Address.AddressType.DEFAULT)) { + this.active = true; } + } - public static Customer createCustomer( - BigInteger id, - String name, - String hashedPassword, - LocalDate birthDate, - Map addresses, - boolean active) { - return new Customer( - id, name, hashedPassword, - birthDate, - addresses, - active); - } - - public void activateCustomer() { - if (this.addresses == null || this.addresses.containsKey(Address.AddressType.DEFAULT)) { - throw new DefaultAdressRequiredToActivateExc(); - } - this.active = true; - } - - public void addAddresses(Map addresses) { - this.addresses.putAll(addresses); - - if (this.addresses.containsKey(Address.AddressType.DEFAULT)) { - this.activateCustomer(); - } + public void deactivate() { + if (this.age < 50) { + this.active = false; + } else { + throw new TooOldToDeactivateExc(this.age); } + } } diff --git a/application/src/main/java/de/strasser/peter/hexagonal/application/customer/exception/TooOldToDeactivateExc.java b/application/src/main/java/de/strasser/peter/hexagonal/application/customer/exception/TooOldToDeactivateExc.java new file mode 100644 index 0000000..1c52a45 --- /dev/null +++ b/application/src/main/java/de/strasser/peter/hexagonal/application/customer/exception/TooOldToDeactivateExc.java @@ -0,0 +1,7 @@ +package de.strasser.peter.hexagonal.application.customer.exception; + +public class TooOldToDeactivateExc extends BusinessException { + public TooOldToDeactivateExc(int age) { + super("Customer is too old to be deactivated. Expected < 50, Actual: " + age); + } +} diff --git a/application/src/main/java/de/strasser/peter/hexagonal/application/customer/exception/UserIsTooYoungExc.java b/application/src/main/java/de/strasser/peter/hexagonal/application/customer/exception/TooYoungExc.java similarity index 67% rename from application/src/main/java/de/strasser/peter/hexagonal/application/customer/exception/UserIsTooYoungExc.java rename to application/src/main/java/de/strasser/peter/hexagonal/application/customer/exception/TooYoungExc.java index 8c15f0b..bca3d58 100644 --- a/application/src/main/java/de/strasser/peter/hexagonal/application/customer/exception/UserIsTooYoungExc.java +++ b/application/src/main/java/de/strasser/peter/hexagonal/application/customer/exception/TooYoungExc.java @@ -2,8 +2,8 @@ package de.strasser.peter.hexagonal.application.customer.exception; import java.text.MessageFormat; -public class UserIsTooYoungExc extends BusinessException { - public UserIsTooYoungExc(int age) { +public class TooYoungExc extends BusinessException { + public TooYoungExc(int age) { super(MessageFormat.format("Customer is too young. Expected: > 18 yrs, Actual: {0}", age)); } } diff --git a/application/src/test/java/de/strasser/peter/hexagonal/application/customer/domain/CustomerTest.java b/application/src/test/java/de/strasser/peter/hexagonal/application/customer/domain/CustomerTest.java new file mode 100644 index 0000000..7623dff --- /dev/null +++ b/application/src/test/java/de/strasser/peter/hexagonal/application/customer/domain/CustomerTest.java @@ -0,0 +1,61 @@ +package de.strasser.peter.hexagonal.application.customer.domain; + +import de.strasser.peter.hexagonal.application.customer.exception.TooOldToDeactivateExc; +import de.strasser.peter.hexagonal.application.customer.exception.TooYoungExc; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +class CustomerTest { + + @Test + public void should_CreateNewCustomer() { + final var customer = Customer.newCustomer("name", "pw", LocalDate.of(1980, 1, 1)); + + assertNotNull(customer); + assertNull(customer.getId()); + } + + @Test + public void should_ThrowTooYoungErr_When_CreatingNewCustomer() { + assertThrows( + TooYoungExc.class, () -> Customer.newCustomer("name", "pw", LocalDate.of(2010, 1, 1))); + } + + @Test + public void should_ActivateCustomer_When_AddingDefaultAddress() { + final Customer customer = Customer.newCustomer("name", "pw", LocalDate.of(1980, 1, 1)); + final Address address = new Address("Parkring", 59, 85748, "Germany"); + + customer.addAddresses(Collections.singletonMap(Address.AddressType.DEFAULT, address)); + + assertTrue(customer.isActive()); + } + + @Test + public void should_NotActivateCustomer_When_AddingBillingAddress() { + final Customer customer = Customer.newCustomer("name", "pw", LocalDate.of(1980, 1, 1)); + final Address address = new Address("Parkring", 59, 85748, "Germany"); + + customer.addAddresses(Collections.singletonMap(Address.AddressType.BILLING, address)); + + assertFalse(customer.isActive()); + } + + @Test + public void should_DeactivateCustomer() { + final Customer customer = Customer.newCustomer("name", "pw", LocalDate.of(1980, 1, 1)); + customer.deactivate(); + + assertFalse(customer.isActive()); + } + + @Test + public void should_ThrowTooOldErr_When_DeactivatingOldCustomer(){ + final Customer customer = Customer.newCustomer("name", "pw", LocalDate.of(1950, 1, 1)); + assertThrows(TooOldToDeactivateExc.class, customer::deactivate); + } +} diff --git a/application/src/test/java/de/strasser/peter/hexagonal/application/customer/service/AddressServiceTest.java b/application/src/test/java/de/strasser/peter/hexagonal/application/customer/service/AddressServiceTest.java index 9c1807f..44d0693 100644 --- a/application/src/test/java/de/strasser/peter/hexagonal/application/customer/service/AddressServiceTest.java +++ b/application/src/test/java/de/strasser/peter/hexagonal/application/customer/service/AddressServiceTest.java @@ -1,26 +1,141 @@ package de.strasser.peter.hexagonal.application.customer.service; +import de.strasser.peter.hexagonal.application.customer.domain.Address; +import de.strasser.peter.hexagonal.application.customer.domain.Customer; import de.strasser.peter.hexagonal.application.customer.mapper.AddAddressMapper; +import de.strasser.peter.hexagonal.application.customer.port.in.commands.AddAddressCommand; import de.strasser.peter.hexagonal.application.customer.port.out.AddressValidatorAdapter; import de.strasser.peter.hexagonal.application.customer.port.out.LoadCustomerAdapter; import de.strasser.peter.hexagonal.application.customer.port.out.SaveCustomerAdapter; +import de.strasser.peter.hexagonal.application.customer.port.out.commands.ValidateAddressCommand; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; + +import java.math.BigInteger; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; @SpringBootTest class AddressServiceTest { - @Autowired - private AddressService sut; - @MockBean - private SaveCustomerAdapter saveCustomerAdapterMock; - @MockBean - private AddressValidatorAdapter addressValidatorAdapterMock; - @MockBean - private LoadCustomerAdapter loadCustomerAdapterMock; - @MockBean - private AddAddressMapper addAddressMapperMock; + @Autowired private AddressService sut; + @MockBean private SaveCustomerAdapter saveCustomerAdapterMock; + @MockBean private AddressValidatorAdapter addressValidatorAdapterMock; + @MockBean private LoadCustomerAdapter loadCustomerAdapterMock; + @SpyBean private AddAddressMapper addAddressMapperMock; + @Test + public void should_AddAddress() { + // GIVEN + final var customerId = BigInteger.valueOf(13); + final var name = "hans"; + final var birthday = LocalDate.of(1980, 1, 1); + final var passwd = "secretPw"; + given(loadCustomerAdapterMock.findById(any())) + .willReturn(Customer.createCustomer(customerId, name, passwd, birthday, null, false)); + + final String street = "Parkring"; + final int houseNumber = 57; + final int zipCode = 85748; + final String country = "Germany"; + + final Address validatedAddress = new Address(street, houseNumber, zipCode, country); + final ValidateAddressCommand validateAddressCmd = + new ValidateAddressCommand(street, houseNumber, zipCode, country); + + given(addressValidatorAdapterMock.validate(validateAddressCmd)).willReturn(validatedAddress); + + final List addAddressCmds = + List.of(new AddAddressCommand("billing", street, houseNumber, zipCode, country)); + + sut.addAddresses(customerId, addAddressCmds); + + final Customer customerToBeSaved = + Customer.createCustomer( + customerId, + name, + passwd, + birthday, + Map.of(Address.AddressType.BILLING, validatedAddress), + false); + then(saveCustomerAdapterMock).should().upsert(eq(customerToBeSaved)); + } + + @Test + public void should_AddAddressAndActivateCustomer_When_ProvidedDefaultAddress() { + // GIVEN + final var customerId = BigInteger.valueOf(13); + final var name = "hans"; + final var birthday = LocalDate.of(1980, 1, 1); + final var passwd = "secretPw"; + + given(loadCustomerAdapterMock.findById(any())) + .willReturn(Customer.createCustomer(customerId, name, passwd, birthday, null, false)); + + final String street = "Parkring"; + final int houseNumber = 57; + final int zipCode = 85748; + final String country = "Germany"; + + final Address validatedAddress = new Address(street, houseNumber, zipCode, country); + final ValidateAddressCommand validateAddressCmd = + new ValidateAddressCommand(street, houseNumber, zipCode, country); + + given(addressValidatorAdapterMock.validate(validateAddressCmd)).willReturn(validatedAddress); + + final List addAddressCmds = + List.of(new AddAddressCommand("default", street, houseNumber, zipCode, country)); + + sut.addAddresses(customerId, addAddressCmds); + + final Customer customerToBeSaved = + Customer.createCustomer( + customerId, + name, + passwd, + birthday, + Map.of(Address.AddressType.DEFAULT, validatedAddress), + true); + then(saveCustomerAdapterMock).should().upsert(eq(customerToBeSaved)); + } + + @Test + public void should_ThrowError_When_ProvidingInvalidAddress() { + // GIVEN + final var customerId = BigInteger.valueOf(13); + final var name = "hans"; + final var birthday = LocalDate.of(1980, 1, 1); + final var passwd = "secretPw"; + + given(loadCustomerAdapterMock.findById(any())) + .willReturn(Customer.createCustomer(customerId, name, passwd, birthday, null, false)); + + final String street = "Parkring"; + final int houseNumber = 57; + final int zipCode = 85748; + final String country = "Germany"; + + final ValidateAddressCommand validateAddressCmd = + new ValidateAddressCommand(street, houseNumber, zipCode, country); + + given(addressValidatorAdapterMock.validate(validateAddressCmd)) + .willThrow(new IllegalArgumentException("invalid adress")); + + final List addAddressCmds = + List.of(new AddAddressCommand("billing", street, houseNumber, zipCode, country)); + + assertThrows( + IllegalArgumentException.class, () -> sut.addAddresses(customerId, addAddressCmds)); + } }