diff --git a/src/main/java/cat/udl/eps/softarch/demo/domain/Adoption.java b/src/main/java/cat/udl/eps/softarch/demo/domain/Adoption.java index ff6f357e..539b27d7 100644 --- a/src/main/java/cat/udl/eps/softarch/demo/domain/Adoption.java +++ b/src/main/java/cat/udl/eps/softarch/demo/domain/Adoption.java @@ -1,5 +1,7 @@ package cat.udl.eps.softarch.demo.domain; +import com.fasterxml.jackson.annotation.JsonIdentityReference; +import com.fasterxml.jackson.annotation.JsonInclude; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -30,8 +32,15 @@ public class Adoption extends UriEntity { private ZonedDateTime endDate; @ManyToOne + @JsonIdentityReference(alwaysAsId = true) // Only serialize the pet ID + @JsonInclude(JsonInclude.Include.NON_NULL) private User user; //Adopter - + + @ManyToOne + @JsonIdentityReference(alwaysAsId = true) // Only serialize the pet ID + @JsonInclude(JsonInclude.Include.NON_NULL) // Include pet only if it's not null private Pet pet; + + } diff --git a/src/main/java/cat/udl/eps/softarch/demo/exceptions/InvalidPostRequest.java b/src/main/java/cat/udl/eps/softarch/demo/exceptions/InvalidPostRequest.java new file mode 100644 index 00000000..87820593 --- /dev/null +++ b/src/main/java/cat/udl/eps/softarch/demo/exceptions/InvalidPostRequest.java @@ -0,0 +1,7 @@ +package cat.udl.eps.softarch.demo.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.CONFLICT, reason = "Bad post request") +public class InvalidPostRequest extends RuntimeException {} \ No newline at end of file diff --git a/src/main/java/cat/udl/eps/softarch/demo/handler/AdoptionEventHandler.java b/src/main/java/cat/udl/eps/softarch/demo/handler/AdoptionEventHandler.java new file mode 100644 index 00000000..d8adffd5 --- /dev/null +++ b/src/main/java/cat/udl/eps/softarch/demo/handler/AdoptionEventHandler.java @@ -0,0 +1,112 @@ +package cat.udl.eps.softarch.demo.handler; + +import cat.udl.eps.softarch.demo.domain.Adoption; +import cat.udl.eps.softarch.demo.exceptions.InvalidPostRequest; +import org.springframework.data.rest.core.annotation.HandleAfterSave; +import org.springframework.data.rest.core.annotation.HandleBeforeCreate; +import org.springframework.data.rest.core.annotation.HandleBeforeSave; +import org.springframework.data.rest.core.annotation.RepositoryEventHandler; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import cat.udl.eps.softarch.demo.exceptions.UnauthorizedAccessException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.List; + +/** + * This class defines event handlers for Adoption entity. + */ +@Component +@RepositoryEventHandler() // Ensure this handler is for MedicalRecord entity +public class AdoptionEventHandler { + + private static final Logger logger = LoggerFactory.getLogger(AdoptionEventHandler.class); + + /** + * These are the roles that are allowed to create an adoption. + */ + private static final String ROLE_USER = "ROLE_USER"; + private static final String ROLE_SHELTER_VOLUNTEER = "ROLE_SHELTER_VOLUNTEER"; + private static final String ROLE_ADMIN = "ROLE_ADMIN"; + + /** + * Handles actions before creating an adoption. + * @param adoption The adoption object to be created. + * @throws UnauthorizedAccessException If the user is not authorized. + * @throws InvalidPostRequest If the request is invalid. + */ + @HandleBeforeCreate + public void handleAdoptionBeforeCreate(Adoption adoption) throws UnauthorizedAccessException, InvalidPostRequest { + + checkAuthorization("POST"); + // If the pet is already adopted or the pet is null, an exception is thrown + if (adoption.getPet() == null || adoption.getPet().isAdopted() || adoption.getConfirmed()) { + logger.error("Pet is already adopted or bad request"); + throw new InvalidPostRequest(); + } + // If the adoption is successful, the adoption is in process + + logger.info("Adoption for pet {} created successfully ", adoption.getPet().getName()); + } + + /** + * Handles actions before saving an adoption. + * @param adoption The adoption object to be saved. + * @throws UnauthorizedAccessException If the user is not authorized. + */ + @HandleBeforeSave + public void handleAdoptionBeforeSave(Adoption adoption) throws UnauthorizedAccessException { + checkAuthorization("PUT"); + logger.info("Authorized save of adoption for pet {} ", adoption.getPet().getName()); + } + // This function is called after editing an adoption + @HandleAfterSave + public void handleAdoptionAfterSave(Adoption adoption) throws UnauthorizedAccessException { + adoption.getPet().setAdopted(true); + logger.info("Pet {} adopted successfully", adoption.getPet().getName()); + } + + + /** + * Checks if the user is authorized to create or edit an adoption. + * @param httpMethod The HTTP method used for the request. + * @throws UnauthorizedAccessException If the user is not authorized. + */ + private void checkAuthorization(String httpMethod) throws UnauthorizedAccessException { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (!isAuthorized(authentication, httpMethod)) { + throw new UnauthorizedAccessException(); + } + } + + /** + * Checks if the user is authorized and checks the role of the user. + * @param authentication The authentication object of the user. + * @param HTTPMethod The HTTP method used for the request. + * @return True if the user is authorized, false otherwise. + */ + private boolean isAuthorized(Authentication authentication, String HTTPMethod) { + if (authentication == null || !authentication.isAuthenticated()) { + return false; + } + List requiredAuthorities; + + if (HTTPMethod.equals("PUT")) { + requiredAuthorities = Arrays.asList(ROLE_SHELTER_VOLUNTEER, ROLE_ADMIN); + } + else if (HTTPMethod.equals("POST")) { + requiredAuthorities = Arrays.asList(ROLE_SHELTER_VOLUNTEER, ROLE_ADMIN, ROLE_USER); + } + else { + requiredAuthorities = Arrays.asList(ROLE_ADMIN, ROLE_SHELTER_VOLUNTEER); + } + return authentication.getAuthorities().stream() + .anyMatch(grantedAuthority -> requiredAuthorities.contains(grantedAuthority.getAuthority())); + } + + + +} diff --git a/src/main/java/cat/udl/eps/softarch/demo/repository/AdoptionRepository.java b/src/main/java/cat/udl/eps/softarch/demo/repository/AdoptionRepository.java new file mode 100644 index 00000000..1604313a --- /dev/null +++ b/src/main/java/cat/udl/eps/softarch/demo/repository/AdoptionRepository.java @@ -0,0 +1,9 @@ +package cat.udl.eps.softarch.demo.repository; + +import cat.udl.eps.softarch.demo.domain.Adoption; +import cat.udl.eps.softarch.demo.domain.Location; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; + +public interface AdoptionRepository extends CrudRepository, PagingAndSortingRepository { +} diff --git a/src/test/java/cat/udl/eps/softarch/demo/steps/ProcessAdoptionStepDefs.java b/src/test/java/cat/udl/eps/softarch/demo/steps/ProcessAdoptionStepDefs.java new file mode 100644 index 00000000..8183374a --- /dev/null +++ b/src/test/java/cat/udl/eps/softarch/demo/steps/ProcessAdoptionStepDefs.java @@ -0,0 +1,127 @@ +package cat.udl.eps.softarch.demo.steps; + + +import cat.udl.eps.softarch.demo.domain.Adoption; +import cat.udl.eps.softarch.demo.domain.Pet; +import cat.udl.eps.softarch.demo.repository.AdoptionRepository; +import cat.udl.eps.softarch.demo.repository.PetRepository; +import cat.udl.eps.softarch.demo.repository.UserRepository; +import io.cucumber.java.en.And; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.When; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; + +import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; + +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SuppressWarnings("ALL") +public class ProcessAdoptionStepDefs { + + + @Autowired + StepDefs stepDefs; + + @Autowired + UserRepository userRepository; + + @Autowired + PetRepository petRepository; + + @Autowired + AdoptionRepository adoptionRepository; + + protected ResultActions result; + + + @And("I receive a confirmation message for adopting the pet") + public void iReceiveAConfirmationMessageForAdoptingThePet() throws Throwable { + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.message", is("Adoption successful"))); + + } + + @Given("There is an available pet with name {string} i want to adopt") + public void thereIsAnAvailablePetWithName(String arg0) { + + Pet pet = new Pet(); + pet.setName(arg0); + pet.setAdopted(false); + pet.setColor("color"); + pet.setSize("size"); + pet.setWeight(1.0); + pet.setAge("age"); + pet.setDescription("description"); + pet.setBreed("breed"); + petRepository.save(pet); + + } + + @When("I request to adopt the pet with name {string}") + public void iRequestToAdoptThePetWithName(String arg0) throws Throwable { + + Adoption adoption = new Adoption(); + adoption.setPet(petRepository.findByName(arg0).get(0)); + adoption.setUser(userRepository.findAll().iterator().next()); + adoption.setStartDate(ZonedDateTime.now()); + adoption.setConfirmed(false); + adoption.setType("Adoption"); + adoption.setEndDate(null); + + stepDefs.result = stepDefs.mockMvc.perform( + post("/adoptions") + .contentType(MediaType.APPLICATION_JSON) + .content(stepDefs.mapper.writeValueAsString(adoption)) + .characterEncoding(StandardCharsets.UTF_8) + .with(AuthenticationStepDefs.authenticate())) + .andDo(print()); + + } + + + + @When("I request to adopt without a pet") + public void iRequestToAdoptWithoutAPet() throws Throwable{ + // Proceed with adoption logic + Adoption adoption = new Adoption(); + adoption.setUser(userRepository.findAll().iterator().next()); + adoption.setStartDate(ZonedDateTime.now()); + adoption.setConfirmed(false); + adoption.setType("Adoption"); + adoption.setEndDate(null); + + + stepDefs.result = stepDefs.mockMvc.perform( + post("/adoptions") + .contentType(MediaType.APPLICATION_JSON) + .content(stepDefs.mapper.writeValueAsString(adoption)) + .characterEncoding(StandardCharsets.UTF_8) + .with(AuthenticationStepDefs.authenticate())) + .andDo(print()); + + } + + @And("The pet with name {string} is already adopted") + public void thePetWithNameIsAlreadyAdopted(String arg0) { + + Pet pet = new Pet(); + pet.setName(arg0); + pet.setAdopted(true); + pet.setColor("color"); + pet.setSize("size"); + pet.setWeight(1.0); + pet.setAge("age"); + pet.setDescription("description"); + pet.setBreed("breed"); + petRepository.save(pet); + + + } +} diff --git a/src/test/java/cat/udl/eps/softarch/demo/steps/ValidateAdoptionStepDefs.java b/src/test/java/cat/udl/eps/softarch/demo/steps/ValidateAdoptionStepDefs.java new file mode 100644 index 00000000..1ddad609 --- /dev/null +++ b/src/test/java/cat/udl/eps/softarch/demo/steps/ValidateAdoptionStepDefs.java @@ -0,0 +1,81 @@ +package cat.udl.eps.softarch.demo.steps; + + + +import cat.udl.eps.softarch.demo.domain.Pet; +import cat.udl.eps.softarch.demo.domain.Adoption; +import cat.udl.eps.softarch.demo.repository.AdoptionRepository; +import cat.udl.eps.softarch.demo.repository.PetRepository; +import cat.udl.eps.softarch.demo.repository.UserRepository; +import io.cucumber.java.en.And; +import io.cucumber.java.en.When; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; + +import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; + +@SuppressWarnings("ALL") +public class ValidateAdoptionStepDefs { + + @Autowired + StepDefs stepDefs; + + @Autowired + UserRepository userRepository; + + @Autowired + PetRepository petRepository; + + @Autowired + AdoptionRepository adoptionRepository; + + protected ResultActions result; + + + @And("There is a dog with a pending adoption request from an user") + public void thereIsAPendingAdoptionRequestForPetFromUser() { + Pet pet = new Pet(); + pet.setName("Pet"); + pet.setAdopted(false); + pet.setColor("color"); + pet.setSize("size"); + pet.setWeight(1.0); + pet.setAge("age"); + pet.setDescription("description"); + pet.setBreed("breed"); + petRepository.save(pet); + + + Adoption adoption = new Adoption(); + adoption.setConfirmed(false); + adoption.setStartDate(ZonedDateTime.now()); + adoption.setUser(userRepository.findAll().iterator().next()); + adoption.setPet(petRepository.findAll().iterator().next()); + adoption.setType("Adoption"); + adoption.setEndDate(null); + adoptionRepository.save(adoption); + + } + + @When("I validate the adoption request") + public void iValidateTheAdoptionRequestForPetFromUser() throws Throwable { + Adoption existingAdoption = adoptionRepository.findAll().iterator().next(); + existingAdoption.setConfirmed(true); + + stepDefs.result = stepDefs.mockMvc.perform( + put("/adoptions/" + existingAdoption.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(stepDefs.mapper.writeValueAsString(existingAdoption)) + .characterEncoding(StandardCharsets.UTF_8) + .with(AuthenticationStepDefs.authenticate())) + .andDo(print()); + + } + + +} diff --git a/src/test/resources/features/ProcessAdoption.feature b/src/test/resources/features/ProcessAdoption.feature new file mode 100644 index 00000000..13d578d3 --- /dev/null +++ b/src/test/resources/features/ProcessAdoption.feature @@ -0,0 +1,34 @@ +Feature: Process Adoption (User) + In order to adopt a pet + As a user + I want to process adoption requests + + Background: + Given There is a registered user with username "username" and password "password" and email "user@sample.app" + + + Scenario: User is not logged in + Given I'm not logged in + And There is an available pet with name "pet" i want to adopt + When I request to adopt the pet with name "pet" + Then The response code is 401 + + Scenario: Process adoption request for available pet + Given I login as "username" with password "password" + And There is an available pet with name "pet" i want to adopt + When I request to adopt the pet with name "pet" + Then The response code is 201 + + + Scenario: Process adoption request without pet + Given I login as "username" with password "password" + When I request to adopt without a pet + Then The response code is 409 + + + + Scenario: Process adoption request for already adopted pet + Given I login as "username" with password "password" + And The pet with name "pet" is already adopted + When I request to adopt the pet with name "pet" + Then The response code is 409 diff --git a/src/test/resources/features/ValidateAdoption.feature b/src/test/resources/features/ValidateAdoption.feature new file mode 100644 index 00000000..4bfc76fe --- /dev/null +++ b/src/test/resources/features/ValidateAdoption.feature @@ -0,0 +1,31 @@ +Feature: Validate Adoption (Admin or Shelter Volunteer) + + In order to validate an adoption + As an admin or shelter volunteer + I want to review and approve adoption requests + + Background: + Given There is a registered user with username "username" and password "password" and email "user@sample.app" + And There is a registered admin with name "admin" and password "password" and email "admin@sample.app" + And There is a registered volunteer with name "volunteer" and password "password" and email "volunteer@sample.app" + And There is a dog with a pending adoption request from an user + + Scenario: Admin validates adoption request + Given I login as "admin" with password "password" + When I validate the adoption request + And The response code is 204 + + Scenario: Shelter volunteer validates adoption request + Given I login as "volunteer" with password "password" + When I validate the adoption request + And The response code is 204 + + Scenario: User validates adoption request + Given I login as "username" with password "password" + When I validate the adoption request + And The response code is 403 + + Scenario: User is not logged in + Given I'm not logged in + When I validate the adoption request + Then The response code is 401 \ No newline at end of file