diff --git a/README.md b/README.md index 8246a53..47b212d 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ * Postman ## Application Architecture -![alt architecture](./wiki/SecurityC4PO_Architecture.png) +![alt architecture](./wiki/C4PO-Architecture.png) ## Data Structure -![alt architecture](./wiki/SecurityC4PO_Data_Structure.png) +![alt datastructure](./wiki/C4PO-Datastructure.png) ### Conventions * Branch: `_c4po_` @@ -38,3 +38,6 @@ Execute 'c4po.sh' and all services will run on a dev server. ### Testuser Credentials: * Username: ttt * Password: Test1234! + +## C4PO Roadmap +![alt roadmap](./wiki/C4PO-Roadmap.png) diff --git a/security-c4po-api/build.gradle.kts b/security-c4po-api/build.gradle.kts index f200e78..9e74c6a 100644 --- a/security-c4po-api/build.gradle.kts +++ b/security-c4po-api/build.gradle.kts @@ -60,6 +60,7 @@ spotbugs { val snippetsDir = file("build/generated-snippets") dependencies { + implementation("org.json:json:20140107") implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:2.11.3") implementation("org.springframework.boot:spring-boot-starter-data-mongodb") implementation("org.springframework.boot:spring-boot-starter-data-mongodb-reactive") @@ -76,7 +77,10 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-oauth2-client") implementation("org.modelmapper:modelmapper:2.3.2") + api("org.springframework.boot:spring-boot-starter-test") api("org.springframework.security:spring-security-jwt:1.1.1.RELEASE") + api("net.logstash.logback:logstash-logback-encoder:6.2") + api("ch.qos.logback:logback-classic:1.2.3") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0") diff --git a/security-c4po-api/security-c4po-api.postman_collection.json b/security-c4po-api/security-c4po-api.postman_collection.json index b210825..b51782d 100644 --- a/security-c4po-api/security-c4po-api.postman_collection.json +++ b/security-c4po-api/security-c4po-api.postman_collection.json @@ -1,8 +1,9 @@ { "info": { - "_postman_id": "1823096a-1cb2-438d-9872-73ec0ca94cfa", + "_postman_id": "6537da59-5c7a-478d-bf24-09a39022a690", "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", + "_exporter_id": "5225213" }, "item": [ { @@ -93,9 +94,34 @@ { "name": "deleteProject", "request": { - "method": "GET", + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NTcwNDI3NDcsImlhdCI6MTY1NzA0MjQ0NywianRpIjoiZGFjYWY0MzItNWRlMS00ZGU1LWE0ZjgtZmExNmYyNDMwMDRhIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiI3Nzc1ZGExMS0xYWI3LTQyZjItYjJmZC0yNDFmZTE0NjAyYTgiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImM0cG9fdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJjNHBvX2xvY2FsIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6InRlc3QgdXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6InR0dCIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ1c2VyIn0.EqTkweqw6KkmttmI7KyvZM-yoo4MczNo8Nlj1zRUHgzXDnQ2JbSCXLAFjvUFYTrCrGIlTn23Ojsx4WhVwvIkBmKmn8ZsrCifNwJfOYKbtu6rV0unMTJqXV1imdaRecti95wJLnFdKQf_gIPUALLzTIXH_klPZfz5zKup7OfWMXlrKhRHRzYbg0hFHBFlpd9QCYiNWzh4Z2_vq-V2YESViuCPxN6sFacR_fvz6-d2y-zWS6XHvHdblLBPKsMIn9EBTGfx49TQo-CDgUichi_w8VWMkk3vUyRUH2wl-CIz2qrYdA5y-PzAPju5yTxjgydGn-7LIIFCiOpDStdREPyFBA", + "type": "string" + }, + { + "key": "undefined", + "type": "any" + } + ] + }, + "method": "DELETE", "header": [], - "url": null + "url": { + "raw": "http://localhost:8443/projects/41051d0a-63ef-4290-b984-e6fbd736f218", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8443", + "path": [ + "projects", + "41051d0a-63ef-4290-b984-e6fbd736f218" + ] + } }, "response": [] }, diff --git a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc index 14a4a76..9e1789e 100644 --- a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc +++ b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc @@ -67,6 +67,10 @@ include::{snippets}/deleteProject/http-request.adoc[] include::{snippets}/deleteProject/http-response.adoc[] +If the project has already been deleted "204 No Content" is following. + +include::{snippets}/deleteNotExistingProject/http-response.adoc[] + ==== Response structure include::{snippets}/deleteProject/response-fields.adoc[] diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/ErrorCodeEncoder.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/ErrorCodeEncoder.kt new file mode 100644 index 0000000..88b30ee --- /dev/null +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/ErrorCodeEncoder.kt @@ -0,0 +1,16 @@ +package com.securityc4po.api.configuration.error.handler + +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.classic.spi.ThrowableProxy + +class ErrorCodeEncoder { + /*override*/ fun convert(event: ILoggingEvent): String { + return if (event.throwableProxy != null) { + val throwable = event.throwableProxy as ThrowableProxy + val ex = throwable.throwable as C4POBaseException + ex.errorcode.code.toString() + } else { + "" + } + } +} \ No newline at end of file diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/Errorcode.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/Errorcode.kt new file mode 100644 index 0000000..6d60e48 --- /dev/null +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/Errorcode.kt @@ -0,0 +1,37 @@ +package com.securityc4po.api.configuration.error.handler + +enum class Errorcode(val code: Int) { + // 1XXX Information Not Found + ProjectsNotFound(1001), + ProjectNotFound(1002), + PentestNotFound(1003), + + // 2XXX Already Changed + ProjectAlreadyChanged(2001), + PentestAlreadyChanged(2002), + + // 3XXX Invalid Model + ProjectInvalid(3000), + PentestInvalid(3001), + InsufficientData(3002), + InvalidToken(3003), + TokenWithoutField(3004), + UserIdIsEmpty(3005), + + // 4XXX Unauthorized + ProjectAdjustmentNotAuthorized(4000), + PentestAdjustmentNotAuthorized(4001), + + // 5XXX Server Errors + UserIdDoesNotMatch(5000), + + // 6XXX Failed transaction + ProjectDeletionFailed(6000), + PentestDeletionFailed(6001), + ProjectUpdateFailed(6002), + PentestUpdateFailed(6003), + ProjectFetchingFailed(6004), + PentestFetchingFailed(6005), + ProjectInsertionFailed(6006), + PentestInsertionFailed(6007), +} \ No newline at end of file diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/GlobalErrorAttributes.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/GlobalErrorAttributes.kt new file mode 100644 index 0000000..07f77d8 --- /dev/null +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/GlobalErrorAttributes.kt @@ -0,0 +1,79 @@ +package com.securityc4po.api.configuration.error.handler + +import org.springframework.boot.web.error.ErrorAttributeOptions +import org.springframework.boot.web.reactive.error.DefaultErrorAttributes +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClientResponseException +import org.springframework.web.reactive.function.server.ServerRequest +import org.json.JSONObject + +@Component +class GlobalErrorAttributes : DefaultErrorAttributes() { + override fun getErrorAttributes(request: ServerRequest, options: ErrorAttributeOptions): Map { + val map = super.getErrorAttributes(request, options) + + ///////////////////////// + ////// Exceptions /////// + ///////////////////////// + + if (getError(request) is EntityNotFoundException) { + val ex = getError(request) as EntityNotFoundException + map.put("message", ex.errormessage) + map.put("errorcode", ex.errorcode.code) + map.put("status", ex.status.value()) + map.put("error", ex.status.reasonPhrase) + return map + } + + if (getError(request) is EntityAlreadyChangedException) { + val ex = getError(request) as EntityAlreadyChangedException + map.put("message", ex.errormessage) + map.put("errorcode", ex.errorcode.code) + map.put("status", ex.status.value()) + map.put("error", ex.status.reasonPhrase) + return map + } + + if (getError(request) is InvalidModelException) { + val ex = getError(request) as InvalidModelException + map.put("message", ex.errormessage) + map.put("errorcode", ex.errorcode.code) + map.put("status", ex.status.value()) + map.put("error", ex.status.reasonPhrase) + return map + } + + if (getError(request) is UnauthorizedException) { + val ex = getError(request) as UnauthorizedException + map.put("message", ex.errormessage) + map.put("errorcode", ex.errorcode.code) + map.put("status", ex.status.value()) + map.put("error", ex.status.reasonPhrase) + return map + } + + if (getError(request) is TransactionInterruptedException) { + val ex = getError(request) as TransactionInterruptedException + map.put("message", ex.errormessage) + map.put("errorcode", ex.errorcode.code) + map.put("status", ex.status.value()) + map.put("error", ex.status.reasonPhrase) + return map + } + + if (getError(request) is WebClientResponseException) { + val ex = getError(request) as WebClientResponseException + val body = JSONObject(ex.responseBodyAsString) + map.put("message", body.get("message")) + map.put("errorcode", body.get("errorcode")) + map.put("status", ex.rawStatusCode) + map.put("error", ex.statusText) + return map + } + + map.put("message", map["message"]) + map.put("status", map["status"] as Int) + map.put("error", map["error"]) + return map + } +} \ No newline at end of file diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/GlobalErrorWebExceptionHandler.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/GlobalErrorWebExceptionHandler.kt new file mode 100644 index 0000000..3639bc4 --- /dev/null +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/GlobalErrorWebExceptionHandler.kt @@ -0,0 +1,70 @@ +package com.securityc4po.api.configuration.error.handler + +import org.springframework.web.reactive.function.BodyInserters +import org.springframework.http.HttpStatus +import org.springframework.boot.autoconfigure.web.ResourceProperties +import org.springframework.http.codec.ServerCodecConfigurer +import org.springframework.context.ApplicationContext +import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler +import org.springframework.boot.web.error.ErrorAttributeOptions +import org.springframework.boot.web.reactive.error.ErrorAttributes +import org.springframework.core.annotation.Order +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.server.* +import org.springframework.web.server.ResponseStatusException +import reactor.core.publisher.Mono + +@Component +@Order(-2) +class GlobalErrorWebExceptionHandler(g: GlobalErrorAttributes, applicationContext: ApplicationContext, + serverCodecConfigurer: ServerCodecConfigurer) : AbstractErrorWebExceptionHandler(g, ResourceProperties(), applicationContext) { + + init { + super.setMessageWriters(serverCodecConfigurer.writers) + super.setMessageReaders(serverCodecConfigurer.readers) + } + + override fun getRoutingFunction(errorAttributes: ErrorAttributes): RouterFunction { + return RouterFunctions.route(RequestPredicates.all(), HandlerFunction { this.renderErrorResponse(it) }) + } + + private fun renderErrorResponse(request: ServerRequest): Mono { + val errorPropertiesMap = getErrorAttributes(request, ErrorAttributeOptions.defaults()) + return ServerResponse.status(HttpStatus.valueOf(errorPropertiesMap["status"] as Int)) + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(errorPropertiesMap)) + } +} + +open class C4POBaseException (val errorcode: Errorcode, httpStatus: HttpStatus): ResponseStatusException(httpStatus) +internal class EntityNotFoundException(val errormessage: String, code: Errorcode): C4POBaseException(code, HttpStatus.NOT_FOUND) +internal class EntityAlreadyChangedException(val errormessage: String, code: Errorcode) : C4POBaseException(code, HttpStatus.CONFLICT) +internal class InvalidModelException (val errormessage: String, code: Errorcode) : C4POBaseException(code, HttpStatus.BAD_REQUEST) +internal class UnauthorizedException (val errormessage: String, code: Errorcode) : C4POBaseException(code, HttpStatus.UNAUTHORIZED) +internal class TransactionInterruptedException(val errormessage: String, code: Errorcode): C4POBaseException(code, HttpStatus.FAILED_DEPENDENCY) + +/** + * This method is used to throw an exception, and log a message if needed, if a certain condition is true. + * + * @param require of type boolean. It is the condition to check on. + * @param logging lambda expression for optional logging. + * @param mappedException of type OpenSpaceBaseException. + */ +inline fun validate(require: Boolean, logging: () -> Unit, mappedException: C4POBaseException) { + if (!require) { + throw wrappedException(logging= { logging() }, mappedException = mappedException) + } +} + +/** + * This method is used to reduce some lines of code when we throw an exception. + * It has an optional logging part using a lambda expression. + * + * @param logging lambda expression for optional logging. + * @param mappedException of type OpenSpaceBaseException. + */ +inline fun wrappedException(logging: () -> Unit, mappedException: C4POBaseException): C4POBaseException { + logging() + return mappedException +} 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 5ee958c..46cc0f2 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 @@ -17,6 +17,17 @@ data class Project( val createdBy: String ) +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 + ) +} + fun Project.toProjectResponseBody(): ResponseBody { return mapOf( "id" to id, @@ -47,9 +58,23 @@ fun ProjectOverview.toProjectOverviewResponseBody(): ResponseBody { data class ProjectRequestBody( val client: String, val title: String, - val tester: String? = null + val tester: String ) +/** + * Validates if a [ProjectRequestBody] is valid + * + * @return Boolean describing if the body is valid + */ +fun ProjectRequestBody.isValid(): Boolean { + return when { + this.client.isBlank() -> false + this.title.isBlank() -> false + this.tester.isBlank() -> false + else -> true + } +} + fun ProjectRequestBody.toProject(): Project { return Project( id = 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 de4440d..6c661f2 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 @@ -7,7 +7,7 @@ import com.securityc4po.api.ResponseBody import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import reactor.core.publisher.Mono -import java.util.* +import reactor.kotlin.core.publisher.switchIfEmpty @RestController @RequestMapping("/projects") @@ -47,6 +47,8 @@ class ProjectController(private val projectService: ProjectService) { fun deleteProject(@PathVariable(value = "id") id: String): Mono> { return this.projectService.deleteProject(id).map{ ResponseEntity.ok().body(it.toProjectDeleteResponseBody()) + }.switchIfEmpty { + Mono.just(ResponseEntity.noContent().build()) } } 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 6e3cf93..12c7aea 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 @@ -2,14 +2,15 @@ package com.securityc4po.api.project import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION import com.securityc4po.api.configuration.MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION +import com.securityc4po.api.configuration.error.handler.* +import com.securityc4po.api.configuration.error.handler.EntityNotFoundException +import com.securityc4po.api.configuration.error.handler.InvalidModelException import com.securityc4po.api.extensions.getLoggerFor 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 @SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION) @@ -20,35 +21,86 @@ class ProjectService(private val projectRepository: ProjectRepository) { /** * Get all [Project]s * + * @throws [EntityNotFoundException] if there are no [Project]s in collection * @return list of [Project] */ fun getProjects(): Mono> { return projectRepository.findAll().collectList().map { it.map { projectEntity -> projectEntity.toProject() } + }.switchIfEmpty { + val msg = "No projects not found." + val ex = EntityNotFoundException(msg, Errorcode.ProjectsNotFound) + logger.warn(msg, ex) + throw ex } } + /** + * Save [Project] + * + * @throws [InvalidModelException] if the [Project] is invalid + * @throws [TransactionInterruptedException] if the [Project] could not be stored + * @return saved [Project] + */ fun saveProject(body: ProjectRequestBody): Mono { + validate( + require = body.isValid(), + logging = { logger.warn("Project not valid.") }, + mappedException = InvalidModelException( + "Project not valid.", Errorcode.ProjectInvalid + ) + ) 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) + throw wrappedException( + logging = { logger.warn("Project could not be stored in Database. Thrown exception: ", it) }, + mappedException = TransactionInterruptedException( + "Project could not be stored.", + Errorcode.ProjectInsertionFailed + ) + ) } } + /** + * Delete [Project] + * + * @throws [TransactionInterruptedException] if the [Project] could not be deleted + * @return status code of deleted [Project] + */ fun deleteProject(id: String): Mono { return projectRepository.findProjectById(id).switchIfEmpty{ - logger.info("Project with id $id not found. Deletion not possible.") + logger.info("Project with id $id not found. Deletion not necessary.") Mono.empty() }.flatMap{ projectEntity: ProjectEntity -> val project = projectEntity.toProject() projectRepository.deleteProjectById(id).map{project} + }.onErrorMap { + TransactionInterruptedException( + "Deleting Project failed!", + Errorcode.ProjectDeletionFailed + ) } } + /** + * Update [Project] + * + * @throws [InvalidModelException] if the [Project] is invalid + * @throws [TransactionInterruptedException] if the [Project] could not be updated + * @return updated [Project] + */ fun updateProject(id: String, body: ProjectRequestBody): Mono { + validate( + require = body.isValid(), + logging = { logger.warn("Project not valid.") }, + mappedException = InvalidModelException( + "Project not valid.", Errorcode.ProjectInvalid + ) + ) return projectRepository.findProjectById(id).switchIfEmpty{ logger.info("Project with id $id not found. Updating not possible.") Mono.empty() @@ -58,19 +110,14 @@ class ProjectService(private val projectRepository: ProjectRepository) { projectRepository.save(projectEntity).map{ it.toProject() }.doOnError { - logger.warn("Project could not be updated in Database. Thrown exception: ", it) + throw wrappedException( + logging = { logger.warn("Project could not be updated in Database. Thrown exception: ", it) }, + mappedException = TransactionInterruptedException( + "Project could not be updated.", + Errorcode.ProjectInsertionFailed + ) + ) } } } } - -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 251aaf2..7f96346 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 @@ -16,6 +16,7 @@ import org.springframework.data.mongodb.core.query.Query import org.springframework.restdocs.operation.preprocess.Preprocessors import org.springframework.restdocs.payload.JsonFieldType import org.springframework.restdocs.payload.PayloadDocumentation +import org.springframework.restdocs.request.RequestDocumentation import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation import reactor.core.publisher.Mono @@ -179,6 +180,34 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() { ) } + @Test + fun deleteNotExistingProject() { + val randomUUID = "f85ee127-83b7-4ba3-8940-7b8d1e0a1c6e" + webTestClient.delete().uri("/projects/{id}", randomUUID) + .header("Authorization", "Bearer $tokenAdmin") + .exchange() + .expectStatus().isNoContent + .expectHeader().valueEquals("Application-Name", "SecurityC4PO") + .expectBody() + .consumeWith( + WebTestClientRestDocumentation.document( + "{methodName}", + Preprocessors.preprocessRequest( + Preprocessors.prettyPrint(), + Preprocessors.modifyUris().removePort(), + Preprocessors.removeHeaders("Host", "Content-Length") + ), + Preprocessors.preprocessResponse( + Preprocessors.prettyPrint() + ), + RequestDocumentation.relaxedPathParameters( + RequestDocumentation.parameterWithName("id") + .description("The id of the deleted project") + ) + ) + ) + } + val project = Project( id = "4f6567a8-76fd-487b-8602-f82d0ca4d1f9", client = "E Corp", 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 529d2fd..6707db4 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 @@ -128,6 +128,17 @@ class ProjectControllerIntTest : BaseIntTest() { .expectBody().json(Json.write(projectTwo.toProjectDeleteResponseBody())) } + @Test + fun `delete not existing project`() { + val randomUUID = "f85ee127-83b7-4ba3-8940-7b8d1e0a1c6e" + webTestClient.delete().uri("/projects/{id}", randomUUID) + .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", diff --git a/security-c4po-api/src/test/resources/collections/projects.json b/security-c4po-api/src/test/resources/collections/projects.json index 309f382..591d287 100644 --- a/security-c4po-api/src/test/resources/collections/projects.json +++ b/security-c4po-api/src/test/resources/collections/projects.json @@ -1,26 +1,55 @@ [{ - "_id": "1234567890123456789012345", - "_class": "com.securityc4po.api.project.ProjectEntity", + "_id": { + "$oid": "62c4018f18f1f463ed1e11be" + }, + "lastModified": { + "$date": { + "$numberLong": "1657012623629" + } + }, "data": { - "_id": "92e7a4e3-2968-4831-a2bd-94c3b33f85fd", + "_id": "41051d0a-63ef-4290-b984-e6fbd736f218", "client": "E Corp", "title": "Some Mock API (v1.0) Scanning", - "createdAt": "2021-01-10T18:05:00Z", + "createdAt": "2022-07-05T09:17:03.629331Z", "tester": "Novatester", - "createdBy": "f8aab31f-4925-4242-a6fa-f98135b4b031" + "createdBy": "ca447a34-cac3-495e-9295-0a5bf5de502a" }, - "lastModified": "2021-01-01T12:00:00.000Z" -}, - { - "_id": "1234567890123456789023456", - "_class": "com.securityc4po.api.project.ProjectEntity", - "data": { - "_id": "9bcde7fe-df3d-4c6b-b392-36f4087b0446", - "client": "Allsafe", - "title": "CashMyData (iOS)", - "createdAt": "2021-01-10T18:05:00Z", - "tester": "Elliot", - "createdBy": "f8aab31f-4925-4242-a6fa-f98135b4b031" - }, - "lastModified": "2021-01-01T12:00:00.000Z" - }] \ No newline at end of file + "_class": "com.securityc4po.api.project.ProjectEntity" +},{ + "_id": { + "$oid": "62c401b718f1f463ed1e11bf" + }, + "lastModified": { + "$date": { + "$numberLong": "1657012663386" + } + }, + "data": { + "_id": "0fa17ddb-7094-4f9c-b295-3433244c32c2", + "client": "Allsafe", + "title": "CashMyData (iOS)", + "createdAt": "2022-07-05T09:17:43.386782Z", + "tester": "Elliot", + "createdBy": "1ac9753a-5a62-4bf7-8412-6fde779ea33a" + }, + "_class": "com.securityc4po.api.project.ProjectEntity" +},{ + "_id": { + "$oid": "62c401e318f1f463ed1e11c0" + }, + "lastModified": { + "$date": { + "$numberLong": "1657012707557" + } + }, + "data": { + "_id": "85aa8d79-5899-4b68-894c-d07f3b168cd4", + "client": "Novatec", + "title": "Openspace log4J", + "createdAt": "2022-07-05T09:18:27.557740Z", + "tester": "mhg", + "createdBy": "3201f6f8-10a4-4826-9b49-b6bdef28b152" + }, + "_class": "com.securityc4po.api.project.ProjectEntity" +}] \ No newline at end of file diff --git a/wiki/SecurityC4PO_Architecture.png b/wiki/C4PO-Architecture.png similarity index 100% rename from wiki/SecurityC4PO_Architecture.png rename to wiki/C4PO-Architecture.png diff --git a/wiki/SecurityC4PO_Data_Structure.png b/wiki/C4PO-Datastructure.png similarity index 100% rename from wiki/SecurityC4PO_Data_Structure.png rename to wiki/C4PO-Datastructure.png diff --git a/wiki/C4PO-Roadmap.png b/wiki/C4PO-Roadmap.png new file mode 100644 index 0000000..75332bb Binary files /dev/null and b/wiki/C4PO-Roadmap.png differ