From 6a95b5926a0deb92ae12eacabc222e97a7cdecc1 Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Sun, 8 Oct 2023 21:23:35 +0200 Subject: [PATCH 01/19] Expand the Users.sq to return a object that matches the Profile contract --- .../sqldelight/io/github/nomisrev/sqldelight/Users.sq | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/sqldelight/io/github/nomisrev/sqldelight/Users.sq b/src/main/sqldelight/io/github/nomisrev/sqldelight/Users.sq index a1234646..cff671a4 100644 --- a/src/main/sqldelight/io/github/nomisrev/sqldelight/Users.sq +++ b/src/main/sqldelight/io/github/nomisrev/sqldelight/Users.sq @@ -20,6 +20,16 @@ SELECT email, username, bio, image FROM users WHERE username = :username; +selectProfile: +SELECT users.username, users.bio, users.image, +CASE + WHEN following.follower_id IS NULL THEN 0 + ELSE 1 +END AS following +FROM users +LEFT JOIN following ON CAST(users.id AS BIGINT) = following.followed_id +WHERE users.username = :username; + selectSecurityByEmail: SELECT id, username, salt, hashed_password, bio, image FROM users From 437682c19aad8e6d63a99ad7898604277b9e1173 Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Sun, 8 Oct 2023 21:24:18 +0200 Subject: [PATCH 02/19] Connect the UserPersistence with the changes from the User.sq --- .../io/github/nomisrev/repo/UserPersistence.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/io/github/nomisrev/repo/UserPersistence.kt b/src/main/kotlin/io/github/nomisrev/repo/UserPersistence.kt index 49e21f19..ce4c19e7 100644 --- a/src/main/kotlin/io/github/nomisrev/repo/UserPersistence.kt +++ b/src/main/kotlin/io/github/nomisrev/repo/UserPersistence.kt @@ -8,6 +8,7 @@ import io.github.nomisrev.DomainError import io.github.nomisrev.PasswordNotMatched import io.github.nomisrev.UserNotFound import io.github.nomisrev.UsernameAlreadyExists +import io.github.nomisrev.routes.Profile import io.github.nomisrev.service.UserInfo import io.github.nomisrev.sqldelight.FollowingQueries import io.github.nomisrev.sqldelight.UsersQueries @@ -32,9 +33,11 @@ interface UserPersistence { /** Select a User by its [UserId] */ suspend fun select(userId: UserId): Either - /** Select a User by its username */ + /** Select a User by its [UserName]*/ suspend fun select(username: String): Either + suspend fun selectProfile(username: String): Either + @Suppress("LongParameterList") suspend fun update( userId: UserId, @@ -104,6 +107,18 @@ fun userPersistence( ensureNotNull(userInfo) { UserNotFound("username=$username") } } + override suspend fun selectProfile(username: String): Either = either { + val profileInfo = usersQueries.selectProfile(username, ::toProfile).executeAsOneOrNull() + ensureNotNull(profileInfo) { UserNotFound("username=$username") } + } + + fun toProfile( + username: String, + bio: String, + image: String, + following: Long + ): Profile = Profile(username, bio, image, following > 0) + override suspend fun update( userId: UserId, email: String?, From 0d90feabc157262247ee2c52aceec4f5088de63c Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Sun, 8 Oct 2023 21:27:36 +0200 Subject: [PATCH 03/19] Add ProfileService --- .../github/nomisrev/service/ProfileService.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/kotlin/io/github/nomisrev/service/ProfileService.kt diff --git a/src/main/kotlin/io/github/nomisrev/service/ProfileService.kt b/src/main/kotlin/io/github/nomisrev/service/ProfileService.kt new file mode 100644 index 00000000..771a4802 --- /dev/null +++ b/src/main/kotlin/io/github/nomisrev/service/ProfileService.kt @@ -0,0 +1,21 @@ +package io.github.nomisrev.service + +import arrow.core.Either +import io.github.nomisrev.DomainError +import io.github.nomisrev.repo.UserPersistence +import io.github.nomisrev.routes.Profile + + +interface ProfileService { + /** Select a Profile by its username */ + suspend fun getProfile(username: String): Either +} + +fun profileService( + repo: UserPersistence, +): ProfileService = object : ProfileService { + + override suspend fun getProfile( + username: String, + ): Either = repo.selectProfile(username) +} \ No newline at end of file From b38fd78670756761d4627399b4697593c12b79c5 Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Sun, 8 Oct 2023 21:27:57 +0200 Subject: [PATCH 04/19] Add profile routes with the dependency setup --- .../kotlin/io/github/nomisrev/env/Dependencies.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt b/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt index b694d93f..99496275 100644 --- a/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt +++ b/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt @@ -11,16 +11,19 @@ import io.github.nomisrev.repo.tagPersistence import io.github.nomisrev.repo.userPersistence import io.github.nomisrev.service.ArticleService import io.github.nomisrev.service.JwtService +import io.github.nomisrev.service.ProfileService import io.github.nomisrev.service.UserService import io.github.nomisrev.service.articleService import io.github.nomisrev.service.jwtService +import io.github.nomisrev.service.profileService import io.github.nomisrev.service.slugifyGenerator import io.github.nomisrev.service.userService -import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers +import kotlin.time.Duration.Companion.seconds class Dependencies( val userService: UserService, + val profileService: ProfileService, val jwtService: JwtService, val articleService: ArticleService, val healthCheck: HealthCheckRegistry, @@ -38,6 +41,7 @@ suspend fun ResourceScope.dependencies(env: Env): Dependencies { val jwtService = jwtService(env.auth, userRepo) val slugGenerator = slugifyGenerator() val userService = userService(userRepo, jwtService) + val profileService = profileService(userRepo) val checks = HealthCheckRegistry(Dispatchers.Default) { @@ -45,10 +49,10 @@ suspend fun ResourceScope.dependencies(env: Env): Dependencies { } return Dependencies( - userService, - jwtService, - articleService(slugGenerator, articleRepo, userRepo, tagPersistence, favouritePersistence), - checks, + userService = userService, + jwtService = jwtService, + articleService = articleService(slugGenerator, articleRepo, userRepo, tagPersistence, favouritePersistence), + healthCheck = checks, tagPersistence, userRepo ) From 0ead863dc3091c9207bcee2d07f631eb45c66040 Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Tue, 10 Oct 2023 22:37:07 +0200 Subject: [PATCH 05/19] Add test coverage for the profileRoute --- .../nomisrev/routes/ProfileRouteSpec.kt | 115 +++++++++++------- 1 file changed, 74 insertions(+), 41 deletions(-) diff --git a/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt b/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt index adeeda3c..83986ee3 100644 --- a/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt +++ b/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt @@ -1,5 +1,6 @@ package io.github.nomisrev.routes +import io.github.nomisrev.env.Dependencies import io.github.nomisrev.service.RegisterUser import io.github.nomisrev.withServer import io.kotest.assertions.arrow.core.shouldBeRight @@ -11,55 +12,87 @@ import io.ktor.client.plugins.resources.delete import io.ktor.http.HttpStatusCode class ProfileRouteSpec : - StringSpec({ - val validUsername = "username" - val validEmail = "valid@domain.com" - val validPw = "123456789" - val validUsernameFollowed = "username2" - val validEmailFollowed = "valid2@domain.com" + StringSpec({ + val validUsername = "username" + val validEmail = "valid@domain.com" + val validPw = "123456789" + val validUsernameFollowed = "username2" + val validEmailFollowed = "valid2@domain.com" - "Can unfollow profile" { - withServer { dependencies -> - val token = dependencies.userService - .register(RegisterUser(validUsername, validEmail, validPw)) - .shouldBeRight() - dependencies.userService - .register(RegisterUser(validUsernameFollowed, validEmailFollowed, validPw)) - .shouldBeRight() + "Can unfollow profile" { + withServer { dependencies -> + val token = dependencies.userService + .register(RegisterUser(validUsername, validEmail, validPw)) + .shouldBeRight() + dependencies.userService + .register(RegisterUser(validUsernameFollowed, validEmailFollowed, validPw)) + .shouldBeRight() - val response = delete(ProfilesResource.Follow(username = validUsernameFollowed)) { - bearerAuth(token.value) + val response = delete(ProfilesResource.Follow(username = validUsernameFollowed)) { + bearerAuth(token.value) + } + + response.status shouldBe HttpStatusCode.OK + with(response.body>().profile) { + username shouldBe validUsernameFollowed + bio shouldBe "" + image shouldBe "" + following shouldBe false + } + } } - response.status shouldBe HttpStatusCode.OK - with(response.body>().profile) { - username shouldBe validUsernameFollowed - bio shouldBe "" - image shouldBe "" - following shouldBe false + "Needs token to unfollow" { + withServer { + val response = delete(ProfilesResource.Follow(username = validUsernameFollowed)) + + response.status shouldBe HttpStatusCode.Unauthorized + } } - } - } - "Needs token to unfollow" { - withServer { - val response = delete(ProfilesResource.Follow(username = validUsernameFollowed)) + "Username invalid to unfollow" { + withServer { dependencies -> + val token = dependencies.userService + .register(RegisterUser(validUsername, validEmail, validPw)) + .shouldBeRight() - response.status shouldBe HttpStatusCode.Unauthorized - } - } + val response = delete(ProfilesResource.Follow(username = validUsernameFollowed)) { + bearerAuth(token.value) + } + + response.status shouldBe HttpStatusCode.UnprocessableEntity + } + } + + "Get profile with no following" { + withServer { dependencies: Dependencies -> + dependencies.userService + .register(RegisterUser(userName, validEmail, validPw)) + .shouldBeRight() + val response = get("/profiles/$userName") { + contentType(ContentType.Application.Json) + } + + response.status shouldBe HttpStatusCode.OK + with(response.body()) { + username shouldBe userName + bio shouldBe "" + image shouldBe "" + following shouldBe false + } + } + } - "Username invalid to unfollow" { - withServer {dependencies -> - val token = dependencies.userService - .register(RegisterUser(validUsername, validEmail, validPw)) - .shouldBeRight() + "Get profile invalid username" { + withServer { + val response = get("/profiles/$userName") { + contentType(ContentType.Application.Json) + } - val response = delete(ProfilesResource.Follow(username = validUsernameFollowed)) { - bearerAuth(token.value) + response.status shouldBe HttpStatusCode.UnprocessableEntity + response.body().errors.body shouldBe + listOf("User with username=$userName not found") + } } - response.status shouldBe HttpStatusCode.UnprocessableEntity - } - } - }) + }) From 9a5bfdddbbb5299f7f5d3ff5ed1e42675b52c467 Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Tue, 10 Oct 2023 22:48:49 +0200 Subject: [PATCH 06/19] Add profilesRoutes to the new routes extension --- src/main/kotlin/io/github/nomisrev/routes/profile.kt | 10 +++++++++- src/main/kotlin/io/github/nomisrev/routes/root.kt | 1 - 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/io/github/nomisrev/routes/profile.kt b/src/main/kotlin/io/github/nomisrev/routes/profile.kt index 3f7663cc..f9f1a89c 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/profile.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/profile.kt @@ -8,7 +8,15 @@ import io.ktor.http.HttpStatusCode import io.ktor.resources.Resource import io.ktor.server.resources.delete import io.ktor.server.routing.Route - +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.call +import io.ktor.server.routing.Route +import io.ktor.server.routing.get +import io.ktor.server.routing.route +import io.ktor.server.util.getOrFail +import io.ktor.util.pipeline.PipelineContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.MissingFieldException import kotlinx.serialization.Serializable @Serializable diff --git a/src/main/kotlin/io/github/nomisrev/routes/root.kt b/src/main/kotlin/io/github/nomisrev/routes/root.kt index c49952a5..da4cd87c 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/root.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/root.kt @@ -10,7 +10,6 @@ fun Application.routes(deps: Dependencies) = routing { articleRoutes(deps.articleService, deps.jwtService) tagRoutes(deps.tagPersistence) profileRoutes(deps.userPersistence, deps.jwtService) - articleRoutes(deps.articleService, deps.jwtService) } @Resource("/api") data object RootResource From 610ccfd62d414c99418a911989e9aa512c25f5dc Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Wed, 11 Oct 2023 09:12:10 +0200 Subject: [PATCH 07/19] Migrate to the resource route definition --- .../io/github/nomisrev/routes/profile.kt | 53 ++++++++++++------- .../nomisrev/routes/ProfileRouteSpec.kt | 6 +-- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/main/kotlin/io/github/nomisrev/routes/profile.kt b/src/main/kotlin/io/github/nomisrev/routes/profile.kt index f9f1a89c..031cd7ba 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/profile.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/profile.kt @@ -10,9 +10,8 @@ import io.ktor.server.resources.delete import io.ktor.server.routing.Route import io.ktor.server.application.ApplicationCall import io.ktor.server.application.call +import io.ktor.server.resources.get import io.ktor.server.routing.Route -import io.ktor.server.routing.get -import io.ktor.server.routing.route import io.ktor.server.util.getOrFail import io.ktor.util.pipeline.PipelineContext import kotlinx.serialization.ExperimentalSerializationApi @@ -24,29 +23,47 @@ data class ProfileWrapper(val profile: T) @Serializable data class Profile( - val username: String, - val bio: String, - val image: String, - val following: Boolean + val username: String, + val bio: String, + val image: String, + val following: Boolean ) @Resource("/profiles") data class ProfilesResource(val parent: RootResource = RootResource) { - @Resource("/{username}/follow") - data class Follow(val parent: ProfilesResource = ProfilesResource(), val username: String) + @Resource("/{username}/follow") + data class Follow(val parent: ProfilesResource = ProfilesResource(), val username: String) + + @Resource("/{$USERNAME}") + data class Username(val parent: ProfileResource = ProfileResource(), val username: String) } fun Route.profileRoutes( - userPersistence: UserPersistence, - jwtService: JwtService + userPersistence: UserPersistence, + jwtService: JwtService ) { - delete { follow -> - jwtAuth(jwtService) { (_, userId) -> - either { - userPersistence.unfollowProfile(follow.username, userId) - val userUnfollowed = userPersistence.select(follow.username).bind() - ProfileWrapper(Profile(userUnfollowed.username, userUnfollowed.bio, userUnfollowed.image, false)) - }.respond(HttpStatusCode.OK) + get { + either { + val username = parameters(USERNAME, ::GetProfile).bind().username + profileService.getProfile(username).bind() + }.respond(HttpStatusCode.OK) + } + delete { follow -> + jwtAuth(jwtService) { (_, userId) -> + either { + userPersistence.unfollowProfile(follow.username, userId) + val userUnfollowed = userPersistence.select(follow.username).bind() + ProfileWrapper(Profile(userUnfollowed.username, userUnfollowed.bio, userUnfollowed.image, false)) + }.respond(HttpStatusCode.OK) + } } - } } + +@OptIn(ExperimentalSerializationApi::class) +private inline fun PipelineContext.parameters( + parameters: String, + noinline toRight: (String) -> A, +): Either = + Either.catchOrThrow { + call.parameters.getOrFail(parameters).let { toRight(it) } + }.mapLeft(::IncorrectJson) \ No newline at end of file diff --git a/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt b/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt index 83986ee3..ec65b4b5 100644 --- a/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt +++ b/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt @@ -7,7 +7,7 @@ import io.kotest.assertions.arrow.core.shouldBeRight import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.ktor.client.call.body -import io.ktor.client.request.bearerAuth +import io.ktor.client.plugins.resources.bearerAuth import io.ktor.client.plugins.resources.delete import io.ktor.http.HttpStatusCode @@ -69,7 +69,7 @@ class ProfileRouteSpec : dependencies.userService .register(RegisterUser(userName, validEmail, validPw)) .shouldBeRight() - val response = get("/profiles/$userName") { + val response = get(ProfileResource.Username(username = userName)) { contentType(ContentType.Application.Json) } @@ -85,7 +85,7 @@ class ProfileRouteSpec : "Get profile invalid username" { withServer { - val response = get("/profiles/$userName") { + val response = get(ProfileResource.Username(username = userName)) { contentType(ContentType.Application.Json) } From 7e662352be5a7d494043b394ed8d0ac782f391bc Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Wed, 11 Oct 2023 09:16:35 +0200 Subject: [PATCH 08/19] Rename the path extraction from parameters -> parameter since parameters is misleading, we only extract one with that API --- src/main/kotlin/io/github/nomisrev/routes/profile.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/io/github/nomisrev/routes/profile.kt b/src/main/kotlin/io/github/nomisrev/routes/profile.kt index 031cd7ba..a530a40c 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/profile.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/profile.kt @@ -44,7 +44,7 @@ fun Route.profileRoutes( ) { get { either { - val username = parameters(USERNAME, ::GetProfile).bind().username + val username = parameter(USERNAME, ::GetProfile).bind().username profileService.getProfile(username).bind() }.respond(HttpStatusCode.OK) } @@ -60,7 +60,7 @@ fun Route.profileRoutes( } @OptIn(ExperimentalSerializationApi::class) -private inline fun PipelineContext.parameters( +private inline fun PipelineContext.parameter( parameters: String, noinline toRight: (String) -> A, ): Either = From 95e523b08c9975f23c3a817639b70e33b800bd02 Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Wed, 11 Oct 2023 09:17:38 +0200 Subject: [PATCH 09/19] Address lint issues --- .../io/github/nomisrev/env/Dependencies.kt | 2 +- .../github/nomisrev/repo/UserPersistence.kt | 10 +--- .../io/github/nomisrev/routes/profile.kt | 6 +- .../github/nomisrev/service/ProfileService.kt | 14 ++--- .../nomisrev/routes/ProfileRouteSpec.kt | 59 ++++++++++--------- 5 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt b/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt index 99496275..cb31b418 100644 --- a/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt +++ b/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt @@ -18,8 +18,8 @@ import io.github.nomisrev.service.jwtService import io.github.nomisrev.service.profileService import io.github.nomisrev.service.slugifyGenerator import io.github.nomisrev.service.userService -import kotlinx.coroutines.Dispatchers import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.Dispatchers class Dependencies( val userService: UserService, diff --git a/src/main/kotlin/io/github/nomisrev/repo/UserPersistence.kt b/src/main/kotlin/io/github/nomisrev/repo/UserPersistence.kt index ce4c19e7..d3286b6c 100644 --- a/src/main/kotlin/io/github/nomisrev/repo/UserPersistence.kt +++ b/src/main/kotlin/io/github/nomisrev/repo/UserPersistence.kt @@ -33,7 +33,7 @@ interface UserPersistence { /** Select a User by its [UserId] */ suspend fun select(userId: UserId): Either - /** Select a User by its [UserName]*/ + /** Select a User by its username */ suspend fun select(username: String): Either suspend fun selectProfile(username: String): Either @@ -112,12 +112,8 @@ fun userPersistence( ensureNotNull(profileInfo) { UserNotFound("username=$username") } } - fun toProfile( - username: String, - bio: String, - image: String, - following: Long - ): Profile = Profile(username, bio, image, following > 0) + fun toProfile(username: String, bio: String, image: String, following: Long): Profile = + Profile(username, bio, image, following > 0) override suspend fun update( userId: UserId, diff --git a/src/main/kotlin/io/github/nomisrev/routes/profile.kt b/src/main/kotlin/io/github/nomisrev/routes/profile.kt index a530a40c..e0cfdd08 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/profile.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/profile.kt @@ -46,7 +46,8 @@ fun Route.profileRoutes( either { val username = parameter(USERNAME, ::GetProfile).bind().username profileService.getProfile(username).bind() - }.respond(HttpStatusCode.OK) + } + .respond(HttpStatusCode.OK) } delete { follow -> jwtAuth(jwtService) { (_, userId) -> @@ -66,4 +67,5 @@ private inline fun PipelineContext.para ): Either = Either.catchOrThrow { call.parameters.getOrFail(parameters).let { toRight(it) } - }.mapLeft(::IncorrectJson) \ No newline at end of file + }.mapLeft(::IncorrectJson) + diff --git a/src/main/kotlin/io/github/nomisrev/service/ProfileService.kt b/src/main/kotlin/io/github/nomisrev/service/ProfileService.kt index 771a4802..96502fc1 100644 --- a/src/main/kotlin/io/github/nomisrev/service/ProfileService.kt +++ b/src/main/kotlin/io/github/nomisrev/service/ProfileService.kt @@ -5,17 +5,17 @@ import io.github.nomisrev.DomainError import io.github.nomisrev.repo.UserPersistence import io.github.nomisrev.routes.Profile - interface ProfileService { - /** Select a Profile by its username */ - suspend fun getProfile(username: String): Either + /** Select a Profile by its username */ + suspend fun getProfile(username: String): Either } fun profileService( - repo: UserPersistence, -): ProfileService = object : ProfileService { + repo: UserPersistence, +): ProfileService = + object : ProfileService { override suspend fun getProfile( - username: String, + username: String, ): Either = repo.selectProfile(username) -} \ No newline at end of file + } diff --git a/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt b/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt index ec65b4b5..010f2965 100644 --- a/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt +++ b/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt @@ -12,7 +12,7 @@ import io.ktor.client.plugins.resources.delete import io.ktor.http.HttpStatusCode class ProfileRouteSpec : - StringSpec({ + StringSpec({ val validUsername = "username" val validEmail = "valid@domain.com" val validPw = "123456789" @@ -64,35 +64,36 @@ class ProfileRouteSpec : } } - "Get profile with no following" { - withServer { dependencies: Dependencies -> - dependencies.userService - .register(RegisterUser(userName, validEmail, validPw)) - .shouldBeRight() - val response = get(ProfileResource.Username(username = userName)) { - contentType(ContentType.Application.Json) - } + "Get profile with no following" { + withServer { dependencies: Dependencies -> + dependencies.userService + .register(RegisterUser(userName, validEmail, validPw)) + .shouldBeRight() + val response = + get(ProfileResource.Username(username = userName)) { + contentType(ContentType.Application.Json) + } - response.status shouldBe HttpStatusCode.OK - with(response.body()) { - username shouldBe userName - bio shouldBe "" - image shouldBe "" - following shouldBe false - } - } + response.status shouldBe HttpStatusCode.OK + with(response.body()) { + username shouldBe userName + bio shouldBe "" + image shouldBe "" + following shouldBe false } + } + } - "Get profile invalid username" { - withServer { - val response = get(ProfileResource.Username(username = userName)) { - contentType(ContentType.Application.Json) - } - - response.status shouldBe HttpStatusCode.UnprocessableEntity - response.body().errors.body shouldBe - listOf("User with username=$userName not found") - } - } + "Get profile invalid username" { + withServer { + val response = + get(ProfileResource.Username(username = userName)) { + contentType(ContentType.Application.Json) + } - }) + response.status shouldBe HttpStatusCode.UnprocessableEntity + response.body().errors.body shouldBe + listOf("User with username=$userName not found") + } + } + }) \ No newline at end of file From 3a14800e165b8d43b42122452fb36301c398b8dd Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Wed, 11 Oct 2023 16:21:58 +0200 Subject: [PATCH 10/19] Rename misleading parameter name --- src/main/kotlin/io/github/nomisrev/routes/profile.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/io/github/nomisrev/routes/profile.kt b/src/main/kotlin/io/github/nomisrev/routes/profile.kt index e0cfdd08..35bfce2d 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/profile.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/profile.kt @@ -62,10 +62,10 @@ fun Route.profileRoutes( @OptIn(ExperimentalSerializationApi::class) private inline fun PipelineContext.parameter( - parameters: String, + parameter: String, noinline toRight: (String) -> A, ): Either = Either.catchOrThrow { - call.parameters.getOrFail(parameters).let { toRight(it) } + call.parameters.getOrFail(parameter).let { toRight(it) } }.mapLeft(::IncorrectJson) From 5bf96ccbff9b8cc85bc3d318af4053520e0ec706 Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Thu, 19 Oct 2023 21:36:02 +0200 Subject: [PATCH 11/19] Remove the ProfileService as it's just essentially a wrapper around UserPersistence --- .../io/github/nomisrev/env/Dependencies.kt | 9 +++----- .../io/github/nomisrev/routes/profile.kt | 4 +++- .../github/nomisrev/service/ProfileService.kt | 21 ------------------- 3 files changed, 6 insertions(+), 28 deletions(-) delete mode 100644 src/main/kotlin/io/github/nomisrev/service/ProfileService.kt diff --git a/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt b/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt index cb31b418..468fdd99 100644 --- a/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt +++ b/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt @@ -11,11 +11,9 @@ import io.github.nomisrev.repo.tagPersistence import io.github.nomisrev.repo.userPersistence import io.github.nomisrev.service.ArticleService import io.github.nomisrev.service.JwtService -import io.github.nomisrev.service.ProfileService import io.github.nomisrev.service.UserService import io.github.nomisrev.service.articleService import io.github.nomisrev.service.jwtService -import io.github.nomisrev.service.profileService import io.github.nomisrev.service.slugifyGenerator import io.github.nomisrev.service.userService import kotlin.time.Duration.Companion.seconds @@ -23,7 +21,7 @@ import kotlinx.coroutines.Dispatchers class Dependencies( val userService: UserService, - val profileService: ProfileService, + val userRepo: UserPersistence, val jwtService: JwtService, val articleService: ArticleService, val healthCheck: HealthCheckRegistry, @@ -41,7 +39,6 @@ suspend fun ResourceScope.dependencies(env: Env): Dependencies { val jwtService = jwtService(env.auth, userRepo) val slugGenerator = slugifyGenerator() val userService = userService(userRepo, jwtService) - val profileService = profileService(userRepo) val checks = HealthCheckRegistry(Dispatchers.Default) { @@ -50,10 +47,10 @@ suspend fun ResourceScope.dependencies(env: Env): Dependencies { return Dependencies( userService = userService, + userRepo = userRepo, jwtService = jwtService, articleService = articleService(slugGenerator, articleRepo, userRepo, tagPersistence, favouritePersistence), healthCheck = checks, - tagPersistence, - userRepo + tagPersistence = tagPersistence ) } diff --git a/src/main/kotlin/io/github/nomisrev/routes/profile.kt b/src/main/kotlin/io/github/nomisrev/routes/profile.kt index 35bfce2d..2a405df0 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/profile.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/profile.kt @@ -4,6 +4,8 @@ import arrow.core.raise.either import io.github.nomisrev.auth.jwtAuth import io.github.nomisrev.repo.UserPersistence import io.github.nomisrev.service.JwtService +import io.github.nomisrev.IncorrectJson +import io.github.nomisrev.repo.UserPersistence import io.ktor.http.HttpStatusCode import io.ktor.resources.Resource import io.ktor.server.resources.delete @@ -45,7 +47,7 @@ fun Route.profileRoutes( get { either { val username = parameter(USERNAME, ::GetProfile).bind().username - profileService.getProfile(username).bind() + repo.selectProfile(username).bind() } .respond(HttpStatusCode.OK) } diff --git a/src/main/kotlin/io/github/nomisrev/service/ProfileService.kt b/src/main/kotlin/io/github/nomisrev/service/ProfileService.kt deleted file mode 100644 index 96502fc1..00000000 --- a/src/main/kotlin/io/github/nomisrev/service/ProfileService.kt +++ /dev/null @@ -1,21 +0,0 @@ -package io.github.nomisrev.service - -import arrow.core.Either -import io.github.nomisrev.DomainError -import io.github.nomisrev.repo.UserPersistence -import io.github.nomisrev.routes.Profile - -interface ProfileService { - /** Select a Profile by its username */ - suspend fun getProfile(username: String): Either -} - -fun profileService( - repo: UserPersistence, -): ProfileService = - object : ProfileService { - - override suspend fun getProfile( - username: String, - ): Either = repo.selectProfile(username) - } From 56bcca7ebe1aad803a798ea9c976a2587df3786a Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Sat, 21 Oct 2023 09:41:53 +0200 Subject: [PATCH 12/19] Add MissingParameter in DomainErrors and resolve it in error.kt --- src/main/kotlin/io/github/nomisrev/DomainError.kt | 2 ++ .../kotlin/io/github/nomisrev/routes/error.kt | 2 ++ .../kotlin/io/github/nomisrev/routes/profile.kt | 15 +++++++-------- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/io/github/nomisrev/DomainError.kt b/src/main/kotlin/io/github/nomisrev/DomainError.kt index bc7158ab..77861b2e 100644 --- a/src/main/kotlin/io/github/nomisrev/DomainError.kt +++ b/src/main/kotlin/io/github/nomisrev/DomainError.kt @@ -18,6 +18,8 @@ data class IncorrectInput(val errors: NonEmptyList) : ValidationEr constructor(head: InvalidField) : this(nonEmptyListOf(head)) } +data class MissingParameter(val name: String) : ValidationError + sealed interface UserError : DomainError data class UserNotFound(val property: String) : UserError diff --git a/src/main/kotlin/io/github/nomisrev/routes/error.kt b/src/main/kotlin/io/github/nomisrev/routes/error.kt index 0535ccc1..5fe66e79 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/error.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/error.kt @@ -10,6 +10,7 @@ import io.github.nomisrev.IncorrectInput import io.github.nomisrev.IncorrectJson import io.github.nomisrev.JwtGeneration import io.github.nomisrev.JwtInvalid +import io.github.nomisrev.MissingParameter import io.github.nomisrev.PasswordNotMatched import io.github.nomisrev.UserNotFound import io.github.nomisrev.UsernameAlreadyExists @@ -55,6 +56,7 @@ suspend fun PipelineContext.respond(error: DomainError): is JwtInvalid -> unprocessable(error.description) is CannotGenerateSlug -> unprocessable(error.description) is ArticleBySlugNotFound -> unprocessable("Article by slug ${error.slug} not found") + is MissingParameter -> unprocessable("Missing ${error.name} parameter in request") } private suspend inline fun PipelineContext.unprocessable( diff --git a/src/main/kotlin/io/github/nomisrev/routes/profile.kt b/src/main/kotlin/io/github/nomisrev/routes/profile.kt index 2a405df0..f018e982 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/profile.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/profile.kt @@ -1,6 +1,11 @@ package io.github.nomisrev.routes +import arrow.core.Either +import arrow.core.left import arrow.core.raise.either +import arrow.core.right +import io.github.nomisrev.DomainError +import io.github.nomisrev.MissingParameter import io.github.nomisrev.auth.jwtAuth import io.github.nomisrev.repo.UserPersistence import io.github.nomisrev.service.JwtService @@ -14,10 +19,7 @@ import io.ktor.server.application.ApplicationCall import io.ktor.server.application.call import io.ktor.server.resources.get import io.ktor.server.routing.Route -import io.ktor.server.util.getOrFail import io.ktor.util.pipeline.PipelineContext -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.MissingFieldException import kotlinx.serialization.Serializable @Serializable @@ -62,12 +64,9 @@ fun Route.profileRoutes( } } -@OptIn(ExperimentalSerializationApi::class) private inline fun PipelineContext.parameter( parameter: String, noinline toRight: (String) -> A, -): Either = - Either.catchOrThrow { - call.parameters.getOrFail(parameter).let { toRight(it) } - }.mapLeft(::IncorrectJson) +): Either = + call.parameters[parameter]?.let(toRight)?.right() ?: MissingParameter(parameter).left() From e820d09b559934090121425f0108cdba40c97f20 Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Sat, 21 Oct 2023 09:43:17 +0200 Subject: [PATCH 13/19] Remove unnecessary GetProfile --- src/main/kotlin/io/github/nomisrev/routes/profile.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/io/github/nomisrev/routes/profile.kt b/src/main/kotlin/io/github/nomisrev/routes/profile.kt index f018e982..4d02da93 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/profile.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/profile.kt @@ -48,7 +48,7 @@ fun Route.profileRoutes( ) { get { either { - val username = parameter(USERNAME, ::GetProfile).bind().username + val username = parameter(USERNAME) { it }.bind() repo.selectProfile(username).bind() } .respond(HttpStatusCode.OK) From 8c1113f2e4e27ecf34c454cff52821d8599dea35 Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Mon, 30 Oct 2023 11:27:09 +0100 Subject: [PATCH 14/19] Use the parameter from the provided route receiver --- src/main/kotlin/io/github/nomisrev/routes/profile.kt | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/io/github/nomisrev/routes/profile.kt b/src/main/kotlin/io/github/nomisrev/routes/profile.kt index 4d02da93..2b9e749e 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/profile.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/profile.kt @@ -46,14 +46,9 @@ fun Route.profileRoutes( userPersistence: UserPersistence, jwtService: JwtService ) { - get { - either { - val username = parameter(USERNAME) { it }.bind() - repo.selectProfile(username).bind() - } - .respond(HttpStatusCode.OK) - } - delete { follow -> + get { route -> + either { repo.selectProfile(route.username).bind() }.respond(HttpStatusCode.OK) + }delete { follow -> jwtAuth(jwtService) { (_, userId) -> either { userPersistence.unfollowProfile(follow.username, userId) From 87e034153c8c4563c3aeed3933db52aabbb42672 Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Tue, 31 Oct 2023 14:25:55 +0100 Subject: [PATCH 15/19] Get rid of duplicate Profile --- src/main/kotlin/io/github/nomisrev/routes/articles.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/kotlin/io/github/nomisrev/routes/articles.kt b/src/main/kotlin/io/github/nomisrev/routes/articles.kt index 956d3913..95f98c49 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/articles.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/articles.kt @@ -48,14 +48,6 @@ data class MultipleArticlesResponse( @JvmInline @Serializable value class FeedLimit(val limit: Int) -@Serializable -data class Profile( - val username: String, - val bio: String, - val image: String, - val following: Boolean -) - @Serializable data class Comment( val commentId: Long, From 6064c0b47e4e164f27ce7e2eaa5cd9a5e283aead Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Tue, 31 Oct 2023 14:26:13 +0100 Subject: [PATCH 16/19] Cleanup duplicate userRepo in Dependencies --- src/main/kotlin/io/github/nomisrev/env/Dependencies.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt b/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt index 468fdd99..00895c4f 100644 --- a/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt +++ b/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.Dispatchers class Dependencies( val userService: UserService, - val userRepo: UserPersistence, val jwtService: JwtService, val articleService: ArticleService, val healthCheck: HealthCheckRegistry, @@ -47,10 +46,11 @@ suspend fun ResourceScope.dependencies(env: Env): Dependencies { return Dependencies( userService = userService, - userRepo = userRepo, jwtService = jwtService, - articleService = articleService(slugGenerator, articleRepo, userRepo, tagPersistence, favouritePersistence), + articleService = + articleService(slugGenerator, articleRepo, userRepo, tagPersistence, favouritePersistence), healthCheck = checks, - tagPersistence = tagPersistence + tagPersistence = tagPersistence, + userPersistence = userRepo, ) } From 76bea8b64edebda3dd37fdf1973bc33f0b12a642 Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Tue, 31 Oct 2023 17:53:16 +0100 Subject: [PATCH 17/19] Remove the `parameter` implementation, we have access to the route object inside the type safe API --- .../io/github/nomisrev/routes/profile.kt | 66 +++++++------------ 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/src/main/kotlin/io/github/nomisrev/routes/profile.kt b/src/main/kotlin/io/github/nomisrev/routes/profile.kt index 2b9e749e..d458c07d 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/profile.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/profile.kt @@ -1,67 +1,51 @@ +@file:Suppress("MatchingDeclarationName") + package io.github.nomisrev.routes -import arrow.core.Either -import arrow.core.left import arrow.core.raise.either -import arrow.core.right -import io.github.nomisrev.DomainError -import io.github.nomisrev.MissingParameter import io.github.nomisrev.auth.jwtAuth import io.github.nomisrev.repo.UserPersistence import io.github.nomisrev.service.JwtService -import io.github.nomisrev.IncorrectJson -import io.github.nomisrev.repo.UserPersistence import io.ktor.http.HttpStatusCode import io.ktor.resources.Resource import io.ktor.server.resources.delete -import io.ktor.server.routing.Route -import io.ktor.server.application.ApplicationCall -import io.ktor.server.application.call import io.ktor.server.resources.get import io.ktor.server.routing.Route -import io.ktor.util.pipeline.PipelineContext import kotlinx.serialization.Serializable -@Serializable -data class ProfileWrapper(val profile: T) +@Serializable data class ProfileWrapper(val profile: T) @Serializable data class Profile( - val username: String, - val bio: String, - val image: String, - val following: Boolean + val username: String, + val bio: String, + val image: String, + val following: Boolean ) @Resource("/profiles") data class ProfilesResource(val parent: RootResource = RootResource) { - @Resource("/{username}/follow") - data class Follow(val parent: ProfilesResource = ProfilesResource(), val username: String) + @Resource("/{username}") + data class Username(val parent: ProfilesResource = ProfilesResource(), val username: String) - @Resource("/{$USERNAME}") - data class Username(val parent: ProfileResource = ProfileResource(), val username: String) + @Resource("/{username}/follow") + data class Follow(val parent: ProfilesResource = ProfilesResource(), val username: String) } -fun Route.profileRoutes( - userPersistence: UserPersistence, - jwtService: JwtService -) { - get { route -> - either { repo.selectProfile(route.username).bind() }.respond(HttpStatusCode.OK) - }delete { follow -> - jwtAuth(jwtService) { (_, userId) -> - either { - userPersistence.unfollowProfile(follow.username, userId) - val userUnfollowed = userPersistence.select(follow.username).bind() - ProfileWrapper(Profile(userUnfollowed.username, userUnfollowed.bio, userUnfollowed.image, false)) - }.respond(HttpStatusCode.OK) +fun Route.profileRoutes(userPersistence: UserPersistence, jwtService: JwtService) { + get { route -> + either { userPersistence.selectProfile(route.username).bind() }.respond(HttpStatusCode.OK) + } + delete { follow -> + jwtAuth(jwtService) { (_, userId) -> + either { + userPersistence.unfollowProfile(follow.username, userId) + val userUnfollowed = userPersistence.select(follow.username).bind() + ProfileWrapper( + Profile(userUnfollowed.username, userUnfollowed.bio, userUnfollowed.image, false) + ) } + .respond(HttpStatusCode.OK) } + } } - -private inline fun PipelineContext.parameter( - parameter: String, - noinline toRight: (String) -> A, -): Either = - call.parameters[parameter]?.let(toRight)?.right() ?: MissingParameter(parameter).left() - From 85eb3930d1f64e5c4226b3f679aa0b42ee31d732 Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Tue, 31 Oct 2023 20:45:34 +0100 Subject: [PATCH 18/19] Make the username optional in an attempt to trigger /api/profile --- src/main/kotlin/io/github/nomisrev/routes/profile.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/io/github/nomisrev/routes/profile.kt b/src/main/kotlin/io/github/nomisrev/routes/profile.kt index d458c07d..94c366f1 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/profile.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/profile.kt @@ -3,6 +3,8 @@ package io.github.nomisrev.routes import arrow.core.raise.either +import arrow.core.raise.ensure +import io.github.nomisrev.MissingParameter import io.github.nomisrev.auth.jwtAuth import io.github.nomisrev.repo.UserPersistence import io.github.nomisrev.service.JwtService @@ -25,8 +27,8 @@ data class Profile( @Resource("/profiles") data class ProfilesResource(val parent: RootResource = RootResource) { - @Resource("/{username}") - data class Username(val parent: ProfilesResource = ProfilesResource(), val username: String) + @Resource("/{username?}") + data class Username(val parent: ProfilesResource = ProfilesResource(), val username: String?) @Resource("/{username}/follow") data class Follow(val parent: ProfilesResource = ProfilesResource(), val username: String) @@ -34,7 +36,11 @@ data class ProfilesResource(val parent: RootResource = RootResource) { fun Route.profileRoutes(userPersistence: UserPersistence, jwtService: JwtService) { get { route -> - either { userPersistence.selectProfile(route.username).bind() }.respond(HttpStatusCode.OK) + either { + ensure(!route.username.isNullOrBlank()) { MissingParameter("username") } + userPersistence.selectProfile(route.username).bind() + } + .respond(HttpStatusCode.OK) } delete { follow -> jwtAuth(jwtService) { (_, userId) -> From 64dbe4d7141fe837da867733f527b93f08cd4b90 Mon Sep 17 00:00:00 2001 From: marinjuricev Date: Tue, 31 Oct 2023 20:45:54 +0100 Subject: [PATCH 19/19] Add test that covers the missing username path --- .../github/nomisrev/repo/UserPersistence.kt | 6 +- .../nomisrev/routes/ProfileRouteSpec.kt | 122 ++++++++++-------- 2 files changed, 73 insertions(+), 55 deletions(-) diff --git a/src/main/kotlin/io/github/nomisrev/repo/UserPersistence.kt b/src/main/kotlin/io/github/nomisrev/repo/UserPersistence.kt index d3286b6c..d3ffdd9f 100644 --- a/src/main/kotlin/io/github/nomisrev/repo/UserPersistence.kt +++ b/src/main/kotlin/io/github/nomisrev/repo/UserPersistence.kt @@ -139,10 +139,8 @@ fun userPersistence( ensureNotNull(info) { UserNotFound("userId=$userId") } } - override suspend fun unfollowProfile( - followedUsername: String, - followerId: UserId - ): Unit = followingQueries.delete(followedUsername, followerId.serial) + override suspend fun unfollowProfile(followedUsername: String, followerId: UserId): Unit = + followingQueries.delete(followedUsername, followerId.serial) private fun generateSalt(): ByteArray = UUID.randomUUID().toString().toByteArray() diff --git a/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt b/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt index 010f2965..6c4e4bce 100644 --- a/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt +++ b/src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt @@ -7,76 +7,83 @@ import io.kotest.assertions.arrow.core.shouldBeRight import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.ktor.client.call.body -import io.ktor.client.plugins.resources.bearerAuth import io.ktor.client.plugins.resources.delete +import io.ktor.client.plugins.resources.get +import io.ktor.client.request.bearerAuth +import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType class ProfileRouteSpec : StringSpec({ - val validUsername = "username" - val validEmail = "valid@domain.com" - val validPw = "123456789" - val validUsernameFollowed = "username2" - val validEmailFollowed = "valid2@domain.com" - - "Can unfollow profile" { - withServer { dependencies -> - val token = dependencies.userService - .register(RegisterUser(validUsername, validEmail, validPw)) - .shouldBeRight() - dependencies.userService - .register(RegisterUser(validUsernameFollowed, validEmailFollowed, validPw)) - .shouldBeRight() - - val response = delete(ProfilesResource.Follow(username = validUsernameFollowed)) { - bearerAuth(token.value) - } - - response.status shouldBe HttpStatusCode.OK - with(response.body>().profile) { - username shouldBe validUsernameFollowed - bio shouldBe "" - image shouldBe "" - following shouldBe false - } - } - } + val validUsername = "username" + val validEmail = "valid@domain.com" + val validPw = "123456789" + val validUsernameFollowed = "username2" + val validEmailFollowed = "valid2@domain.com" - "Needs token to unfollow" { - withServer { - val response = delete(ProfilesResource.Follow(username = validUsernameFollowed)) + "Can unfollow profile" { + withServer { dependencies -> + val token = + dependencies.userService + .register(RegisterUser(validUsername, validEmail, validPw)) + .shouldBeRight() + dependencies.userService + .register(RegisterUser(validUsernameFollowed, validEmailFollowed, validPw)) + .shouldBeRight() - response.status shouldBe HttpStatusCode.Unauthorized - } + val response = + delete(ProfilesResource.Follow(username = validUsernameFollowed)) { + bearerAuth(token.value) + } + + response.status shouldBe HttpStatusCode.OK + with(response.body>().profile) { + username shouldBe validUsernameFollowed + bio shouldBe "" + image shouldBe "" + following shouldBe false } + } + } - "Username invalid to unfollow" { - withServer { dependencies -> - val token = dependencies.userService - .register(RegisterUser(validUsername, validEmail, validPw)) - .shouldBeRight() + "Needs token to unfollow" { + withServer { + val response = delete(ProfilesResource.Follow(username = validUsernameFollowed)) - val response = delete(ProfilesResource.Follow(username = validUsernameFollowed)) { - bearerAuth(token.value) - } + response.status shouldBe HttpStatusCode.Unauthorized + } + } - response.status shouldBe HttpStatusCode.UnprocessableEntity - } - } + "Username invalid to unfollow" { + withServer { dependencies -> + val token = + dependencies.userService + .register(RegisterUser(validUsername, validEmail, validPw)) + .shouldBeRight() + + val response = + delete(ProfilesResource.Follow(username = validUsernameFollowed)) { + bearerAuth(token.value) + } + + response.status shouldBe HttpStatusCode.UnprocessableEntity + } + } "Get profile with no following" { withServer { dependencies: Dependencies -> dependencies.userService - .register(RegisterUser(userName, validEmail, validPw)) + .register(RegisterUser(validUsername, validEmail, validPw)) .shouldBeRight() val response = - get(ProfileResource.Username(username = userName)) { + get(ProfilesResource.Username(username = validUsername)) { contentType(ContentType.Application.Json) } response.status shouldBe HttpStatusCode.OK with(response.body()) { - username shouldBe userName + username shouldBe validUsername bio shouldBe "" image shouldBe "" following shouldBe false @@ -87,13 +94,26 @@ class ProfileRouteSpec : "Get profile invalid username" { withServer { val response = - get(ProfileResource.Username(username = userName)) { + get(ProfilesResource.Username(username = validUsername)) { + contentType(ContentType.Application.Json) + } + + response.status shouldBe HttpStatusCode.UnprocessableEntity + response.body().errors.body shouldBe + listOf("User with username=$validUsername not found") + } + } + + "Get profile by username missing username" { + withServer { + val response = + get(ProfilesResource.Username(username = "")) { contentType(ContentType.Application.Json) } response.status shouldBe HttpStatusCode.UnprocessableEntity response.body().errors.body shouldBe - listOf("User with username=$userName not found") + listOf("Missing username parameter in request") } } - }) \ No newline at end of file + })