feat: As a developer I want to have a global and customizable exception handeling
This commit is contained in:
parent
f9ce7606f7
commit
e80c0e8560
|
@ -23,10 +23,10 @@
|
|||
* Postman
|
||||
|
||||
## Application Architecture
|
||||

|
||||

|
||||
|
||||
## Data Structure
|
||||

|
||||

|
||||
|
||||
### Conventions
|
||||
* Branch: `<initial>_c4po_<issuenumber>`
|
||||
|
@ -38,3 +38,6 @@ Execute 'c4po.sh' and all services will run on a dev server.
|
|||
### Testuser Credentials:
|
||||
* Username: ttt
|
||||
* Password: Test1234!
|
||||
|
||||
## C4PO Roadmap
|
||||

|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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": []
|
||||
},
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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 {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
|
@ -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<String, Any> {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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<ServerResponse> {
|
||||
return RouterFunctions.route(RequestPredicates.all(), HandlerFunction { this.renderErrorResponse(it) })
|
||||
}
|
||||
|
||||
private fun renderErrorResponse(request: ServerRequest): Mono<ServerResponse> {
|
||||
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
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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<ResponseEntity<ResponseBody>> {
|
||||
return this.projectService.deleteProject(id).map{
|
||||
ResponseEntity.ok().body(it.toProjectDeleteResponseBody())
|
||||
}.switchIfEmpty {
|
||||
Mono.just(ResponseEntity.noContent().build<ResponseBody>())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<List<Project>> {
|
||||
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<Project> {
|
||||
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<Project> {
|
||||
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<Project> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
throw wrappedException(
|
||||
logging = { logger.warn("Project could not be updated in Database. Thrown exception: ", it) },
|
||||
mappedException = TransactionInterruptedException(
|
||||
"Project could not be updated.",
|
||||
Errorcode.ProjectInsertionFailed
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
"_class": "com.securityc4po.api.project.ProjectEntity"
|
||||
},{
|
||||
"_id": {
|
||||
"$oid": "62c401b718f1f463ed1e11bf"
|
||||
},
|
||||
"lastModified": {
|
||||
"$date": {
|
||||
"$numberLong": "1657012663386"
|
||||
}
|
||||
},
|
||||
{
|
||||
"_id": "1234567890123456789023456",
|
||||
"_class": "com.securityc4po.api.project.ProjectEntity",
|
||||
"data": {
|
||||
"_id": "9bcde7fe-df3d-4c6b-b392-36f4087b0446",
|
||||
"_id": "0fa17ddb-7094-4f9c-b295-3433244c32c2",
|
||||
"client": "Allsafe",
|
||||
"title": "CashMyData (iOS)",
|
||||
"createdAt": "2021-01-10T18:05:00Z",
|
||||
"createdAt": "2022-07-05T09:17:43.386782Z",
|
||||
"tester": "Elliot",
|
||||
"createdBy": "f8aab31f-4925-4242-a6fa-f98135b4b031"
|
||||
"createdBy": "1ac9753a-5a62-4bf7-8412-6fde779ea33a"
|
||||
},
|
||||
"lastModified": "2021-01-01T12:00:00.000Z"
|
||||
"_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"
|
||||
}]
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 465 KiB After Width: | Height: | Size: 465 KiB |
Binary file not shown.
After Width: | Height: | Size: 276 KiB |
Loading…
Reference in New Issue