diff --git a/code/tic-tac-tow-service/.editorconfig b/code/tic-tac-tow-service/.editorconfig index 268ff78..24ad816 100644 --- a/code/tic-tac-tow-service/.editorconfig +++ b/code/tic-tac-tow-service/.editorconfig @@ -568,8 +568,8 @@ ij_kotlin_method_call_chain_wrap = normal ij_kotlin_method_parameters_new_line_after_left_paren = true ij_kotlin_method_parameters_right_paren_on_new_line = true ij_kotlin_method_parameters_wrap = on_every_item -ij_kotlin_name_count_to_use_star_import = 5 -ij_kotlin_name_count_to_use_star_import_for_members = 3 +ij_kotlin_name_count_to_use_star_import = 50 +ij_kotlin_name_count_to_use_star_import_for_members = 30 ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.** ij_kotlin_parameter_annotation_wrap = off ij_kotlin_space_after_comma = true diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/TicTacTowApplication.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/TicTacTowApplication.kt index 4d04c0c..0f6ac31 100644 --- a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/TicTacTowApplication.kt +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/TicTacTowApplication.kt @@ -1,10 +1,51 @@ package pt.isel.daw.tictactow +import org.jdbi.v3.core.Jdbi +import org.postgresql.ds.PGSimpleDataSource import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.servlet.config.annotation.InterceptorRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import pt.isel.daw.tictactow.http.pipeline.AuthenticationInterceptor +import pt.isel.daw.tictactow.http.pipeline.UserArgumentResolver +import pt.isel.daw.tictactow.repository.jdbi.configure +import pt.isel.daw.tictactow.utils.Sha256TokenEncoder @SpringBootApplication -class TicTacTowApplication +class TicTacTowApplication { + @Bean + fun jdbi() = Jdbi.create( + PGSimpleDataSource().apply { + setURL("jdbc:postgresql://localhost:5432/db?user=dbuser&password=changeit") + } + ).configure() + + @Bean + fun passwordEncoder() = BCryptPasswordEncoder() + + @Bean + fun tokenEncoder() = Sha256TokenEncoder() +} + +// QUESTION: why cannot this be in TicTacTowApplication +@Configuration +class PipelineConfigurer( + val authenticationInterceptor: AuthenticationInterceptor, + val userArgumentResolver: UserArgumentResolver, +) : WebMvcConfigurer { + + override fun addInterceptors(registry: InterceptorRegistry) { + registry.addInterceptor(authenticationInterceptor) + } + + override fun addArgumentResolvers(resolvers: MutableList) { + resolvers.add(userArgumentResolver) + } +} fun main(args: Array) { runApplication(*args) diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/domain/UserLogic.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/domain/UserLogic.kt index 58f8c6f..9a126af 100644 --- a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/domain/UserLogic.kt +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/domain/UserLogic.kt @@ -1,8 +1,10 @@ package pt.isel.daw.tictactow.domain +import org.springframework.stereotype.Component import java.security.SecureRandom import java.util.* +@Component class UserLogic { fun generateToken(): String = diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/Uris.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/Uris.kt index 45096fb..6037e2a 100644 --- a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/Uris.kt +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/Uris.kt @@ -10,7 +10,13 @@ object Uris { const val USER_HOME = "/me" const val GAME_BY_ID = "/games/{gid}" + const val USERS_CREATE = "/users" + const val USERS_TOKEN = "/users/token" + const val USERS_GET_BY_ID = "/users/{id}" + fun home(): URI = URI(HOME) fun userHome(): URI = URI(USER_HOME) fun gameById(game: Game) = UriTemplate(GAME_BY_ID).expand(game.id) + + fun userById(id: String) = UriTemplate(USERS_GET_BY_ID).expand(id) } \ No newline at end of file diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/UsersController.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/UsersController.kt new file mode 100644 index 0000000..f662107 --- /dev/null +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/UsersController.kt @@ -0,0 +1,65 @@ +package pt.isel.daw.tictactow.http + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController +import pt.isel.daw.tictactow.Either +import pt.isel.daw.tictactow.domain.User +import pt.isel.daw.tictactow.http.model.Problem +import pt.isel.daw.tictactow.http.model.UserCreateInputModel +import pt.isel.daw.tictactow.http.model.UserCreateTokenInputModel +import pt.isel.daw.tictactow.http.model.UserHomeOutputModel +import pt.isel.daw.tictactow.http.model.UserTokenCreateOutputModel +import pt.isel.daw.tictactow.service.TokenCreationError +import pt.isel.daw.tictactow.service.UserCreationError +import pt.isel.daw.tictactow.service.UsersService + +@RestController +class UsersController( + private val userService: UsersService +) { + + @PostMapping(Uris.USERS_CREATE) + fun create(@RequestBody input: UserCreateInputModel): ResponseEntity<*> { + val res = userService.createUser(input.username, input.password) + return when (res) { + is Either.Right -> ResponseEntity.status(201) + .header( + "Location", + Uris.userById(res.value).toASCIIString() + ).build() + is Either.Left -> when (res.value) { + UserCreationError.InsecurePassword -> Problem.response(400, Problem.insecurePassword) + UserCreationError.UserAlreadyExists -> Problem.response(400, Problem.userAlreadyExists) + } + } + } + + @PostMapping(Uris.USERS_TOKEN) + fun token(@RequestBody input: UserCreateTokenInputModel): ResponseEntity<*> { + val res = userService.createToken(input.username, input.password) + return when (res) { + is Either.Right -> ResponseEntity.status(200) + .body(UserTokenCreateOutputModel(res.value)) + is Either.Left -> when (res.value) { + TokenCreationError.UserOrPasswordAreInvalid -> Problem.response(400, Problem.userOrPasswordAreInvalid) + } + } + } + + @GetMapping(Uris.USERS_GET_BY_ID) + fun getById(@PathVariable id: String) { + TODO("TODO") + } + + @GetMapping(Uris.USER_HOME) + fun getUserHome(user: User): UserHomeOutputModel { + return UserHomeOutputModel( + id = user.id.toString(), + username = user.username, + ) + } +} \ 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 new file mode 100644 index 0000000..d0f41aa --- /dev/null +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/model/Problem.kt @@ -0,0 +1,38 @@ +package pt.isel.daw.tictactow.http.model + +import org.springframework.http.ResponseEntity +import java.net.URI + +class Problem( + typeUri: URI +) { + val type = typeUri.toASCIIString() + + companion object { + const val MEDIA_TYPE = "application/problem+json" + fun response(status: Int, problem: Problem) = ResponseEntity + .status(status) + .header("Content-Type", MEDIA_TYPE) + .body(problem) + + val userAlreadyExists = Problem( + URI( + "https://github.com/isel-leic-daw/s2223i-51d-51n-public/tree/main/code/tic-tac-tow-service/" + + "docs/problems/user-already-exists" + ) + ) + val insecurePassword = Problem( + URI( + "https://github.com/isel-leic-daw/s2223i-51d-51n-public/tree/main/code/tic-tac-tow-service/" + + "docs/problems/insecure-password" + ) + ) + + val userOrPasswordAreInvalid = Problem( + URI( + "https://github.com/isel-leic-daw/s2223i-51d-51n-public/tree/main/code/tic-tac-tow-service/" + + "docs/problems/user-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/UserInputModels.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/model/UserInputModels.kt new file mode 100644 index 0000000..09cbe5f --- /dev/null +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/model/UserInputModels.kt @@ -0,0 +1,11 @@ +package pt.isel.daw.tictactow.http.model + +data class UserCreateInputModel( + val username: String, + val password: String, +) + +data class UserCreateTokenInputModel( + val username: String, + val password: String, +) \ No newline at end of file diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/model/UserOutputModels.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/model/UserOutputModels.kt new file mode 100644 index 0000000..a33c54b --- /dev/null +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/model/UserOutputModels.kt @@ -0,0 +1,10 @@ +package pt.isel.daw.tictactow.http.model + +class UserTokenCreateOutputModel( + val token: String +) + +class UserHomeOutputModel( + val id: String, + val username: String, +) \ No newline at end of file diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/pipeline/AuthenticationInterceptor.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/pipeline/AuthenticationInterceptor.kt new file mode 100644 index 0000000..0f0401f --- /dev/null +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/pipeline/AuthenticationInterceptor.kt @@ -0,0 +1,39 @@ +package pt.isel.daw.tictactow.http.pipeline + +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import org.springframework.web.method.HandlerMethod +import org.springframework.web.servlet.HandlerInterceptor +import pt.isel.daw.tictactow.domain.User +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +@Component +class AuthenticationInterceptor( + private val authorizationHeaderProcessor: AuthorizationHeaderProcessor +) : HandlerInterceptor { + + override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { + if (handler is HandlerMethod && handler.methodParameters.any { it.parameterType == User::class.java } + ) { + // enforce authentication + val user = authorizationHeaderProcessor.process(request.getHeader(NAME_AUTHORIZATION_HEADER)) + if (user == null) { + response.status = 401 + response.addHeader(NAME_WWW_AUTHENTICATE_HEADER, AuthorizationHeaderProcessor.SCHEME) + return false + } else { + UserArgumentResolver.addUserTo(user, request) + return true + } + } + + return true + } + + companion object { + private val logger = LoggerFactory.getLogger(AuthenticationInterceptor::class.java) + private const val NAME_AUTHORIZATION_HEADER = "Authorization" + private const val NAME_WWW_AUTHENTICATE_HEADER = "WWW-Authenticate" + } +} \ No newline at end of file diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/pipeline/AuthorizationHeaderProcessor.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/pipeline/AuthorizationHeaderProcessor.kt new file mode 100644 index 0000000..d770167 --- /dev/null +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/pipeline/AuthorizationHeaderProcessor.kt @@ -0,0 +1,31 @@ +package pt.isel.daw.tictactow.http.pipeline + +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import pt.isel.daw.tictactow.domain.User +import pt.isel.daw.tictactow.service.UsersService + +@Component +class AuthorizationHeaderProcessor( + val usersService: UsersService +) { + + fun process(authorizationValue: String?): User? { + if (authorizationValue == null) { + return null + } + val parts = authorizationValue.trim().split(" ") + if (parts.size != 2) { + return null + } + if (parts[0].lowercase() != SCHEME) { + return null + } + return usersService.getUserByToken(parts[1]) + } + + companion object { + private val logger = LoggerFactory.getLogger(AuthorizationHeaderProcessor::class.java) + const val SCHEME = "bearer" + } +} \ No newline at end of file diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/pipeline/UserArgumentResolver.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/pipeline/UserArgumentResolver.kt new file mode 100644 index 0000000..48fb14b --- /dev/null +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/pipeline/UserArgumentResolver.kt @@ -0,0 +1,41 @@ +package pt.isel.daw.tictactow.http.pipeline + +import org.springframework.core.MethodParameter +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer +import pt.isel.daw.tictactow.domain.User +import javax.servlet.http.HttpServletRequest + +@Component +class UserArgumentResolver : HandlerMethodArgumentResolver { + + override fun supportsParameter(parameter: MethodParameter) = parameter.parameterType == User::class.java + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory? + ): Any? { + val request = webRequest.getNativeRequest(HttpServletRequest::class.java) + ?: throw IllegalStateException("TODO") + return getUserFrom(request) ?: throw IllegalStateException("TODO") + } + + companion object { + private const val KEY = "UserArgumentResolver" + + fun addUserTo(user: User, request: HttpServletRequest) { + return request.setAttribute(KEY, user) + } + + fun getUserFrom(request: HttpServletRequest): User? { + return request.getAttribute(KEY)?.let { + it as? User + } + } + } +} \ No newline at end of file diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/UsersRepository.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/UsersRepository.kt index 2a439d8..643c591 100644 --- a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/UsersRepository.kt +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/UsersRepository.kt @@ -9,7 +9,7 @@ interface UsersRepository { fun storeUser( username: String, passwordValidation: PasswordValidationInfo, - ): Boolean + ): String fun getUserByUsername(username: String): User? diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/jdbi/JdbiTransactionManager.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/jdbi/JdbiTransactionManager.kt index 5bd5213..9b22ec0 100644 --- a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/jdbi/JdbiTransactionManager.kt +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/jdbi/JdbiTransactionManager.kt @@ -1,9 +1,11 @@ package pt.isel.daw.tictactow.repository.jdbi import org.jdbi.v3.core.Jdbi +import org.springframework.stereotype.Component import pt.isel.daw.tictactow.repository.Transaction import pt.isel.daw.tictactow.repository.TransactionManager +@Component class JdbiTransactionManager( private val jdbi: Jdbi ) : TransactionManager { diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/jdbi/JdbiUsersRepository.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/jdbi/JdbiUsersRepository.kt index 1501c45..baaf4c4 100644 --- a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/jdbi/JdbiUsersRepository.kt +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/jdbi/JdbiUsersRepository.kt @@ -17,7 +17,7 @@ class JdbiUsersRepository( .mapTo() .singleOrNull() - override fun storeUser(username: String, passwordValidation: PasswordValidationInfo): Boolean = + override fun storeUser(username: String, passwordValidation: PasswordValidationInfo): String = handle.createUpdate( """ insert into dbo.Users (username, password_validation) values (:username, :password_validation) @@ -25,7 +25,10 @@ class JdbiUsersRepository( ) .bind("username", username) .bind("password_validation", passwordValidation.validationInfo) - .execute() == 1 + .executeAndReturnGeneratedKeys() + .mapTo() + .one() + .toString() override fun isUserStoredByUsername(username: String): Boolean = handle.createQuery("select count(*) from dbo.Users where username = :username") diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/service/UsersService.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/service/UsersService.kt index aff46ad..4155b03 100644 --- a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/service/UsersService.kt +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/service/UsersService.kt @@ -1,6 +1,7 @@ package pt.isel.daw.tictactow.service import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Component import pt.isel.daw.tictactow.Either import pt.isel.daw.tictactow.domain.PasswordValidationInfo import pt.isel.daw.tictactow.domain.User @@ -12,13 +13,14 @@ sealed class UserCreationError { object UserAlreadyExists : UserCreationError() object InsecurePassword : UserCreationError() } -typealias UserCreationResult = Either +typealias UserCreationResult = Either sealed class TokenCreationError { object UserOrPasswordAreInvalid : TokenCreationError() } typealias TokenCreationResult = Either +@Component class UsersService( private val transactionManager: TransactionManager, private val userLogic: UserLogic, @@ -40,8 +42,8 @@ class UsersService( if (usersRepository.isUserStoredByUsername(username)) { Either.Left(UserCreationError.UserAlreadyExists) } else { - usersRepository.storeUser(username, passwordValidationInfo) - Either.Right(Unit) + val id = usersRepository.storeUser(username, passwordValidationInfo) + Either.Right(id) } } } 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 new file mode 100644 index 0000000..09140ba --- /dev/null +++ b/code/tic-tac-tow-service/src/test/kotlin/pt/isel/daw/tictactow/http/UserTests.kt @@ -0,0 +1,173 @@ +package pt.isel.daw.tictactow.http + +import org.junit.jupiter.api.Test +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 kotlin.test.assertTrue + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class UserTests { + + // One of the very few places where we use property injection + @LocalServerPort + var port: Int = 0 + + @Test + fun `can create an user`() { + // given: an HTTP client + val client = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build() + + // and: a random user + val username = UUID.randomUUID().toString() + val password = "changeit" + + // when: creating an user + // then: the response is a 201 with a proper Location header + client.post().uri("/users") + .bodyValue( + mapOf( + "username" to username, + "password" to password + ) + ) + .exchange() + .expectStatus().isCreated + .expectHeader().value("location") { + assertTrue(it.startsWith("/users/")) + } + } + + @Test + fun `can create an user, obtain a token, and access user home`() { + // given: an HTTP client + val client = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build() + + // and: a random user + val username = UUID.randomUUID().toString() + val password = "changeit" + + // when: creating an user + // then: the response is a 201 with a proper Location header + client.post().uri("/users") + .bodyValue( + mapOf( + "username" to username, + "password" to password + ) + ) + .exchange() + .expectStatus().isCreated + .expectHeader().value("location") { + assertTrue(it.startsWith("/users/")) + } + + // when: creating a token + // then: the response is a 200 + val result = client.post().uri("/users/token") + .bodyValue( + mapOf( + "username" to username, + "password" to password + ) + ) + .exchange() + .expectStatus().isOk + .expectBody(TokenResponse::class.java) + .returnResult() + .responseBody!! + + // when: getting the user home with a valid token + // then: the response is a 200 with the proper representation + client.get().uri("/me") + .header("Authorization", "Bearer ${result.token}") + .exchange() + .expectStatus().isOk + .expectBody() + .jsonPath("username").isEqualTo(username) + + // when: getting the user home with an invalid token + // then: the response is a 4001 with the proper problem + client.get().uri("/me") + .header("Authorization", "Bearer ${result.token}-invalid") + .exchange() + .expectStatus().isUnauthorized + .expectHeader().valueEquals("WWW-Authenticate", "bearer") + } + + class TokenResponse( + val token: String + ) + + @Test + fun `user creation produces an error if user already exists`() { + // given: an HTTP client + val client = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build() + + // and: a random user + val username = UUID.randomUUID().toString() + val password = "changeit" + + // when: creating an user + // then: the response is a 201 with a proper Location header + client.post().uri("/users") + .bodyValue( + mapOf( + "username" to username, + "password" to password + ) + ) + .exchange() + .expectStatus().isCreated + .expectHeader().value("location") { + assertTrue(it.startsWith("/users/")) + } + + // when: creating the same user again + // then: the response is a 400 with the proper tyoe + client.post().uri("/users") + .bodyValue( + mapOf( + "username" to username, + "password" to password + ) + ) + .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/user-already-exists" + ) + } + + @Test + fun `user creation produces an error if password is weak`() { + // given: an HTTP client + val client = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build() + + // and: a random user + val username = UUID.randomUUID().toString() + val password = "-" + + // 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().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/insecure-password" + ) + } +} \ No newline at end of file diff --git a/code/tic-tac-tow-service/src/test/kotlin/pt/isel/daw/tictactow/service/UserServiceTests.kt b/code/tic-tac-tow-service/src/test/kotlin/pt/isel/daw/tictactow/service/UserServiceTests.kt index 74cf047..0d8d820 100644 --- a/code/tic-tac-tow-service/src/test/kotlin/pt/isel/daw/tictactow/service/UserServiceTests.kt +++ b/code/tic-tac-tow-service/src/test/kotlin/pt/isel/daw/tictactow/service/UserServiceTests.kt @@ -10,6 +10,8 @@ import pt.isel.daw.tictactow.utils.testWithTransactionManagerAndRollback import java.util.* import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail class UserServiceTests { @@ -29,7 +31,10 @@ class UserServiceTests { val createUserResult = userService.createUser("bob", "changeit") // then: the creation is successful - assertEquals(Either.Right(Unit), createUserResult) + when (createUserResult) { + is Either.Left -> fail("Unexpected $createUserResult") + is Either.Right -> assertTrue(createUserResult.value.length > 0) + } // when: creating a token val createTokenResult = userService.createToken("bob", "changeit")