diff --git a/code/tic-tac-tow-service/docs/problems/insecure-password b/code/tic-tac-tow-service/docs/problems/insecure-password new file mode 100644 index 0000000..2902445 --- /dev/null +++ b/code/tic-tac-tow-service/docs/problems/insecure-password @@ -0,0 +1 @@ +The provided password doesn't comply with the minimum security requirements... \ No newline at end of file diff --git a/code/tic-tac-tow-service/docs/problems/invalid-request-content b/code/tic-tac-tow-service/docs/problems/invalid-request-content new file mode 100644 index 0000000..09ee34d --- /dev/null +++ b/code/tic-tac-tow-service/docs/problems/invalid-request-content @@ -0,0 +1 @@ +The request content is not valid. \ No newline at end of file diff --git a/code/tic-tac-tow-service/docs/problems/user-already-exists b/code/tic-tac-tow-service/docs/problems/user-already-exists new file mode 100644 index 0000000..844eeb9 --- /dev/null +++ b/code/tic-tac-tow-service/docs/problems/user-already-exists @@ -0,0 +1 @@ +The user being created already exists. \ No newline at end of file diff --git a/code/tic-tac-tow-service/docs/problems/user-or-password-are-invalid b/code/tic-tac-tow-service/docs/problems/user-or-password-are-invalid new file mode 100644 index 0000000..b702a75 --- /dev/null +++ b/code/tic-tac-tow-service/docs/problems/user-or-password-are-invalid @@ -0,0 +1 @@ +The provided username or password are invalid. \ No newline at end of file diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/model/Problem.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/model/Problem.kt index d0f41aa..c745b9c 100644 --- a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/model/Problem.kt +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/model/Problem.kt @@ -13,7 +13,7 @@ class Problem( fun response(status: Int, problem: Problem) = ResponseEntity .status(status) .header("Content-Type", MEDIA_TYPE) - .body(problem) + .body(problem) val userAlreadyExists = Problem( URI( @@ -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" + ) + ) } } \ No newline at end of file diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/pipeline/CustomExceptionHandler.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/pipeline/CustomExceptionHandler.kt new file mode 100644 index 0000000..7be4ce2 --- /dev/null +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/pipeline/CustomExceptionHandler.kt @@ -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 { + log.info("Handling BindException: {}", ex.message) + return Problem.response(400, Problem.invalidRequestContent) + } + + override fun handleHttpMessageNotReadable( + ex: HttpMessageNotReadableException, + headers: HttpHeaders, + status: HttpStatus, + request: WebRequest + ): ResponseEntity { + log.info("Handling HttpMessageNotReadableException: {}", ex.message) + return Problem.response(400, Problem.invalidRequestContent) + } + + @ExceptionHandler( + Exception::class, + ) + fun handleAll(): ResponseEntity { + return ResponseEntity.status(500).build() + } + + companion object { + private val log = LoggerFactory.getLogger(CustomExceptionHandler::class.java) + } +} \ No newline at end of file diff --git a/code/tic-tac-tow-service/src/test/kotlin/pt/isel/daw/tictactow/http/InternalErrorTests.kt b/code/tic-tac-tow-service/src/test/kotlin/pt/isel/daw/tictactow/http/InternalErrorTests.kt new file mode 100644 index 0000000..bb6df79 --- /dev/null +++ b/code/tic-tac-tow-service/src/test/kotlin/pt/isel/daw/tictactow/http/InternalErrorTests.kt @@ -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") + } +} \ No newline at end of file diff --git a/code/tic-tac-tow-service/src/test/kotlin/pt/isel/daw/tictactow/http/UserTests.kt b/code/tic-tac-tow-service/src/test/kotlin/pt/isel/daw/tictactow/http/UserTests.kt index 09140ba..6d00842 100644 --- a/code/tic-tac-tow-service/src/test/kotlin/pt/isel/daw/tictactow/http/UserTests.kt +++ b/code/tic-tac-tow-service/src/test/kotlin/pt/isel/daw/tictactow/http/UserTests.kt @@ -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) @@ -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 = + Stream.of( + mapOf( + // no username or password + ), + mapOf( + "username" to "alice" + // no password + ), + mapOf( + "password" to "bad", + // no username + ), + mapOf( + "username" to listOf(), + "password" to "changeit", + // invalid username type + ), + ) + } } \ No newline at end of file