diff --git a/security-c4po-api/security-c4po-api.postman_collection.json b/security-c4po-api/security-c4po-api.postman_collection.json index 334682f..5c1fe42 100644 --- a/security-c4po-api/security-c4po-api.postman_collection.json +++ b/security-c4po-api/security-c4po-api.postman_collection.json @@ -10,6 +10,9 @@ "item": [ { "name": "getProjects", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, "request": { "auth": { "type": "oauth2", @@ -38,6 +41,43 @@ }, "method": "GET", "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8443/projects", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8443", + "path": [ + "projects" + ] + } + }, + "response": [] + }, + { + "name": "saveProject", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"client\": \"Novatec\",\n \"title\": \"log4j Pentest\",\n \"tester\": \"Stipe\",\n \"createyBy\": \"10e06d7a-8dd0-4ecd-8963-056b45079c4f\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { "raw": "http://localhost:8443/projects", "protocol": "http", diff --git a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc index 08bfb63..dd6b4a9 100644 --- a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc +++ b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc @@ -39,10 +39,28 @@ include::{snippets}/getProjects/http-response.adoc[] include::{snippets}/getProjects/response-fields.adoc[] +=== Save project + +To save a project, call the POST request /projects + +==== Request example + +include::{snippets}/saveProject/http-request.adoc[] + +==== Response example + +include::{snippets}/saveProject/http-response.adoc[] + +==== Response structure + +include::{snippets}/saveProject/response-fields.adoc[] + == Change History |=== |Date |Change +|2021-12-22 +|Added POST endpoint to save Projects |2021-02-14 |Added GET endpoint to receive Projects |2021-02-12 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 017ee7f..4f4328c 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 @@ -37,3 +37,21 @@ fun ProjectOverview.toProjectOverviewResponseBody(): ResponseBody { "projects" to projects ) } + +data class ProjectRequestBody( + val client: String, + val title: String, + val tester: String? = null, + val createdBy: String +) + +fun ProjectRequestBody.toProject(): Project { + return Project( + id = UUID.randomUUID().toString(), + client = this.client, + title = this.title, + createdAt = Instant.now().toString(), + tester = this.tester, + createdBy = this.createdBy + ) +} 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 416e1b1..812d4ef 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 @@ -14,7 +14,7 @@ import reactor.core.publisher.Mono origins = [], allowCredentials = "false", allowedHeaders = ["*"], - methods = [RequestMethod.GET] + methods = [RequestMethod.GET, RequestMethod.POST] ) @SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION) class ProjectController(private val projectService: ProjectService) { @@ -23,12 +23,21 @@ class ProjectController(private val projectService: ProjectService) { @GetMapping fun getProjects(): Mono>> { - return projectService.getProjects().map { - it.map { + return projectService.getProjects().map { projectList -> + projectList.map { it.toProjectResponseBody() } }.map { ResponseEntity.ok(it) } } + + @PostMapping + fun saveProject( + @RequestBody body: ProjectRequestBody + ): Mono> { + return this.projectService.saveProject(body).map { + ResponseEntity.accepted().body(it.toProjectResponseBody()) + } + } } 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 98163c2..f1c8f1e 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 @@ -23,4 +23,14 @@ class ProjectService(private val projectRepository: ProjectRepository) { it.map { projectEntity -> projectEntity.toProject() } } } + + fun saveProject(body: ProjectRequestBody): Mono { + val project = body.toProject() + val projectEntity = ProjectEntity(project) + return projectRepository.insert(projectEntity).map { + it.toProject() + }.doOnError { + logger.warn("Project could not be stored in Database. Thrown exception: ", it) + } + } } 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 ab79926..73e424f 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 @@ -17,6 +17,7 @@ import org.springframework.restdocs.operation.preprocess.Preprocessors import org.springframework.restdocs.payload.JsonFieldType import org.springframework.restdocs.payload.PayloadDocumentation import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation +import reactor.core.publisher.Mono @SuppressFBWarnings( SIC_INNER_SHOULD_BE_STATIC, @@ -90,6 +91,43 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() { ) } + @Nested + inner class SaveProject { + @Test + fun saveProject() { + webTestClient.post().uri("/projects") + .header("Authorization", "Bearer $tokenAdmin") + .body(Mono.just(project), ProjectRequestBody::class.java) + .exchange() + .expectStatus().isAccepted + .expectHeader().valueEquals("Application-Name", "SecurityC4PO") + .expectBody().json(Json.write(project)) + .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 requested project"), + PayloadDocumentation.fieldWithPath("client").type(JsonFieldType.STRING).description("The name of the client of the requested project"), + PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING).description("The title of the requested project"), + PayloadDocumentation.fieldWithPath("createdAt").type(JsonFieldType.STRING).description("The date where the project was created at"), + PayloadDocumentation.fieldWithPath("tester").type(JsonFieldType.STRING).description("The 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 project = ProjectRequestBody( + client = "Novatec", + title = "log4j Pentest", + tester = "Stipe", + 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 fe56a2e..4c2dc87 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 @@ -15,6 +15,7 @@ import org.springframework.boot.web.server.LocalServerPort import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.query.Query import org.springframework.test.web.reactive.server.WebTestClient +import reactor.core.publisher.Mono import java.time.Duration @SuppressFBWarnings( @@ -87,6 +88,33 @@ class ProjectControllerIntTest : BaseIntTest() { ) } + @Nested + inner class SaveProject { + @Test + fun `save project successfully`() { + webTestClient.post().uri("/projects") + .header("Authorization", "Bearer $tokenAdmin") + .body(Mono.just(project), ProjectRequestBody::class.java) + .exchange() + .expectStatus().isAccepted + .expectHeader().valueEquals("Application-Name", "SecurityC4PO") + .expectBody().json(Json.write(project)) + .jsonPath("$.client").isEqualTo("Novatec") + .jsonPath("$.title").isEqualTo("log4j Pentest") + .jsonPath("$.tester").isEqualTo("Stipe") + .jsonPath("$.createdBy").isEqualTo("f8aab31f-4925-4242-a6fa-f98135b4b032") + } + + val project = Project( + id = "4f6567a8-76fd-487b-8602-f82d0ca4d1f9", + client = "Novatec", + title = "log4j Pentest", + createdAt = "2021-04-10T18:05:00Z", + tester = "Stipe", + 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/ProjectServiceTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectServiceTest.kt index cb78fa0..9847a6a 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectServiceTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectServiceTest.kt @@ -22,7 +22,6 @@ class ProjectServiceTest { inner class GetProjects { @Test fun `happy path - getProjects successfully`() { - } }