feat: As a developer I want to delete projects

This commit is contained in:
Stipe Knez 2022-02-15 17:02:47 +01:00 committed by GitHub
parent 497ee99e92
commit 203b376ef1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 162 additions and 31 deletions

View File

@ -1,6 +1,6 @@
{ {
"info": { "info": {
"_postman_id": "a20516f0-5f7f-4d15-9f26-ada358993ff8", "_postman_id": "1823096a-1cb2-438d-9872-73ec0ca94cfa",
"name": "security-c4po-api", "name": "security-c4po-api",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
}, },
@ -10,9 +10,6 @@
"item": [ "item": [
{ {
"name": "getProjects", "name": "getProjects",
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": { "request": {
"auth": { "auth": {
"type": "oauth2", "type": "oauth2",
@ -41,9 +38,38 @@
}, },
"method": "GET", "method": "GET",
"header": [], "header": [],
"url": {
"raw": "http://localhost:8443/projects",
"protocol": "http",
"host": [
"localhost"
],
"port": "8443",
"path": [
"projects"
]
}
},
"response": []
},
{
"name": "saveProject",
"request": {
"auth": {
"type": "oauth2",
"oauth2": [
{
"key": "addTokenTo",
"value": "header",
"type": "string"
}
]
},
"method": "POST",
"header": [],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "", "raw": "{\n \"client\": \"Novatec\",\n \"title\": \"log4j pentest\",\n \"tester\" : \"Stipe\",\n \"createdBy\" : \"10e06d7a-8dd0-4ecd-8963-056b45079c4f\"\n}",
"options": { "options": {
"raw": { "raw": {
"language": "json" "language": "json"
@ -65,30 +91,11 @@
"response": [] "response": []
}, },
{ {
"name": "saveProject", "name": "deleteProject",
"request": { "request": {
"method": "POST", "method": "GET",
"header": [], "header": [],
"body": { "url": null
"mode": "raw",
"raw": "{\n \"client\": \"Novatec\",\n \"title\": \"log4j Pentest\",\n \"tester\": \"Stipe\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8443/projects",
"protocol": "http",
"host": [
"localhost"
],
"port": "8443",
"path": [
"projects"
]
}
}, },
"response": [] "response": []
} }

View File

@ -55,10 +55,28 @@ include::{snippets}/saveProject/http-response.adoc[]
include::{snippets}/saveProject/response-fields.adoc[] include::{snippets}/saveProject/response-fields.adoc[]
=== Delete project
To delete a project, call the DELETE request /projects/{projectId}
==== Request example
include::{snippets}/deleteProject/http-request.adoc[]
==== Response example
include::{snippets}/deleteProject/http-response.adoc[]
==== Response structure
include::{snippets}/deleteProject/response-fields.adoc[]
== Change History == Change History
|=== |===
|Date |Change |Date |Change
|2022-02-01
|Added DELETE endpoint to save Projects
|2021-12-22 |2021-12-22
|Added POST endpoint to save Projects |Added POST endpoint to save Projects
|2021-02-14 |2021-02-14

View File

@ -28,6 +28,12 @@ fun Project.toProjectResponseBody(): ResponseBody {
) )
} }
fun Project.toProjectDeleteResponseBody(): ResponseBody {
return mapOf(
"id" to id
)
}
data class ProjectOverview( data class ProjectOverview(
val projects: List<Project> val projects: List<Project>
) )
@ -44,6 +50,10 @@ data class ProjectRequestBody(
val tester: String? = null val tester: String? = null
) )
data class ProjectDeleteRequestBody(
val id: String
)
fun ProjectRequestBody.toProject(): Project { fun ProjectRequestBody.toProject(): Project {
return Project( return Project(
id = UUID.randomUUID().toString(), id = UUID.randomUUID().toString(),
@ -53,5 +63,7 @@ fun ProjectRequestBody.toProject(): Project {
tester = this.tester, tester = this.tester,
// ToDo: Should be changed to SUB from Token after adding AUTH Header // ToDo: Should be changed to SUB from Token after adding AUTH Header
createdBy = UUID.randomUUID().toString() createdBy = UUID.randomUUID().toString()
)
)
} }

View File

@ -15,8 +15,9 @@ import java.util.*
origins = [], origins = [],
allowCredentials = "false", allowCredentials = "false",
allowedHeaders = ["*"], allowedHeaders = ["*"],
methods = [RequestMethod.GET, RequestMethod.POST] methods = [RequestMethod.GET, RequestMethod.DELETE, RequestMethod.POST]
) )
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION) @SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION)
class ProjectController(private val projectService: ProjectService) { class ProjectController(private val projectService: ProjectService) {
@ -41,4 +42,11 @@ class ProjectController(private val projectService: ProjectService) {
ResponseEntity.accepted().body(it.toProjectResponseBody()) ResponseEntity.accepted().body(it.toProjectResponseBody())
} }
} }
@DeleteMapping("/{id}")
fun deleteProject(@PathVariable(value = "id") id: String): Mono<ResponseEntity<ResponseBody>> {
return this.projectService.deleteProject(id).map{
ResponseEntity.ok().body(it.toProjectDeleteResponseBody())
}
}
} }

View File

@ -1,5 +1,6 @@
package com.securityc4po.api.project package com.securityc4po.api.project
import org.springframework.data.mongodb.repository.DeleteQuery
import org.springframework.data.mongodb.repository.Query import org.springframework.data.mongodb.repository.Query
import org.springframework.data.mongodb.repository.ReactiveMongoRepository import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@ -10,4 +11,7 @@ interface ProjectRepository: ReactiveMongoRepository<ProjectEntity, String> {
@Query("{'data._id' : ?0}") @Query("{'data._id' : ?0}")
fun findProjectById(id: String): Mono<ProjectEntity> fun findProjectById(id: String): Mono<ProjectEntity>
@DeleteQuery("{'data._id' : ?0}")
fun deleteProjectById(id: String): Mono<Long>
} }

View File

@ -6,6 +6,8 @@ import com.securityc4po.api.extensions.getLoggerFor
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.switchIfEmpty
@Service @Service
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION) @SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION)
@ -33,4 +35,14 @@ class ProjectService(private val projectRepository: ProjectRepository) {
logger.warn("Project could not be stored in Database. Thrown exception: ", it) logger.warn("Project could not be stored in Database. Thrown exception: ", it)
} }
} }
fun deleteProject(id: String): Mono<Project> {
return projectRepository.findProjectById(id).switchIfEmpty{
logger.info("Project with id $id not found. Deletion not possible.")
Mono.empty()
}.flatMap{ projectEntity: ProjectEntity ->
val project = projectEntity.toProject()
projectRepository.deleteProjectById(id).map{project}
}
}
} }

View File

@ -39,7 +39,7 @@ abstract class BaseContainerizedTest {
companion object { companion object {
val mongoDbContainer = KGenericContainer(ImageFromDockerfile("c4poapibasecontainerizedtest").withDockerfileFromBuilder { val mongoDbContainer = KGenericContainer(ImageFromDockerfile("c4poapibasecontainerizedtest").withDockerfileFromBuilder {
it.from("mongo") it.from("mongo:4.4.6")
it.env("MONGO_INITDB_ROOT_USERNAME", "root") it.env("MONGO_INITDB_ROOT_USERNAME", "root")
it.env("MONGO_INITDB_ROOT_PASSWORD", "cjwkbencowepoc324pon2mop3mp4") it.env("MONGO_INITDB_ROOT_PASSWORD", "cjwkbencowepoc324pon2mop3mp4")
it.env("MONGO_INITDB_DATABASE", "admin") it.env("MONGO_INITDB_DATABASE", "admin")

View File

@ -149,6 +149,46 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() {
) )
} }
@Nested
inner class DeleteProject {
@Test
fun deleteProject() {
webTestClient.delete().uri("/projects/${project.id}")
.header("Authorization", "Bearer $tokenAdmin")
.exchange()
.expectStatus().isOk
.expectHeader().valueEquals("Application-Name", "SecurityC4PO")
.expectBody()
.json(Json.write(project.toProjectDeleteResponseBody()))
.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 deleted project")
)
)
)
}
val project = Project(
id = "4f6567a8-76fd-487b-8602-f82d0ca4d1f9",
client = "E Corp",
title = "Some Mock API (v1.0) Scanning",
createdAt = "2021-01-10T18:05:00Z",
tester = "Novatester",
createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032"
)
}
private fun persistBasicTestScenario() { private fun persistBasicTestScenario() {
// setup test data // setup test data
val projectOne = Project( val projectOne = Project(

View File

@ -116,6 +116,36 @@ class ProjectControllerIntTest : BaseIntTest() {
) )
} }
@Nested
inner class DeleteProject {
@Test
fun `deleted project successfully`() {
webTestClient.delete().uri("/projects/{id}", projectTwo.id)
.header("Authorization", "Bearer $tokenAdmin")
.exchange()
.expectStatus().isOk
.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",
title = "CashMyData (iOS)",
createdAt = "2021-01-10T18:05:00Z",
tester = "Elliot",
createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032"
)
}
private fun persistBasicTestScenario() { private fun persistBasicTestScenario() {
// setup test data // setup test data
val projectOne = Project( val projectOne = Project(

View File

@ -2,7 +2,7 @@ version: '3.1'
services: services:
c4po-db: c4po-db:
image: mongo:latest image: mongo:4.4.6
container_name: c4po-db container_name: c4po-db
volumes: volumes:
- ../volumes/mongodb/data:/data/db - ../volumes/mongodb/data:/data/db