diff --git a/pet-clinic-data/pom.xml b/pet-clinic-data/pom.xml index 9e36c1512..908ca57a8 100644 --- a/pet-clinic-data/pom.xml +++ b/pet-clinic-data/pom.xml @@ -12,6 +12,7 @@ true + 6.1.2.Final @@ -34,6 +35,32 @@ lombok true + + javax.validation + validation-api + 2.0.1.Final + + + org.hibernate.validator + hibernate-validator + ${hibernate.validator.version} + + + org.hibernate.validator + hibernate-validator-annotation-processor + ${hibernate.validator.version} + + + + + javax.cache + cache-api + + + org.ehcache + ehcache + + org.springframework.boot spring-boot-starter-test diff --git a/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/cache/CacheConfiguration.java b/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/cache/CacheConfiguration.java new file mode 100644 index 000000000..11a829cbe --- /dev/null +++ b/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/cache/CacheConfiguration.java @@ -0,0 +1,38 @@ +package guru.springframework.sfgpetclinic.cache; + +import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.cache.configuration.MutableConfiguration; + +/** + * Cache configuration intended for caches providing the JCache API. This configuration + * creates the used cache for the application and enables statistics that become + * accessible via JMX. + */ +@Configuration +@EnableCaching +class CacheConfiguration { + + @Bean + public JCacheManagerCustomizer petclinicCacheConfigurationCustomizer() { + return cm -> { + cm.createCache("vets", cacheConfiguration()); + }; + } + + /** + * Create a simple configuration that enable statistics via the JCache programmatic + * configuration API. + *

+ * Within the configuration object that is provided by the JCache API standard, there + * is only a very limited set of configuration options. The really relevant + * configuration options (like the size limit) must be set via a configuration + * mechanism that is provided by the selected JCache implementation. + */ + private javax.cache.configuration.Configuration cacheConfiguration() { + return new MutableConfiguration<>().setStatisticsEnabled(true); + } +} \ No newline at end of file diff --git a/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Owner.java b/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Owner.java index ac8fe4385..9619d9759 100644 --- a/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Owner.java +++ b/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Owner.java @@ -6,6 +6,8 @@ import lombok.Setter; import javax.persistence.*; +import javax.validation.constraints.Digits; +import javax.validation.constraints.NotEmpty; import java.util.HashSet; import java.util.Set; @@ -33,12 +35,16 @@ public Owner(Long id, String firstName, String lastName, String address, String } @Column(name = "address") + @NotEmpty private String address; @Column(name = "city") + @NotEmpty private String city; @Column(name = "telephone") + @NotEmpty + @Digits(fraction = 0, integer = 10) private String telephone; @OneToMany(cascade = CascadeType.ALL, mappedBy = "owner") diff --git a/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Person.java b/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Person.java index 2b684429a..fbd37c186 100644 --- a/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Person.java +++ b/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Person.java @@ -7,6 +7,7 @@ import javax.persistence.Column; import javax.persistence.MappedSuperclass; +import javax.validation.constraints.NotEmpty; /** * Created by jt on 7/13/18. @@ -25,9 +26,11 @@ public Person(Long id, String firstName, String lastName) { } @Column(name = "first_name") + @NotEmpty private String firstName; @Column(name = "last_name") + @NotEmpty private String lastName; } diff --git a/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Pet.java b/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Pet.java index dbfa259ec..c1c4e538f 100644 --- a/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Pet.java +++ b/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Pet.java @@ -4,6 +4,7 @@ import org.springframework.format.annotation.DateTimeFormat; import javax.persistence.*; +import javax.validation.constraints.NotEmpty; import java.time.LocalDate; import java.util.HashSet; import java.util.Set; @@ -33,6 +34,7 @@ public Pet(Long id, String name, PetType petType, Owner owner, LocalDate birthDa } @Column(name = "name") + @NotEmpty private String name; @ManyToOne diff --git a/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Visit.java b/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Visit.java index 8517571f9..44478a737 100644 --- a/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Visit.java +++ b/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/model/Visit.java @@ -1,8 +1,10 @@ package guru.springframework.sfgpetclinic.model; import lombok.*; +import org.springframework.format.annotation.DateTimeFormat; import javax.persistence.*; +import javax.validation.constraints.NotEmpty; import java.time.LocalDate; /** @@ -18,9 +20,11 @@ public class Visit extends BaseEntity { @Column(name = "date") + @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate date; @Column(name = "description") + @NotEmpty private String description; @ManyToOne diff --git a/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/repositories/VetRepository.java b/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/repositories/VetRepository.java index 0af0f91c2..2e21df2ab 100644 --- a/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/repositories/VetRepository.java +++ b/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/repositories/VetRepository.java @@ -1,10 +1,18 @@ package guru.springframework.sfgpetclinic.repositories; import guru.springframework.sfgpetclinic.model.Vet; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.dao.DataAccessException; import org.springframework.data.repository.CrudRepository; +import javax.transaction.Transactional; +import java.util.Collection; + /** * Created by jt on 8/5/18. */ public interface VetRepository extends CrudRepository { + @Transactional + @Cacheable("vets") + Collection findAll() throws DataAccessException; } diff --git a/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/validators/PetValidator.java b/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/validators/PetValidator.java new file mode 100644 index 000000000..51c51337a --- /dev/null +++ b/pet-clinic-data/src/main/java/guru/springframework/sfgpetclinic/validators/PetValidator.java @@ -0,0 +1,42 @@ +package guru.springframework.sfgpetclinic.validators; + +import guru.springframework.sfgpetclinic.model.Pet; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +/** + * @author Gaetan Bloch + * Created on 30/03/2020 + */ +public final class PetValidator implements Validator { + private static final String REQUIRED = "required"; + + @Override + public void validate(Object obj, Errors errors) { + Pet pet = (Pet) obj; + String name = pet.getName(); + // name validation + if (!StringUtils.hasLength(name)) { + errors.rejectValue("name", REQUIRED, REQUIRED); + } + + // type validation + if (pet.isNew() && pet.getPetType() == null) { + errors.rejectValue("petType", REQUIRED, REQUIRED); + } + + // birth date validation + if (pet.getBirthDate() == null) { + errors.rejectValue("birthDate", REQUIRED, REQUIRED); + } + } + + /** + * This Validator validates *just* Pet instances + */ + @Override + public boolean supports(Class clazz) { + return Pet.class.isAssignableFrom(clazz); + } +} diff --git a/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/CrashController.java b/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/CrashController.java new file mode 100644 index 000000000..b68c05040 --- /dev/null +++ b/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/CrashController.java @@ -0,0 +1,18 @@ +package guru.springframework.sfgpetclinic.controllers; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +/** + * @author Gaetan Bloch + * Created on 30/03/2020 + */ +@Controller +final class CrashController { + static final String URL_OUPS = "/oups"; + + @GetMapping(URL_OUPS) + public String triggerException() { + throw new RuntimeException("Expected: controller used to showcase what happens when an exception is thrown"); + } +} diff --git a/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/IndexController.java b/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/IndexController.java index 020b271e0..eb1633a2e 100644 --- a/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/IndexController.java +++ b/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/IndexController.java @@ -14,9 +14,4 @@ public String index(){ return "index"; } - - @RequestMapping("/oups") - public String oupsHandler(){ - return "notimplemented"; - } } diff --git a/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/OwnerController.java b/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/OwnerController.java index 394432160..9f2deeb0d 100644 --- a/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/OwnerController.java +++ b/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/OwnerController.java @@ -18,7 +18,7 @@ @RequestMapping("/owners") @Controller public class OwnerController { - private static final String VIEWS_OWNER_CREATE_OR_UPDATE_FORM = "owners/createOrUpdateOwnerForm"; + static final String VIEWS_OWNER_CREATE_OR_UPDATE_FORM = "owners/createOrUpdateOwnerForm"; private final OwnerService ownerService; diff --git a/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/PetController.java b/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/PetController.java index 5597cbaa3..226195a57 100644 --- a/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/PetController.java +++ b/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/PetController.java @@ -6,6 +6,7 @@ import guru.springframework.sfgpetclinic.services.OwnerService; import guru.springframework.sfgpetclinic.services.PetService; import guru.springframework.sfgpetclinic.services.PetTypeService; +import guru.springframework.sfgpetclinic.validators.PetValidator; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.ui.ModelMap; @@ -24,7 +25,7 @@ @RequestMapping("/owners/{ownerId}") public class PetController { - private static final String VIEWS_PETS_CREATE_OR_UPDATE_FORM = "pets/createOrUpdatePetForm"; + static final String VIEWS_PETS_CREATE_OR_UPDATE_FORM = "pets/createOrUpdatePetForm"; private final PetService petService; private final OwnerService ownerService; @@ -51,6 +52,11 @@ public void initOwnerBinder(WebDataBinder dataBinder) { dataBinder.setDisallowedFields("id"); } + @InitBinder("pet") + public void initPetBinder(WebDataBinder dataBinder) { + dataBinder.setValidator(new PetValidator()); + } + @GetMapping("/pets/new") public String initCreationForm(Owner owner, Model model) { Pet pet = new Pet(); diff --git a/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/VisitController.java b/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/VisitController.java index ca1394dde..d8480223c 100644 --- a/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/VisitController.java +++ b/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/controllers/VisitController.java @@ -21,6 +21,8 @@ @Controller public class VisitController { + static final String PETS_CREATE_OR_UPDATE_VISIT_FORM = "pets/createOrUpdateVisitForm"; + private final VisitService visitService; private final PetService petService; @@ -64,14 +66,14 @@ public Visit loadPetWithVisit(@PathVariable("petId") Long petId, Map model) { - return "pets/createOrUpdateVisitForm"; + return PETS_CREATE_OR_UPDATE_VISIT_FORM; } // Spring MVC calls method loadPetWithVisit(...) before processNewVisitForm is called @PostMapping("/owners/{ownerId}/pets/{petId}/visits/new") public String processNewVisitForm(@Valid Visit visit, BindingResult result) { if (result.hasErrors()) { - return "pets/createOrUpdateVisitForm"; + return PETS_CREATE_OR_UPDATE_VISIT_FORM; } else { visitService.save(visit); diff --git a/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/formatters/PetTypeFormatter.java b/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/formatters/PetTypeFormatter.java index 43a2296ec..0db7165eb 100644 --- a/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/formatters/PetTypeFormatter.java +++ b/pet-clinic-web/src/main/java/guru/springframework/sfgpetclinic/formatters/PetTypeFormatter.java @@ -13,7 +13,7 @@ * Created by jt on 9/22/18. */ @Component -public class PetTypeFormatter implements Formatter { +public final class PetTypeFormatter implements Formatter { private final PetTypeService petTypeService; diff --git a/pet-clinic-web/src/main/resources/application.properties b/pet-clinic-web/src/main/resources/application.properties index 4e700166c..f8dc89ac3 100644 --- a/pet-clinic-web/src/main/resources/application.properties +++ b/pet-clinic-web/src/main/resources/application.properties @@ -3,4 +3,7 @@ spring.banner.image.location=vizsla.jpg # Internationalization spring.messages.basename=messages/messages -spring.profiles.active=springdatajpa \ No newline at end of file +spring.profiles.active=springdatajpa + +# Maximum time static resources should be cached +spring.resources.cache.cachecontrol.max-age=12h \ No newline at end of file diff --git a/pet-clinic-web/src/main/resources/templates/error.html b/pet-clinic-web/src/main/resources/templates/error.html new file mode 100644 index 000000000..b9026690e --- /dev/null +++ b/pet-clinic-web/src/main/resources/templates/error.html @@ -0,0 +1,11 @@ + + + + + + +

Something happened...

+

Exception message

+ + + \ No newline at end of file diff --git a/pet-clinic-web/src/main/resources/templates/notimplemented.html b/pet-clinic-web/src/main/resources/templates/notimplemented.html deleted file mode 100644 index 4eea12409..000000000 --- a/pet-clinic-web/src/main/resources/templates/notimplemented.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Not Implemented - - -

Not Implemented Yet!

- - \ No newline at end of file diff --git a/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/PetClinicIntegrationTest.java b/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/PetClinicIntegrationTest.java new file mode 100644 index 000000000..f678d47bc --- /dev/null +++ b/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/PetClinicIntegrationTest.java @@ -0,0 +1,19 @@ +package guru.springframework.sfgpetclinic; + +import guru.springframework.sfgpetclinic.services.VetService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class PetClinicIntegrationTest { + + @Autowired + private VetService vetService; + + @Test + void testFindAll() { + vetService.findAll(); + vetService.findAll(); // served from cache + } +} diff --git a/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/controllers/CrashControllerTest.java b/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/controllers/CrashControllerTest.java new file mode 100644 index 000000000..6f0b67cde --- /dev/null +++ b/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/controllers/CrashControllerTest.java @@ -0,0 +1,30 @@ +package guru.springframework.sfgpetclinic.controllers; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.util.NestedServletException; + +import static guru.springframework.sfgpetclinic.controllers.CrashController.URL_OUPS; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +/** + * @author Gaetan Bloch + * Created on 30/03/2020 + */ +class CrashControllerTest { + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(new CrashController()).build(); + } + + @Test + void triggerExceptionTest() { + assertThrows(NestedServletException.class, () -> mockMvc.perform(get(URL_OUPS))); + } +} \ No newline at end of file diff --git a/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/controllers/OwnerControllerTest.java b/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/controllers/OwnerControllerTest.java index 267bb8b7a..8c3e5767d 100644 --- a/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/controllers/OwnerControllerTest.java +++ b/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/controllers/OwnerControllerTest.java @@ -5,18 +5,20 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentMatchers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import java.util.*; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import static guru.springframework.sfgpetclinic.controllers.OwnerController.VIEWS_OWNER_CREATE_OR_UPDATE_FORM; import static org.hamcrest.Matchers.*; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -25,6 +27,9 @@ @ExtendWith(MockitoExtension.class) class OwnerControllerTest { + private static final String URL_OWNERS_NEW = "/owners/new"; + private static final String URL_OWNERS_EDIT = "/owners/1/edit"; + @Mock OwnerService ownerService; @@ -84,10 +89,11 @@ void processFindFormEmptyReturnMany() throws Exception { Owner.builder().id(2l).build())); mockMvc.perform(get("/owners") - .param("lastName","")) + .param("lastName", "")) .andExpect(status().isOk()) .andExpect(view().name("owners/ownersList")) - .andExpect(model().attribute("selections", hasSize(2)));; + .andExpect(model().attribute("selections", hasSize(2))); + ; } @Test @@ -103,47 +109,93 @@ void displayOwner() throws Exception { @Test void initCreationForm() throws Exception { - mockMvc.perform(get("/owners/new")) + mockMvc.perform(get(URL_OWNERS_NEW)) .andExpect(status().isOk()) - .andExpect(view().name("owners/createOrUpdateOwnerForm")) + .andExpect(view().name(VIEWS_OWNER_CREATE_OR_UPDATE_FORM)) .andExpect(model().attributeExists("owner")); verifyZeroInteractions(ownerService); } @Test - void processCreationForm() throws Exception { - when(ownerService.save(ArgumentMatchers.any())).thenReturn(Owner.builder().id(1l).build()); - - mockMvc.perform(post("/owners/new")) + void processCreationFormTest() throws Exception { + // Given + when(ownerService.save(any())).thenReturn(Owner.builder().id(1L).build()); + + // When + mockMvc.perform(post(URL_OWNERS_NEW) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("firstName", "John") + .param("lastName", "Doe") + .param("address", "123 Paris street") + .param("city", "Paris") + .param("telephone", "0123123123")) + + // Then .andExpect(status().is3xxRedirection()) .andExpect(view().name("redirect:/owners/1")) .andExpect(model().attributeExists("owner")); - verify(ownerService).save(ArgumentMatchers.any()); + verify(ownerService).save(any()); + } + + @Test + void processCreationFormValidationFailedTest() throws Exception { + // When + mockMvc.perform(post(URL_OWNERS_NEW).contentType(MediaType.APPLICATION_FORM_URLENCODED)) + + // Then + .andExpect(status().isOk()) + .andExpect(view().name(VIEWS_OWNER_CREATE_OR_UPDATE_FORM)) + .andExpect(model().attributeExists("owner")); + + verifyZeroInteractions(ownerService); } @Test void initUpdateOwnerForm() throws Exception { when(ownerService.findById(anyLong())).thenReturn(Owner.builder().id(1l).build()); - mockMvc.perform(get("/owners/1/edit")) + mockMvc.perform(get(URL_OWNERS_EDIT)) .andExpect(status().isOk()) - .andExpect(view().name("owners/createOrUpdateOwnerForm")) + .andExpect(view().name(VIEWS_OWNER_CREATE_OR_UPDATE_FORM)) .andExpect(model().attributeExists("owner")); verifyZeroInteractions(ownerService); } @Test - void processUpdateOwnerForm() throws Exception { - when(ownerService.save(ArgumentMatchers.any())).thenReturn(Owner.builder().id(1l).build()); - - mockMvc.perform(post("/owners/1/edit")) + void processUpdateOwnerFormTest() throws Exception { + // Given + when(ownerService.save(any())).thenReturn(Owner.builder().id(1L).build()); + + // When + mockMvc.perform(post(URL_OWNERS_EDIT) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("firstName", "John") + .param("lastName", "Doe") + .param("address", "123 Paris street") + .param("city", "Paris") + .param("telephone", "0123123123")) + + // Then .andExpect(status().is3xxRedirection()) .andExpect(view().name("redirect:/owners/1")) .andExpect(model().attributeExists("owner")); - verify(ownerService).save(ArgumentMatchers.any()); + verify(ownerService).save(any()); + } + + @Test + void processUpdateOwnerFormValidationFailedTest() throws Exception { + // When + mockMvc.perform(post(URL_OWNERS_EDIT).contentType(MediaType.APPLICATION_FORM_URLENCODED)) + + // Then + .andExpect(status().isOk()) + .andExpect(view().name(VIEWS_OWNER_CREATE_OR_UPDATE_FORM)) + .andExpect(model().attributeExists("owner")); + + verifyZeroInteractions(ownerService); } } \ No newline at end of file diff --git a/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/controllers/PetControllerTest.java b/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/controllers/PetControllerTest.java index d8076776b..38dc53470 100644 --- a/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/controllers/PetControllerTest.java +++ b/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/controllers/PetControllerTest.java @@ -1,5 +1,6 @@ package guru.springframework.sfgpetclinic.controllers; +import guru.springframework.sfgpetclinic.formatters.PetTypeFormatter; import guru.springframework.sfgpetclinic.model.Owner; import guru.springframework.sfgpetclinic.model.Pet; import guru.springframework.sfgpetclinic.model.PetType; @@ -12,16 +13,18 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import java.util.HashSet; import java.util.Set; +import static guru.springframework.sfgpetclinic.controllers.PetController.VIEWS_PETS_CREATE_OR_UPDATE_FORM; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -29,6 +32,9 @@ @ExtendWith(MockitoExtension.class) class PetControllerTest { + private static final String URL_PETS_NEW = "/owners/1/pets/new"; + private static final String URL_PETS_EDIT = "/owners/1/pets/2/edit"; + @Mock PetService petService; @@ -54,8 +60,11 @@ void setUp() { petTypes.add(PetType.builder().id(1L).name("Dog").build()); petTypes.add(PetType.builder().id(2L).name("Cat").build()); + var conversionService = new DefaultFormattingConversionService(); + conversionService.addFormatterForFieldType(PetType.class, new PetTypeFormatter(petTypeService)); mockMvc = MockMvcBuilders .standaloneSetup(petController) + .setConversionService(conversionService) .build(); } @@ -64,50 +73,100 @@ void initCreationForm() throws Exception { when(ownerService.findById(anyLong())).thenReturn(owner); when(petTypeService.findAll()).thenReturn(petTypes); - mockMvc.perform(get("/owners/1/pets/new")) + mockMvc.perform(get(URL_PETS_NEW)) .andExpect(status().isOk()) .andExpect(model().attributeExists("owner")) .andExpect(model().attributeExists("pet")) - .andExpect(view().name("pets/createOrUpdatePetForm")); + .andExpect(view().name(VIEWS_PETS_CREATE_OR_UPDATE_FORM)); } @Test - void processCreationForm() throws Exception { + void processCreationFormTest() throws Exception { + // Given when(ownerService.findById(anyLong())).thenReturn(owner); when(petTypeService.findAll()).thenReturn(petTypes); - mockMvc.perform(post("/owners/1/pets/new")) + // When + mockMvc.perform(post(URL_PETS_NEW) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("name", "Milou") + .param("birthDate", "2020-01-01") + .param("petType", "Dog")) + + // Then .andExpect(status().is3xxRedirection()) - .andExpect(view().name("redirect:/owners/1")); + .andExpect(view().name("redirect:/owners/1")) + .andExpect(model().attributeExists("owner")) + .andExpect(model().attributeExists("pet")); verify(petService).save(any()); } + @Test + void processCreationFormValidationFailedTest() throws Exception { + // Given + when(ownerService.findById(anyLong())).thenReturn(owner); + + // When + mockMvc.perform(post(URL_PETS_NEW).contentType(MediaType.APPLICATION_FORM_URLENCODED)) + + // Then + .andExpect(status().isOk()) + .andExpect(view().name(VIEWS_PETS_CREATE_OR_UPDATE_FORM)) + .andExpect(model().attributeExists("owner")) + .andExpect(model().attributeExists("pet")); + + verifyZeroInteractions(petService); + } + @Test void initUpdateForm() throws Exception { when(ownerService.findById(anyLong())).thenReturn(owner); when(petTypeService.findAll()).thenReturn(petTypes); when(petService.findById(anyLong())).thenReturn(Pet.builder().id(2L).build()); - mockMvc.perform(get("/owners/1/pets/2/edit")) + mockMvc.perform(get(URL_PETS_EDIT)) .andExpect(status().isOk()) .andExpect(model().attributeExists("owner")) .andExpect(model().attributeExists("pet")) - .andExpect(view().name("pets/createOrUpdatePetForm")); + .andExpect(view().name(VIEWS_PETS_CREATE_OR_UPDATE_FORM)); } @Test - void processUpdateForm() throws Exception { + void processUpdateFormTest() throws Exception { + // Given when(ownerService.findById(anyLong())).thenReturn(owner); when(petTypeService.findAll()).thenReturn(petTypes); - mockMvc.perform(post("/owners/1/pets/2/edit")) + // When + mockMvc.perform(post(URL_PETS_EDIT) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("name", "Caline") + .param("birthDate", "2020-01-01") + .param("petType", "Cat")) + + // Then .andExpect(status().is3xxRedirection()) - .andExpect(view().name("redirect:/owners/1")); + .andExpect(view().name("redirect:/owners/1")) + .andExpect(model().attributeExists("pet")) + .andExpect(model().attributeExists("owner")); verify(petService).save(any()); } + @Test + void processUpdateFormValidationFailedTest() throws Exception { + // When + mockMvc.perform(post(URL_PETS_EDIT).contentType(MediaType.APPLICATION_FORM_URLENCODED)) + + // Then + .andExpect(status().isOk()) + .andExpect(view().name(VIEWS_PETS_CREATE_OR_UPDATE_FORM)) + .andExpect(model().attributeExists("pet")); + + verifyZeroInteractions(petService); + } + @Test void populatePetTypes() { //todo impl diff --git a/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/controllers/VisitControllerTest.java b/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/controllers/VisitControllerTest.java index 41542c328..ea011c83b 100644 --- a/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/controllers/VisitControllerTest.java +++ b/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/controllers/VisitControllerTest.java @@ -22,6 +22,7 @@ import java.util.HashSet; import java.util.Map; +import static guru.springframework.sfgpetclinic.controllers.VisitController.PETS_CREATE_OR_UPDATE_VISIT_FORM; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -31,7 +32,6 @@ @ExtendWith(MockitoExtension.class) class VisitControllerTest { - private static final String PETS_CREATE_OR_UPDATE_VISIT_FORM = "pets/createOrUpdateVisitForm"; private static final String REDIRECT_OWNERS_1 = "redirect:/owners/{ownerId}"; private static final String YET_ANOTHER_VISIT_DESCRIPTION = "yet another visit"; @@ -57,18 +57,18 @@ void setUp() { when(petService.findById(anyLong())) .thenReturn( Pet.builder() - .id(petId) - .birthDate(LocalDate.of(2018,11,13)) - .name("Cutie") - .visits(new HashSet<>()) - .owner(Owner.builder() - .id(ownerId) - .lastName("Doe") - .firstName("Joe") - .build()) - .petType(PetType.builder() - .name("Dog").build()) - .build() + .id(petId) + .birthDate(LocalDate.of(2018, 11, 13)) + .name("Cutie") + .visits(new HashSet<>()) + .owner(Owner.builder() + .id(ownerId) + .lastName("Doe") + .firstName("Joe") + .build()) + .petType(PetType.builder() + .name("Dog").build()) + .build() ); uriVariables.clear(); @@ -91,14 +91,29 @@ void initNewVisitForm() throws Exception { @Test - void processNewVisitForm() throws Exception { + void processNewVisitFormTest() throws Exception { + // When mockMvc.perform(post(visitsUri) - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .param("date","2018-11-11") - .param("description", YET_ANOTHER_VISIT_DESCRIPTION)) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("date", "2018-11-11") + .param("description", YET_ANOTHER_VISIT_DESCRIPTION)) + + // Then .andExpect(status().is3xxRedirection()) .andExpect(view().name(REDIRECT_OWNERS_1)) .andExpect(model().attributeExists("visit")) - ; + .andExpect(model().attributeExists("pet")); + } + + @Test + void processNewVisitFormValidationFailedTest() throws Exception { + // When + mockMvc.perform(post(visitsUri).contentType(MediaType.APPLICATION_FORM_URLENCODED)) + + // Then + .andExpect(status().isOk()) + .andExpect(view().name(PETS_CREATE_OR_UPDATE_VISIT_FORM)) + .andExpect(model().attributeExists("visit")) + .andExpect(model().attributeExists("pet")); } } \ No newline at end of file diff --git a/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/formatters/PetTypeFormatterTest.java b/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/formatters/PetTypeFormatterTest.java new file mode 100644 index 000000000..4af6ba444 --- /dev/null +++ b/pet-clinic-web/src/test/java/guru/springframework/sfgpetclinic/formatters/PetTypeFormatterTest.java @@ -0,0 +1,74 @@ +package guru.springframework.sfgpetclinic.formatters; + +import guru.springframework.sfgpetclinic.model.PetType; +import guru.springframework.sfgpetclinic.services.PetTypeService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.text.ParseException; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +/** + * @author Gaetan Bloch + * Created on 30/03/2020 + */ +@ExtendWith(MockitoExtension.class) +class PetTypeFormatterTest { + + private static final String PET_TYPE_DOG = "Dog"; + private static final String PET_TYPE_CAT = "Cat"; + private static final String PET_TYPE_FISH = "Fish"; + + private PetTypeFormatter petTypeFormatter; + + private Set petTypes; + + @Mock + private PetTypeService petTypeService; + + @BeforeEach + void setup() { + petTypeFormatter = new PetTypeFormatter(petTypeService); + petTypes = new HashSet<>(); + petTypes.add(PetType.builder().id(1L).name(PET_TYPE_DOG).build()); + petTypes.add(PetType.builder().id(2L).name(PET_TYPE_CAT).build()); + } + + @Test + void printTest() { + // Given + PetType petType = PetType.builder().name(PET_TYPE_DOG).build(); + + // When + String petTypeName = petTypeFormatter.print(petType, Locale.ENGLISH); + + // Then + assertEquals(PET_TYPE_DOG, petTypeName); + } + + @Test + void parseTest() throws ParseException { + // Given + when(petTypeService.findAll()).thenReturn(petTypes); + + // When + PetType petType = petTypeFormatter.parse(PET_TYPE_CAT, Locale.ENGLISH); + + // Then + assertEquals(PET_TYPE_CAT, petType.getName()); + } + + @Test + void parseThrowsExceptionTest() { + Assertions.assertThrows(ParseException.class, () -> petTypeFormatter.parse(PET_TYPE_FISH, Locale.ENGLISH)); + } +} \ No newline at end of file