Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-2: adds exception handling example #12

Merged
merged 1 commit into from
Dec 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions code/tic-tac-tow-service/docs/problems/insecure-password
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The provided password doesn't comply with the minimum security requirements...
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The request content is not valid.
1 change: 1 addition & 0 deletions code/tic-tac-tow-service/docs/problems/user-already-exists
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The user being created already exists.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The provided username or password are invalid.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Problem(
fun response(status: Int, problem: Problem) = ResponseEntity
.status(status)
.header("Content-Type", MEDIA_TYPE)
.body(problem)
.body<Any>(problem)

val userAlreadyExists = Problem(
URI(
Expand All @@ -34,5 +34,12 @@ class Problem(
"docs/problems/user-or-password-are-invalid"
)
)

val invalidRequestContent = Problem(
URI(
"https://github.com/isel-leic-daw/s2223i-51d-51n-public/tree/main/code/tic-tac-tow-service/" +
"docs/problems/invalid-request-content"
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package pt.isel.daw.tictactow.http.pipeline

import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.validation.BindException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
import pt.isel.daw.tictactow.http.model.Problem

@ControllerAdvice
class CustomExceptionHandler : ResponseEntityExceptionHandler() {

override fun handleBindException(
ex: BindException,
headers: HttpHeaders,
status: HttpStatus,
request: WebRequest
): ResponseEntity<Any> {
log.info("Handling BindException: {}", ex.message)
return Problem.response(400, Problem.invalidRequestContent)
}

override fun handleHttpMessageNotReadable(
ex: HttpMessageNotReadableException,
headers: HttpHeaders,
status: HttpStatus,
request: WebRequest
): ResponseEntity<Any> {
log.info("Handling HttpMessageNotReadableException: {}", ex.message)
return Problem.response(400, Problem.invalidRequestContent)
}

@ExceptionHandler(
Exception::class,
)
fun handleAll(): ResponseEntity<Unit> {
return ResponseEntity.status(500).build()
}

companion object {
private val log = LoggerFactory.getLogger(CustomExceptionHandler::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package pt.isel.daw.tictactow.http

import org.hamcrest.CoreMatchers.equalTo
import org.jdbi.v3.core.Jdbi
import org.junit.jupiter.api.Test
import org.postgresql.ds.PGSimpleDataSource
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Primary
import org.springframework.test.web.reactive.server.WebTestClient
import pt.isel.daw.tictactow.repository.jdbi.configure
import java.util.*

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class InternalErrorTests {

@TestConfiguration
class TestConfig {
@Bean
@Primary
fun testJdbi() = Jdbi.create(
PGSimpleDataSource().apply {
setURL("jdbc:postgresql://bad-host:5432/db?user=dbuser&password=changeit")
}
).configure()
}

@LocalServerPort
var port: Int = 0

@Test
fun `Unknown exceptions are mapped into a 500 without a response content`() {
// given: an HTTP client
val client = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()

// and: a random user
val username = UUID.randomUUID().toString()
val password = "is-this-strong?"

// when: creating a user
// then: the response is a 400 with the proper type
client.post().uri("/users")
.bodyValue(
mapOf(
"username" to username,
"password" to password
)
)
.exchange()
.expectStatus().value(equalTo(500))
.expectHeader().doesNotExist("Content-Type")
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package pt.isel.daw.tictactow.http

import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.web.reactive.server.WebTestClient
import java.util.*
import java.util.stream.Stream
import kotlin.test.assertTrue

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
Expand Down Expand Up @@ -170,4 +173,49 @@ class UserTests {
"tic-tac-tow-service/docs/problems/insecure-password"
)
}

@ParameterizedTest
@MethodSource
fun `user creation produces an error request content is not valid`(input: Any) {
// given: an HTTP client
val client = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()

// when: creating a user with invalid data
// then: the response is a 400 with the proper type
client.post().uri("/users")
.bodyValue(
input
)
.exchange()
.expectStatus().isBadRequest
.expectHeader().contentType("application/problem+json")
.expectBody()
.jsonPath("type").isEqualTo(
"https://github.com/isel-leic-daw/s2223i-51d-51n-public/tree/main/code/" +
"tic-tac-tow-service/docs/problems/invalid-request-content"
)
}

companion object {
@JvmStatic
fun `user creation produces an error request content is not valid`(): Stream<Any> =
Stream.of(
mapOf<String, Any>(
// no username or password
),
mapOf(
"username" to "alice"
// no password
),
mapOf(
"password" to "bad",
// no username
),
mapOf(
"username" to listOf<String>(),
"password" to "changeit",
// invalid username type
),
)
}
}