From 0fb0e77b914524e9a40463fa571fc867046101f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=A4ume?= Date: Wed, 23 Oct 2024 09:44:58 +0200 Subject: [PATCH 1/7] PMT-43: Define Endpoint --- api/pmt.yml | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/api/pmt.yml b/api/pmt.yml index e29a938..e45b4d5 100644 --- a/api/pmt.yml +++ b/api/pmt.yml @@ -46,6 +46,28 @@ components: plannedEnd: type: string format: date-time + UpdateProjectDTO: + 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 + end: + type: string + format: date-time CreatedProjectDTO: type: object properties: @@ -192,8 +214,7 @@ paths: $ref: "#/components/responses/InternalError" 503: $ref: "#/components/responses/ServiceUnavailable" - - + /project/{id}: post: operationId: "addEmployee" @@ -225,7 +246,36 @@ paths: $ref: "#/components/responses/InternalError" 503: $ref: "#/components/responses/ServiceUnavailable" - + put: + operationId: "updateProject" + description: "Updates a project" + parameters: + - in: path + name: id + schema: + type: integer + format: int64 + required: true + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateProjectDTO" + responses: + 204: + description: "Project updated successfully" + 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" delete: operationId: "deleteProject" description: "Delete a specific Project" From 5ca3603f65398c1f1a6116dc86a7a432e5e60a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=A4ume?= Date: Wed, 23 Oct 2024 09:46:05 +0200 Subject: [PATCH 2/7] NOTICKET: Relax the Static Analysis Rules for Mutability --- src/main/resources/spotbugs-exclude.xml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/resources/spotbugs-exclude.xml b/src/main/resources/spotbugs-exclude.xml index 6fab22b..df91845 100644 --- a/src/main/resources/spotbugs-exclude.xml +++ b/src/main/resources/spotbugs-exclude.xml @@ -1,12 +1,7 @@ - - - - - - - + + From 3af49e246607000fb52545aab27b44d6fc945513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=A4ume?= Date: Wed, 23 Oct 2024 09:48:18 +0200 Subject: [PATCH 3/7] PMT-43: Separate Validation Logic --- src/main/java/de/hmmh/pmt/ApiController.java | 22 ++++----- src/main/java/de/hmmh/pmt/db/Project.java | 12 ----- src/main/java/de/hmmh/pmt/util/Validator.java | 48 +++++++++++++++++++ 3 files changed, 59 insertions(+), 23 deletions(-) create mode 100644 src/main/java/de/hmmh/pmt/util/Validator.java diff --git a/src/main/java/de/hmmh/pmt/ApiController.java b/src/main/java/de/hmmh/pmt/ApiController.java index 6c57d91..19d7356 100644 --- a/src/main/java/de/hmmh/pmt/ApiController.java +++ b/src/main/java/de/hmmh/pmt/ApiController.java @@ -9,6 +9,7 @@ import de.hmmh.pmt.employee.api.EmployeeControllerApi; import de.hmmh.pmt.employee.dtos.EmployeeResponseDTO; import de.hmmh.pmt.oas.DefaultApi; import de.hmmh.pmt.util.Mapper; +import de.hmmh.pmt.util.Validator; import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -30,6 +31,10 @@ public class ApiController implements DefaultApi { @Autowired private ApiClientFactory apiClientFactory; @Autowired + private Validator validator; + @Autowired + private ApiTools apiTools; + @Autowired private ProjectRepository projectRepository; @Autowired AllocationRepository allocationRepository; @@ -87,7 +92,7 @@ public class ApiController implements DefaultApi { } Project project = mapper.map(body); - if (!project.isValid()) { + if (!validator.isValidProject(project)) { return new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY); } projectRepository.save(project); @@ -121,16 +126,11 @@ public class ApiController implements DefaultApi { return new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY); } - long start = project.getStart().toEpochSecond(ZoneOffset.UTC); - long plannedEnd = project.getPlannedEnd().toEpochSecond(ZoneOffset.UTC); - List allocations = allocationRepository.findAllByEmployeeId(body.getEmployeeId()); - if (allocations.stream() - .map(Allocation::getProject) - .anyMatch(allocatedProject -> { - long allocatedStart = allocatedProject.getStart().toEpochSecond(ZoneOffset.UTC); - long allocatedPlannedEnd = allocatedProject.getPlannedEnd().toEpochSecond(ZoneOffset.UTC); - return Math.max(start, allocatedStart) <= Math.min(plannedEnd, allocatedPlannedEnd); - })) { + if (validator.areAllocationTimeRangesOverlapping( + project.getStart(), + project.getPlannedEnd(), + allocationRepository.findAllByEmployeeId(body.getEmployeeId()) + )) { return new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY); } diff --git a/src/main/java/de/hmmh/pmt/db/Project.java b/src/main/java/de/hmmh/pmt/db/Project.java index 0a70189..2f973e6 100644 --- a/src/main/java/de/hmmh/pmt/db/Project.java +++ b/src/main/java/de/hmmh/pmt/db/Project.java @@ -47,16 +47,4 @@ 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/util/Validator.java b/src/main/java/de/hmmh/pmt/util/Validator.java new file mode 100644 index 0000000..f496d56 --- /dev/null +++ b/src/main/java/de/hmmh/pmt/util/Validator.java @@ -0,0 +1,48 @@ +package de.hmmh.pmt.util; + +import de.hmmh.pmt.db.Allocation; +import de.hmmh.pmt.db.Project; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Set; + +@Component +public class Validator { + + public boolean isValidProject(Project project) { + jakarta.validation.Validator validator = Validation + .buildDefaultValidatorFactory() + .getValidator(); + Set> violations = validator.validate(project); + + LocalDateTime start = project.getStart(); + LocalDateTime plannedEnd = project.getPlannedEnd(); + LocalDateTime realEnd = project.getRealEnd(); + + return violations.isEmpty() && + plannedEnd.isAfter(start) && + (realEnd == null || realEnd.isAfter(start)); + } + + public boolean areAllocationTimeRangesOverlapping( + LocalDateTime start, + LocalDateTime plannedEnd, + List allocations + ){ + long startUnix = start.toEpochSecond(ZoneOffset.UTC); + long plannedEndUnix = plannedEnd.toEpochSecond(ZoneOffset.UTC); + return allocations.stream() + .map(Allocation::getProject) + .anyMatch(allocatedProject -> { + long allocatedStart = allocatedProject.getStart().toEpochSecond(ZoneOffset.UTC); + long allocatedPlannedEnd = allocatedProject.getPlannedEnd().toEpochSecond(ZoneOffset.UTC); + return Math.max(startUnix, allocatedStart) <= Math.min(plannedEndUnix, allocatedPlannedEnd); + }); + } + +} From 224561f03881b89b262525ac117eefde51c8625f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=A4ume?= Date: Wed, 23 Oct 2024 09:49:36 +0200 Subject: [PATCH 4/7] PMT-43: Separate Repeated API Logic into ApiTools --- src/main/java/de/hmmh/pmt/ApiController.java | 29 ++++++----------- src/main/java/de/hmmh/pmt/util/ApiTools.java | 34 ++++++++++++++++++++ 2 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 src/main/java/de/hmmh/pmt/util/ApiTools.java diff --git a/src/main/java/de/hmmh/pmt/ApiController.java b/src/main/java/de/hmmh/pmt/ApiController.java index 19d7356..3843178 100644 --- a/src/main/java/de/hmmh/pmt/ApiController.java +++ b/src/main/java/de/hmmh/pmt/ApiController.java @@ -8,6 +8,7 @@ import de.hmmh.pmt.employee.ApiClientFactory; import de.hmmh.pmt.employee.api.EmployeeControllerApi; import de.hmmh.pmt.employee.dtos.EmployeeResponseDTO; import de.hmmh.pmt.oas.DefaultApi; +import de.hmmh.pmt.util.ApiTools; import de.hmmh.pmt.util.Mapper; import de.hmmh.pmt.util.Validator; import jakarta.servlet.http.HttpServletRequest; @@ -79,16 +80,9 @@ public class ApiController implements DefaultApi { 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); + HttpStatus status = apiTools.checkEmployeeExists(body.getAdministratorId()).status(); + if (status != HttpStatus.OK) { + return new ResponseEntity<>(status); } Project project = mapper.map(body); @@ -109,16 +103,13 @@ public class ApiController implements DefaultApi { } Project project = optionalProject.get(); - EmployeeResponseDTO employee; - try { - employee = apiClientFactory.getEmployeeApi().findById(body.getEmployeeId()); - } 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); + + ApiTools.CheckEmployeeRecord employeeRecord = apiTools.checkEmployeeExists(body.getEmployeeId()); + if (employeeRecord.status() != HttpStatus.OK) { + return new ResponseEntity<>(employeeRecord.status()); } + EmployeeResponseDTO employee = employeeRecord.employee(); + if (employee.getSkillSet() .stream() diff --git a/src/main/java/de/hmmh/pmt/util/ApiTools.java b/src/main/java/de/hmmh/pmt/util/ApiTools.java new file mode 100644 index 0000000..355a820 --- /dev/null +++ b/src/main/java/de/hmmh/pmt/util/ApiTools.java @@ -0,0 +1,34 @@ +package de.hmmh.pmt.util; + +import de.hmmh.pmt.employee.ApiClientFactory; +import de.hmmh.pmt.employee.dtos.EmployeeResponseDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestClientException; + +@Component +public class ApiTools { + + @Autowired + private ApiClientFactory apiClientFactory; + + public record CheckEmployeeRecord(EmployeeResponseDTO employee, HttpStatus status) {} + public CheckEmployeeRecord checkEmployeeExists(long id) { + EmployeeResponseDTO employee; + try { + employee =apiClientFactory.getEmployeeApi().findById(id); + } catch (HttpClientErrorException exception) { + return new CheckEmployeeRecord( + null, + exception.getStatusCode().equals(HttpStatus.NOT_FOUND) + ? HttpStatus.NOT_FOUND + : HttpStatus.SERVICE_UNAVAILABLE + ); + } catch (RestClientException exception) { + return new CheckEmployeeRecord(null, HttpStatus.INTERNAL_SERVER_ERROR); + } + return new CheckEmployeeRecord(employee, HttpStatus.OK); + } +} From 8abec2433df3bea11473a23e33e271a967e9509f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=A4ume?= Date: Wed, 23 Oct 2024 09:50:29 +0200 Subject: [PATCH 5/7] PMT-43: Implement Endpoint --- src/main/java/de/hmmh/pmt/ApiController.java | 42 ++++++++++++++++++++ src/main/java/de/hmmh/pmt/util/Mapper.java | 12 ++++++ 2 files changed, 54 insertions(+) diff --git a/src/main/java/de/hmmh/pmt/ApiController.java b/src/main/java/de/hmmh/pmt/ApiController.java index 3843178..c14b739 100644 --- a/src/main/java/de/hmmh/pmt/ApiController.java +++ b/src/main/java/de/hmmh/pmt/ApiController.java @@ -74,6 +74,48 @@ public class ApiController implements DefaultApi { return ResponseEntity.ok(response); } + @Override + public ResponseEntity updateProject(Long id, UpdateProjectDTO body) { + Optional optionalProject = projectRepository.findById(id); + if (optionalProject.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + Project project = optionalProject.get(); + + if (project.getName().equals(body.getName()) && projectRepository.existsByName(body.getName())) { + return new ResponseEntity<>(HttpStatus.CONFLICT); + } + + if (project.getAdministratorId().equals(body.getAdministratorId())) { + HttpStatus status = apiTools.checkEmployeeExists(body.getAdministratorId()).status(); + if (status != HttpStatus.OK) { + return new ResponseEntity<>(status); + } + } + + Project finalProject = mapper.map(project, body); + if (!validator.isValidProject(project)) { + return new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY); + } + + if (validator.areAllocationTimeRangesOverlapping( + project.getStart(), + project.getPlannedEnd(), + allocationRepository + .findAllByProjectId(finalProject.getId()) + .stream() + .map(Allocation::getEmployeeId) + .flatMap(employeeId -> allocationRepository.findAllByEmployeeId(employeeId).stream()) + .filter(employeeAllocation -> employeeAllocation.getProjectId().equals(finalProject.getId())) + .toList() + )){ + return new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY); + } + + projectRepository.save(project); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + @Override public ResponseEntity createProject(CreateProjectDTO body) { if (projectRepository.existsByName(body.getName())) { diff --git a/src/main/java/de/hmmh/pmt/util/Mapper.java b/src/main/java/de/hmmh/pmt/util/Mapper.java index 09ba201..dd0b7f7 100644 --- a/src/main/java/de/hmmh/pmt/util/Mapper.java +++ b/src/main/java/de/hmmh/pmt/util/Mapper.java @@ -7,6 +7,7 @@ import de.hmmh.pmt.dtos.Employee; import de.hmmh.pmt.dtos.Qualification ; import de.hmmh.pmt.employee.dtos.EmployeeResponseDTO; import de.hmmh.pmt.employee.dtos.QualificationGetDTO; +import de.hmmh.pmt.dtos.UpdateProjectDTO; import org.springframework.stereotype.Component; import java.util.List; @@ -61,4 +62,15 @@ public class Mapper { return dto; } + + public Project map(Project project, UpdateProjectDTO dto) { + project.setName(dto.getName()); + project.setGoal(dto.getGoal()); + project.setCustomerId(dto.getCustomerId()); + project.setAdministratorId(dto.getAdministratorId()); + project.setStart(dto.getStart()); + project.setPlannedEnd(dto.getPlannedEnd()); + project.setRealEnd(dto.getEnd()); + return project; + } } From 65836b77727061fe6f46afde976053066b4d91a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=A4ume?= Date: Wed, 23 Oct 2024 11:20:52 +0200 Subject: [PATCH 6/7] PMT-43: Add Separate Endpoint for Completing Project, because of Spring and Generation limitation --- api/pmt.yml | 27 +++++++++-- src/main/java/de/hmmh/pmt/ApiController.java | 14 ++++++ src/main/java/de/hmmh/pmt/util/Mapper.java | 1 - .../java/de/hmmh/pmt/project/EditTest.java | 48 +++++++++++++++++++ 4 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 src/test/java/de/hmmh/pmt/project/EditTest.java diff --git a/api/pmt.yml b/api/pmt.yml index e45b4d5..25ec77c 100644 --- a/api/pmt.yml +++ b/api/pmt.yml @@ -65,9 +65,6 @@ components: plannedEnd: type: string format: date-time - end: - type: string - format: date-time CreatedProjectDTO: type: object properties: @@ -299,6 +296,30 @@ paths: type: string 500: $ref: "#/components/responses/InternalError" + /project/{id}/completed: + post: + operationId: "completeProject" + description: "Complete a specific project" + parameters: + - in: path + name: id + schema: + type: integer + format: int64 + required: true + responses: + 204: + description: "Completed" + 401: + $ref: "#/components/responses/Unauthorized" + 404: + description: "Project not found" + content: + text/plain: + schema: + type: string + 500: + $ref: "#/components/responses/InternalError" /project/{id}/employee/{employeeId}: delete: diff --git a/src/main/java/de/hmmh/pmt/ApiController.java b/src/main/java/de/hmmh/pmt/ApiController.java index c14b739..495cd6f 100644 --- a/src/main/java/de/hmmh/pmt/ApiController.java +++ b/src/main/java/de/hmmh/pmt/ApiController.java @@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestClientException; +import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.*; import java.util.stream.Collectors; @@ -176,6 +177,19 @@ public class ApiController implements DefaultApi { return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + @Override + public ResponseEntity completeProject(Long id) { + Optional optionalProject = projectRepository.findById(id); + if (optionalProject.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + Project project = optionalProject.get(); + + project.setRealEnd(LocalDateTime.now()); + projectRepository.save(project); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + @Override public ResponseEntity removeEmployeeFromProject(Long id, Long employeeId){ Optional allocation = allocationRepository.findById(new AllocationId(id, employeeId)); diff --git a/src/main/java/de/hmmh/pmt/util/Mapper.java b/src/main/java/de/hmmh/pmt/util/Mapper.java index dd0b7f7..6c13bb7 100644 --- a/src/main/java/de/hmmh/pmt/util/Mapper.java +++ b/src/main/java/de/hmmh/pmt/util/Mapper.java @@ -70,7 +70,6 @@ public class Mapper { project.setAdministratorId(dto.getAdministratorId()); project.setStart(dto.getStart()); project.setPlannedEnd(dto.getPlannedEnd()); - project.setRealEnd(dto.getEnd()); return project; } } diff --git a/src/test/java/de/hmmh/pmt/project/EditTest.java b/src/test/java/de/hmmh/pmt/project/EditTest.java new file mode 100644 index 0000000..8542e9f --- /dev/null +++ b/src/test/java/de/hmmh/pmt/project/EditTest.java @@ -0,0 +1,48 @@ +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.dtos.UpdateProjectDTO; +import de.hmmh.pmt.employee.dtos.EmployeeResponseDTO; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.RequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import java.util.Map; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class EditTest extends IntegrationTest { + + @Test + void successfullyRenamed() throws Exception { + when(this.mockEmployeeApi.findById(Mockito.anyLong())) + .thenReturn(new EmployeeResponseDTO()); + + Map allProjects = createTestProjectData(); + Project projectToEdit = allProjects.get("medical-research"); + + UpdateProjectDTO dto = new UpdateProjectDTO(); + dto.setName("Mafia Drug Ring"); + dto.setGoal(projectToEdit.getGoal()); + dto.setCustomerId(projectToEdit.getCustomerId()); + dto.setAdministratorId(projectToEdit.getAdministratorId()); + dto.setStart(projectToEdit.getStart()); + dto.setPlannedEnd(projectToEdit.getPlannedEnd()); + + this.mvc + .perform(getRequest(projectToEdit.getId(), dto)) + .andExpect(status().isNoContent()); + } + + private RequestBuilder getRequest(long id, UpdateProjectDTO dto) throws Exception { + return MockMvcRequestBuilders + .put(baseUri + "/project/" + id) + .content(this.objectMapper.writeValueAsString(dto)) + .contentType(MediaType.APPLICATION_JSON); + } +} From 6b37fe7ae4acfc95f144d92d55299705f6baf686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=A4ume?= Date: Wed, 23 Oct 2024 12:42:39 +0200 Subject: [PATCH 7/7] PMT-43: Implement Tests and Minor Tweaks --- api/pmt.yml | 12 +- src/main/java/de/hmmh/pmt/ApiController.java | 9 +- .../java/de/hmmh/pmt/IntegrationTest.java | 21 ++++ .../de/hmmh/pmt/project/CompleteTest.java | 31 ++++++ .../java/de/hmmh/pmt/project/EditTest.java | 103 ++++++++++++++++-- 5 files changed, 153 insertions(+), 23 deletions(-) create mode 100644 src/test/java/de/hmmh/pmt/project/CompleteTest.java diff --git a/api/pmt.yml b/api/pmt.yml index 25ec77c..0a6419a 100644 --- a/api/pmt.yml +++ b/api/pmt.yml @@ -289,11 +289,7 @@ paths: 401: $ref: "#/components/responses/Unauthorized" 404: - description: "Project not found" - content: - text/plain: - schema: - type: string + $ref: "#/components/responses/NotFound" 500: $ref: "#/components/responses/InternalError" /project/{id}/completed: @@ -313,11 +309,7 @@ paths: 401: $ref: "#/components/responses/Unauthorized" 404: - description: "Project not found" - content: - text/plain: - schema: - type: string + $ref: "#/components/responses/NotFound" 500: $ref: "#/components/responses/InternalError" diff --git a/src/main/java/de/hmmh/pmt/ApiController.java b/src/main/java/de/hmmh/pmt/ApiController.java index 495cd6f..c29c60b 100644 --- a/src/main/java/de/hmmh/pmt/ApiController.java +++ b/src/main/java/de/hmmh/pmt/ApiController.java @@ -5,7 +5,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.hmmh.pmt.db.*; import de.hmmh.pmt.dtos.*; import de.hmmh.pmt.employee.ApiClientFactory; -import de.hmmh.pmt.employee.api.EmployeeControllerApi; import de.hmmh.pmt.employee.dtos.EmployeeResponseDTO; import de.hmmh.pmt.oas.DefaultApi; import de.hmmh.pmt.util.ApiTools; @@ -17,11 +16,9 @@ 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.time.LocalDateTime; -import java.time.ZoneOffset; import java.util.*; import java.util.stream.Collectors; @@ -83,7 +80,7 @@ public class ApiController implements DefaultApi { } Project project = optionalProject.get(); - if (project.getName().equals(body.getName()) && projectRepository.existsByName(body.getName())) { + if (!project.getName().equals(body.getName()) && projectRepository.existsByName(body.getName())) { return new ResponseEntity<>(HttpStatus.CONFLICT); } @@ -109,8 +106,8 @@ public class ApiController implements DefaultApi { .flatMap(employeeId -> allocationRepository.findAllByEmployeeId(employeeId).stream()) .filter(employeeAllocation -> employeeAllocation.getProjectId().equals(finalProject.getId())) .toList() - )){ - return new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY); + )) { + return new ResponseEntity<>(HttpStatus.CONFLICT); } projectRepository.save(project); diff --git a/src/test/java/de/hmmh/pmt/IntegrationTest.java b/src/test/java/de/hmmh/pmt/IntegrationTest.java index 26ccee2..0d2656d 100644 --- a/src/test/java/de/hmmh/pmt/IntegrationTest.java +++ b/src/test/java/de/hmmh/pmt/IntegrationTest.java @@ -114,6 +114,15 @@ public abstract class IntegrationTest { medicalResearchProject.setPlannedEnd(LocalDateTime.of(2026, 8, 20, 9, 15)); projects.put("medical-research", medicalResearchProject); + Project futureResearchProject = new Project(); + futureResearchProject.setName("What is the Future?"); + futureResearchProject.setGoal("Develop new Future (possibly through Genocide)."); + futureResearchProject.setCustomerId(95L); + futureResearchProject.setAdministratorId(TEST_EMPLOYEE_A_ID); + futureResearchProject.setStart(LocalDateTime.of(2077, 1, 12, 2, 0)); + futureResearchProject.setPlannedEnd(LocalDateTime.of(3000, 6, 19, 4, 0)); + projects.put("future-research", futureResearchProject); + Project overlappingProjectA = new Project(); overlappingProjectA.setName("Overlap A"); overlappingProjectA.setGoal("A Project That Overlaps with another one for Testing"); @@ -177,6 +186,18 @@ public abstract class IntegrationTest { allocation1ToAiResearch.setRole(TEST_QUALIFICATION_A_ID); allocations.put("1>ai-research", allocation1ToAiResearch); + Allocation allocation1ToMedical = new Allocation(); + allocation1ToMedical.setProject(allProjects.get("medical-research")); + allocation1ToMedical.setEmployeeId(TEST_EMPLOYEE_A_ID); + allocation1ToMedical.setRole(TEST_QUALIFICATION_A_ID); + allocations.put("1>medical-research", allocation1ToMedical); + + Allocation allocation1ToFuture = new Allocation(); + allocation1ToFuture.setProject(allProjects.get("future-research")); + allocation1ToFuture.setEmployeeId(TEST_EMPLOYEE_A_ID); + allocation1ToFuture.setRole(TEST_QUALIFICATION_A_ID); + allocations.put("1>future-research", allocation1ToFuture); + allocationRepository.saveAllAndFlush(allocations.values()); return allocations; } diff --git a/src/test/java/de/hmmh/pmt/project/CompleteTest.java b/src/test/java/de/hmmh/pmt/project/CompleteTest.java new file mode 100644 index 0000000..6f212f1 --- /dev/null +++ b/src/test/java/de/hmmh/pmt/project/CompleteTest.java @@ -0,0 +1,31 @@ +package de.hmmh.pmt.project; + +import de.hmmh.pmt.IntegrationTest; +import de.hmmh.pmt.db.Project; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class CompleteTest extends IntegrationTest { + + @Test + void shouldReturnNotFoundWhenProjectDoesntExist() throws Exception { + this.mvc + .perform(post(baseUri + "/project/0/completed")) + .andExpect(status().isNotFound()); + } + + @Test + void successfullyComplete() throws Exception { + + Map allProjects = createTestProjectData(); + Project projectToComplete = allProjects.get("medical-research"); + + this.mvc + .perform(post(baseUri + "/project/" + projectToComplete.getId() + "/completed")) + .andExpect(status().isNoContent()); + } +} diff --git a/src/test/java/de/hmmh/pmt/project/EditTest.java b/src/test/java/de/hmmh/pmt/project/EditTest.java index 8542e9f..7aedbc9 100644 --- a/src/test/java/de/hmmh/pmt/project/EditTest.java +++ b/src/test/java/de/hmmh/pmt/project/EditTest.java @@ -1,16 +1,19 @@ package de.hmmh.pmt.project; import de.hmmh.pmt.IntegrationTest; +import de.hmmh.pmt.db.Allocation; import de.hmmh.pmt.db.Project; -import de.hmmh.pmt.dtos.CreateProjectDTO; import de.hmmh.pmt.dtos.UpdateProjectDTO; 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 java.time.LocalDateTime; import java.util.Map; import static org.mockito.Mockito.when; @@ -26,19 +29,105 @@ public class EditTest extends IntegrationTest { Map allProjects = createTestProjectData(); Project projectToEdit = allProjects.get("medical-research"); - UpdateProjectDTO dto = new UpdateProjectDTO(); + UpdateProjectDTO dto = getDto(projectToEdit); dto.setName("Mafia Drug Ring"); - dto.setGoal(projectToEdit.getGoal()); - dto.setCustomerId(projectToEdit.getCustomerId()); - dto.setAdministratorId(projectToEdit.getAdministratorId()); - dto.setStart(projectToEdit.getStart()); - dto.setPlannedEnd(projectToEdit.getPlannedEnd()); this.mvc .perform(getRequest(projectToEdit.getId(), dto)) .andExpect(status().isNoContent()); } + @Test + void shouldReturnNotFoundWhenProjectDoesntExist() throws Exception { + when(this.mockEmployeeApi.findById(Mockito.anyLong())) + .thenReturn(new EmployeeResponseDTO()); + + Map allProjects = createTestProjectData(); + Project projectToEdit = allProjects.get("medical-research"); + + UpdateProjectDTO dto = getDto(projectToEdit); + + this.mvc + .perform(getRequest(0L, dto)) + .andExpect(status().isNotFound()); + } + + @Test + void shouldReturnNotFoundWhenNewAdministratorDoesntExist() throws Exception { + when(this.mockEmployeeApi.findById(Mockito.anyLong())) + .thenThrow(new HttpClientErrorException(HttpStatus.NOT_FOUND)); + + Map allProjects = createTestProjectData(); + Project projectToEdit = allProjects.get("medical-research"); + + UpdateProjectDTO dto = getDto(projectToEdit); + + this.mvc + .perform(getRequest(projectToEdit.getId(), dto)) + .andExpect(status().isNotFound()); + } + + @Test + void shouldReturnUnprocessableWhenNewTimestampsAreInvalid() throws Exception { + when(this.mockEmployeeApi.findById(Mockito.anyLong())) + .thenReturn(new EmployeeResponseDTO()); + + Map allProjects = createTestProjectData(); + Project projectToEdit = allProjects.get("medical-research"); + + UpdateProjectDTO dto = getDto(projectToEdit); + dto.setPlannedEnd(dto.getStart().minusDays(1)); + + this.mvc + .perform(getRequest(projectToEdit.getId(), dto)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + void shouldReturnConflictWhenUpdateWouldCreateNameConflict() throws Exception { + when(this.mockEmployeeApi.findById(Mockito.anyLong())) + .thenReturn(new EmployeeResponseDTO()); + + Map allProjects = createTestProjectData(); + Project projectToEdit = allProjects.get("medical-research"); + + UpdateProjectDTO dto = getDto(projectToEdit); + dto.setName(allProjects.get("renewable-energy").getName()); + + this.mvc + .perform(getRequest(projectToEdit.getId(), dto)) + .andExpect(status().isConflict()); + } + + @Test + void shouldReturnConflictWhenAllocationsWouldConflictAfterUpdate() throws Exception { + when(this.mockEmployeeApi.findById(Mockito.anyLong())) + .thenReturn(new EmployeeResponseDTO()); + + Map allProjects = createTestProjectData(); + Map allAllocations = createTestAllocationData(allProjects); + Project projectToEdit = allAllocations.get("1>future-research").getProject(); + + UpdateProjectDTO dto = getDto(projectToEdit); + dto.setStart(LocalDateTime.of(1970, 1, 1, 0, 0)); + dto.setStart(LocalDateTime.of(3000, 1, 1, 0, 0)); + + this.mvc + .perform(getRequest(projectToEdit.getId(), dto)) + .andExpect(status().isConflict()); + } + + private UpdateProjectDTO getDto(Project project) { + UpdateProjectDTO dto = new UpdateProjectDTO(); + 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; + } + private RequestBuilder getRequest(long id, UpdateProjectDTO dto) throws Exception { return MockMvcRequestBuilders .put(baseUri + "/project/" + id)