diff --git a/api/pmt.yml b/api/pmt.yml index 8c70a51..3b22c8e 100644 --- a/api/pmt.yml +++ b/api/pmt.yml @@ -27,15 +27,80 @@ components: type: array items: $ref: "#/components/schemas/ProjectInfo" + CreateProjectDTO: + type: object + properties: + name: + type: string + goal: + type: string + customerId: + type: integer + format: int64 + administratorId: + type: integer + format: int64 + start: + type: string + format: date-time + plannedEnd: + type: string + format: date-time + CreatedProjectDTO: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + goal: + type: string + customerId: + type: integer + format: int64 + administratorId: + type: integer + format: int64 + start: + type: string + format: date-time + plannedEnd: + type: string + format: date-time responses: - UnAuthorized: - description: "Un Authorized" + Unauthorized: + description: "Unauthorized" + NotFound: + description: "Not Found" + content: + text/plain: + schema: + type: string + Conflict: + description: "Conflict" + content: + text/plain: + schema: + type: string + UnprocessableContent: + description: "Unprocessable Content" + content: + text/plain: + schema: + type: string InternalError: description: "Internal Server Error" content: text/plain: schema: type: string + ServiceUnavailable: + description: "Service Unavailable" + content: + text/plain: + schema: + type: string paths: /project: get: @@ -49,10 +114,38 @@ paths: schema: $ref: "#/components/schemas/GetAllProjectsDTO" 401: - $ref: "#/components/responses/UnAuthorized" + $ref: "#/components/responses/Unauthorized" 500: $ref: "#/components/responses/InternalError" - + post: + operationId: "CreateProject" + description: "Creates a new Project" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateProjectDTO" + responses: + 201: + description: "Project created successfully" + content: + application/json: + schema: + $ref: "#/components/schemas/CreatedProjectDTO" + 401: + $ref: "#/components/responses/Unauthorized" + 404: + $ref: "#/components/responses/NotFound" + 409: + $ref: "#/components/responses/Conflict" + 422: + $ref: "#/components/responses/UnprocessableContent" + 500: + $ref: "#/components/responses/InternalError" + 503: + $ref: "#/components/responses/ServiceUnavailable" + + /project/{id}: delete: operationId: "deleteProject" @@ -68,7 +161,7 @@ paths: 204: description: "Deletes a specific Project" 401: - $ref: "#/components/responses/UnAuthorized" + $ref: "#/components/responses/Unauthorized" 404: description: "Project not found" content: diff --git a/src/main/java/de/hmmh/pmt/ApiController.java b/src/main/java/de/hmmh/pmt/ApiController.java index bc05059..51799b5 100644 --- a/src/main/java/de/hmmh/pmt/ApiController.java +++ b/src/main/java/de/hmmh/pmt/ApiController.java @@ -1,29 +1,35 @@ package de.hmmh.pmt; import com.fasterxml.jackson.databind.ObjectMapper; -import de.hmmh.pmt.employee.ApiClientFactory; import de.hmmh.pmt.db.Project; import de.hmmh.pmt.db.ProjectRepository; -import de.hmmh.pmt.oas.DefaultApi; +import de.hmmh.pmt.dtos.CreateProjectDTO; +import de.hmmh.pmt.dtos.CreatedProjectDTO; import de.hmmh.pmt.dtos.GetAllProjectsDTO; import de.hmmh.pmt.dtos.ProjectInfo; +import de.hmmh.pmt.employee.ApiClientFactory; +import de.hmmh.pmt.oas.DefaultApi; +import de.hmmh.pmt.util.Mapper; import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestClientException; import java.util.Optional; @Controller @RequestMapping("${openapi.projectManagement.base-path:/api/v1}") public class ApiController implements DefaultApi { + @Autowired + private Mapper mapper; @Autowired private ApiClientFactory apiClientFactory; @Autowired private ProjectRepository projectRepository; - - // apiClientFactory.getEmployeeApi().findAll1() @Override public Optional getObjectMapper() { @@ -49,7 +55,7 @@ public class ApiController implements DefaultApi { public ResponseEntity getAllProjects() { GetAllProjectsDTO response = new GetAllProjectsDTO(); - for (Project project : this.projectRepository.findAll()){ + for (Project project : this.projectRepository.findAll()) { ProjectInfo projectInfo = new ProjectInfo(); projectInfo.setId(project.getId()); projectInfo.setName(project.getName()); @@ -58,4 +64,32 @@ public class ApiController implements DefaultApi { return ResponseEntity.ok(response); } + + @Override + public ResponseEntity createProject(CreateProjectDTO body) { + if (projectRepository.existsByName(body.getName())) { + return new ResponseEntity<>(HttpStatus.CONFLICT); + } + + try { + apiClientFactory.getEmployeeApi().findById(body.getAdministratorId()); + } catch (HttpClientErrorException exception) { + return new ResponseEntity<>( + exception.getStatusCode().equals(HttpStatus.NOT_FOUND) + ? HttpStatus.NOT_FOUND + : HttpStatus.SERVICE_UNAVAILABLE + ); + } catch (RestClientException exception) { + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + + Project project = mapper.map(body); + if (!project.isValid()) { + return new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY); + } + projectRepository.save(project); + + CreatedProjectDTO response = mapper.map(project); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } } diff --git a/src/main/java/de/hmmh/pmt/db/Project.java b/src/main/java/de/hmmh/pmt/db/Project.java index 9c9891f..0a70189 100644 --- a/src/main/java/de/hmmh/pmt/db/Project.java +++ b/src/main/java/de/hmmh/pmt/db/Project.java @@ -1,6 +1,9 @@ package de.hmmh.pmt.db; import jakarta.persistence.*; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -10,6 +13,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import java.time.LocalDateTime; +import java.util.Set; @NoArgsConstructor @AllArgsConstructor @@ -19,7 +23,7 @@ import java.time.LocalDateTime; @Table(name = "project") public class Project { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank @@ -43,5 +47,16 @@ public class Project { private LocalDateTime plannedEnd; private LocalDateTime realEnd; // Cant be named just "end" because it's and SQL Keyword + + + public boolean isValid() { + Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + Set> violations = validator.validate(this); + + return violations.isEmpty() && + plannedEnd.isAfter(start) && + (realEnd == null || realEnd.isAfter(start)); + } + } diff --git a/src/main/java/de/hmmh/pmt/db/ProjectRepository.java b/src/main/java/de/hmmh/pmt/db/ProjectRepository.java index ef501dc..bb02ece 100644 --- a/src/main/java/de/hmmh/pmt/db/ProjectRepository.java +++ b/src/main/java/de/hmmh/pmt/db/ProjectRepository.java @@ -3,4 +3,5 @@ package de.hmmh.pmt.db; import org.springframework.data.jpa.repository.JpaRepository; public interface ProjectRepository extends JpaRepository { + boolean existsByName(String name); } diff --git a/src/main/java/de/hmmh/pmt/util/Mapper.java b/src/main/java/de/hmmh/pmt/util/Mapper.java new file mode 100644 index 0000000..d5d7591 --- /dev/null +++ b/src/main/java/de/hmmh/pmt/util/Mapper.java @@ -0,0 +1,32 @@ +package de.hmmh.pmt.util; + +import de.hmmh.pmt.db.Project; +import de.hmmh.pmt.dtos.CreateProjectDTO; +import de.hmmh.pmt.dtos.CreatedProjectDTO; +import org.springframework.stereotype.Component; + +@Component +public class Mapper { + public Project map(CreateProjectDTO dto) { + Project project = new Project(); + project.setName(dto.getName()); + project.setGoal(dto.getGoal()); + project.setCustomerId(dto.getCustomerId()); + project.setAdministratorId(dto.getAdministratorId()); + project.setStart(dto.getStart()); + project.setPlannedEnd(dto.getPlannedEnd()); + return project; + } + + public CreatedProjectDTO map(Project project) { + CreatedProjectDTO dto = new CreatedProjectDTO(); + dto.setId(project.getId()); + dto.setName(project.getName()); + dto.setGoal(project.getGoal()); + dto.setCustomerId(project.getCustomerId()); + dto.setAdministratorId(project.getAdministratorId()); + dto.setStart(project.getStart()); + dto.setPlannedEnd(project.getPlannedEnd()); + return dto; + } +} diff --git a/src/test/java/de/hmmh/pmt/project/CreateTest.java b/src/test/java/de/hmmh/pmt/project/CreateTest.java new file mode 100644 index 0000000..1b162b3 --- /dev/null +++ b/src/test/java/de/hmmh/pmt/project/CreateTest.java @@ -0,0 +1,105 @@ +package de.hmmh.pmt.project; +import de.hmmh.pmt.IntegrationTest; +import de.hmmh.pmt.db.Project; +import de.hmmh.pmt.dtos.CreateProjectDTO; +import de.hmmh.pmt.employee.dtos.EmployeeResponseDTO; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.RequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestClientException; +import java.time.LocalDateTime; +import java.util.Map; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class CreateTest extends IntegrationTest { + + @Test + void successfullyCreate() throws Exception { + when(this.mockEmployeeApi.findById(Mockito.anyLong())) + .thenReturn(new EmployeeResponseDTO()); + + this.mvc + .perform(getRequest(getCreateProjectDTO())) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").exists()); + } + + @Test + void shouldNotCreateProjectWithSameName() throws Exception { + Map allProjects = createTestProjectData(); + Project spaceStation = allProjects.get("space-station"); + + CreateProjectDTO createDTO = getCreateProjectDTO(); + createDTO.setName(spaceStation.getName()); + + this.mvc + .perform(getRequest(createDTO)) + .andExpect(status().isConflict()); + } + + @Test + void shouldNotCreateProjectWhenAdministratorDoesNotExist() throws Exception { + when(this.mockEmployeeApi.findById(Mockito.anyLong())) + .thenThrow(new HttpClientErrorException(HttpStatus.NOT_FOUND)); + + this.mvc + .perform(getRequest(getCreateProjectDTO())) + .andExpect(status().isNotFound()); + } + + @Test + void shouldReturnUnavailableWhenEmployeeApiIsDown() throws Exception { + when(this.mockEmployeeApi.findById(Mockito.anyLong())) + .thenThrow(new HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR)); + + this.mvc + .perform(getRequest(getCreateProjectDTO())) + .andExpect(status().isServiceUnavailable()); + } + + @Test + void shouldReturnInternalServerErrorOnApiClientCrash() throws Exception { + when(this.mockEmployeeApi.findById(Mockito.anyLong())) + .thenThrow(new RestClientException("Api Client crash")); + + this.mvc + .perform(getRequest(getCreateProjectDTO())) + .andExpect(status().isInternalServerError()); + } + + @Test + void shouldReturnUnprocessableWhenDataIsInvalid() throws Exception { + CreateProjectDTO createDTO = getCreateProjectDTO(); + createDTO.setStart(LocalDateTime.of(2003, 1, 13, 12, 51)); + createDTO.setPlannedEnd(LocalDateTime.of(2002, 3, 21, 11, 42)); + + this.mvc + .perform(getRequest(createDTO)) + .andExpect(status().isUnprocessableEntity()); + } + + private CreateProjectDTO getCreateProjectDTO() { + CreateProjectDTO createDTO = new CreateProjectDTO(); + createDTO.setName("Test"); + createDTO.setGoal("A Test Goal"); + createDTO.setCustomerId(10L); + createDTO.setAdministratorId(10L); + createDTO.setStart(LocalDateTime.of(2000, 1, 13, 12, 51)); + createDTO.setPlannedEnd(LocalDateTime.of(2002, 3, 21, 11, 42)); + return createDTO; + } + + private RequestBuilder getRequest(CreateProjectDTO createDTO) throws Exception { + return MockMvcRequestBuilders + .post(baseUri + "/project") + .accept(MediaType.APPLICATION_JSON) + .content(this.objectMapper.writeValueAsString(createDTO)) + .contentType(MediaType.APPLICATION_JSON); + } +}