diff --git a/security-c4po-api/security-c4po-api.postman_collection.json b/security-c4po-api/security-c4po-api.postman_collection.json index 76ee6a9..b210825 100644 --- a/security-c4po-api/security-c4po-api.postman_collection.json +++ b/security-c4po-api/security-c4po-api.postman_collection.json @@ -98,6 +98,45 @@ "url": null }, "response": [] + }, + { + "name": "updateProject", + "request": { + "auth": { + "type": "oauth2", + "oauth2": [ + { + "key": "addTokenTo", + "value": "header", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"client\": \"updatedProject\",\n \"title\": \"log4j pentest\",\n \"tester\" : \"Stipe\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8443/projects/f2738715-4005-4aca-8d34-27ce9b8efffe", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8443", + "path": [ + "projects", + "f2738715-4005-4aca-8d34-27ce9b8efffe" + ] + } + }, + "response": [] } ] }, diff --git a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc index eda7418..14a4a76 100644 --- a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc +++ b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc @@ -71,10 +71,28 @@ include::{snippets}/deleteProject/http-response.adoc[] include::{snippets}/deleteProject/response-fields.adoc[] +=== Update project + +To update a project, call the PATCH request /projects/{projectId} + +==== Request example + +include::{snippets}/updateProject/http-request.adoc[] + +==== Response example + +include::{snippets}/updateProject/http-response.adoc[] + +==== Response structure + +include::{snippets}/updateProject/response-fields.adoc[] + == Change History |=== |Date |Change +|2022-03-07 +|Added PATCH endpoint to update Projects |2022-02-01 |Added DELETE endpoint to save Projects |2021-12-22 diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/BaseEntity.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/BaseEntity.kt index 8bc5572..9786b01 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/BaseEntity.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/BaseEntity.kt @@ -5,6 +5,7 @@ import com.securityc4po.api.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CO import com.securityc4po.api.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import org.springframework.data.annotation.Id +import java.time.Instant @SuppressFBWarnings( NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR, @@ -16,4 +17,11 @@ abstract class BaseEntity( ) { @Id lateinit var id: String + + var lastModified: Instant = Instant.now() + + fun setLastModifiedToCurrentInstant() { + this.lastModified = Instant.now() + } } + diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/Project.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/Project.kt index 960288b..5ee958c 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/Project.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/Project.kt @@ -50,10 +50,6 @@ data class ProjectRequestBody( val tester: String? = null ) -data class ProjectDeleteRequestBody( - val id: String -) - fun ProjectRequestBody.toProject(): Project { return Project( id = UUID.randomUUID().toString(), @@ -65,5 +61,4 @@ fun ProjectRequestBody.toProject(): Project { createdBy = UUID.randomUUID().toString() ) - } diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectController.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectController.kt index ae01d0d..de4440d 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectController.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectController.kt @@ -15,7 +15,7 @@ import java.util.* origins = [], allowCredentials = "false", allowedHeaders = ["*"], - methods = [RequestMethod.GET, RequestMethod.DELETE, RequestMethod.POST] + methods = [RequestMethod.GET, RequestMethod.DELETE, RequestMethod.POST, RequestMethod.PATCH] ) @SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION) @@ -49,4 +49,14 @@ class ProjectController(private val projectService: ProjectService) { ResponseEntity.ok().body(it.toProjectDeleteResponseBody()) } } + + @PatchMapping("/{id}") + fun updateProject( + @PathVariable(value = "id") id: String, + @RequestBody body: ProjectRequestBody + ): Mono> { + return this.projectService.updateProject(id, body).map { + ResponseEntity.accepted().body(it.toProjectResponseBody()) + } + } } diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectRepository.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectRepository.kt index b896237..ae1b7e4 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectRepository.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectRepository.kt @@ -14,4 +14,6 @@ interface ProjectRepository: ReactiveMongoRepository { @DeleteQuery("{'data._id' : ?0}") fun deleteProjectById(id: String): Mono + + } \ No newline at end of file diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt index e963b92..6e3cf93 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt @@ -7,6 +7,8 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import org.springframework.stereotype.Service import reactor.core.publisher.Mono import reactor.kotlin.core.publisher.switchIfEmpty +import java.time.Instant +import java.util.* @Service @@ -45,4 +47,30 @@ class ProjectService(private val projectRepository: ProjectRepository) { projectRepository.deleteProjectById(id).map{project} } } + + fun updateProject(id: String, body: ProjectRequestBody): Mono { + return projectRepository.findProjectById(id).switchIfEmpty{ + logger.info("Project with id $id not found. Updating not possible.") + Mono.empty() + }.flatMap{projectEntity: ProjectEntity -> + projectEntity.lastModified = Instant.now() + projectEntity.data = buildProject(body, projectEntity) + projectRepository.save(projectEntity).map{ + it.toProject() + }.doOnError { + logger.warn("Project could not be updated in Database. Thrown exception: ", it) + } + } + } } + +private fun buildProject(body: ProjectRequestBody, projectEntity: ProjectEntity): Project{ + return Project( + id = projectEntity.data.id, + client = body.client, + title = body.title, + createdAt = projectEntity.data.createdAt, + tester = body.tester, + createdBy = projectEntity.data.createdBy + ) +} \ No newline at end of file diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerDocumentationTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerDocumentationTest.kt index c5b87bd..251aaf2 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerDocumentationTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerDocumentationTest.kt @@ -189,6 +189,56 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() { ) } + @Nested + inner class UpdateProject { + @Test + fun updateProject() { + webTestClient.patch().uri("/projects/${projectUpdate.id}") + .header("Authorization", "Bearer $tokenAdmin") + .body(Mono.just(projectUpdate), ProjectRequestBody::class.java) + .exchange() + .expectStatus().isAccepted + .expectHeader().valueEquals("Application-Name", "SecurityC4PO") + .expectBody().json(Json.write(projectUpdate)) + .consumeWith( + WebTestClientRestDocumentation.document( + "{methodName}", + Preprocessors.preprocessRequest( + Preprocessors.prettyPrint(), + Preprocessors.modifyUris().removePort(), + Preprocessors.removeHeaders("Host", "Content-Length") + ), + Preprocessors.preprocessResponse( + Preprocessors.prettyPrint() + ), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING) + .description("The id of the updated project"), + PayloadDocumentation.fieldWithPath("client").type(JsonFieldType.STRING) + .description("The updated name of the client of the project"), + PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING) + .description("The updated title of the project"), + PayloadDocumentation.fieldWithPath("createdAt").type(JsonFieldType.STRING) + .description("The date where the project was created at"), + PayloadDocumentation.fieldWithPath("tester").type(JsonFieldType.STRING) + .description("The updated user that is assigned as a tester in the project"), + PayloadDocumentation.fieldWithPath("createdBy").type(JsonFieldType.STRING) + .description("The id of the user that created the project") + ) + ) + ) + } + + val projectUpdate = Project( + id = "4f6567a8-76fd-487b-8602-f82d0ca4d1f9", + client = "Novatec_updated", + title = "log4j Pentest_updated", + createdAt = "2021-01-10T18:05:00Z", + tester = "Stipe_updated", + createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032" + ) + } + private fun persistBasicTestScenario() { // setup test data val projectOne = Project( diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt index 9b89b47..529d2fd 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt @@ -127,15 +127,7 @@ class ProjectControllerIntTest : BaseIntTest() { .expectHeader().valueEquals("Application-Name", "SecurityC4PO") .expectBody().json(Json.write(projectTwo.toProjectDeleteResponseBody())) } - /*@Test - fun `delete project by non-existing id`() { - webTestClient.delete().uri("/projects/{id}", "98754a47-796b-4b3f-abf9-c46c668596c5") - .header("Authorization", "Bearer $tokenAdmin") - .exchange() - .expectStatus().isNoContent - .expectHeader().valueEquals("Application-Name", "SecurityC4PO") - .expectBody().isEmpty - }*/ + val projectTwo = Project( id = "61360a47-796b-4b3f-abf9-c46c668596c5", client = "Allsafe", @@ -146,6 +138,32 @@ class ProjectControllerIntTest : BaseIntTest() { ) } + @Nested + inner class UpdateProject { + @Test + fun `updated project successfully`() { + webTestClient.patch().uri("/projects/{id}", projectUpdate.id) + .header("Authorization", "Bearer $tokenAdmin") + .body(Mono.just(projectUpdate), ProjectRequestBody::class.java) + .exchange() + .expectStatus().isAccepted + .expectHeader().valueEquals("Application-Name", "SecurityC4PO") + .expectBody() + .jsonPath("$.client").isEqualTo("Novatec_updated") + .jsonPath("$.title").isEqualTo("log4j Pentest_updated") + .jsonPath("$.tester").isEqualTo("Stipe_updated") + } + + val projectUpdate = Project( + id = "4f6567a8-76fd-487b-8602-f82d0ca4d1f9", + client = "Novatec_updated", + title = "log4j Pentest_updated", + createdAt = "2021-04-10T18:05:00Z", + tester = "Stipe_updated", + createdBy = "a8891ad2-5cf5-4519-a89e-9ef8eec9e10c" + ) + } + private fun persistBasicTestScenario() { // setup test data val projectOne = Project(