Skip to content

Commit

Permalink
POST follow profile (#194)
Browse files Browse the repository at this point in the history
Co-authored-by: Simon Vergauwen <[email protected]>
  • Loading branch information
organize and nomisRev authored Nov 23, 2023
1 parent 2707b3e commit 0bb8af6
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 5 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ ktor {
spotless {
kotlin {
targetExclude("**/build/**")
ktfmt().googleStyle()
ktfmt("0.46").googleStyle()
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/main/kotlin/io/github/nomisrev/auth/jwt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ suspend inline fun PipelineContext<Unit, ApplicationCall>.optionalJwtAuth(
{ error -> respond(error) },
{ userId -> body(this, JwtContext(JwtToken(token), userId)) }
)
}
?: body(this, null)
} ?: body(this, null)
}

fun PipelineContext<Unit, ApplicationCall>.jwtToken(): String? =
Expand Down
17 changes: 16 additions & 1 deletion src/main/kotlin/io/github/nomisrev/repo/UserPersistence.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.nomisrev.repo

import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.ensureNotNull
Expand Down Expand Up @@ -48,7 +49,9 @@ interface UserPersistence {
image: String?
): Either<DomainError, UserInfo>

suspend fun unfollowProfile(followedUsername: String, followerId: UserId): Unit
suspend fun unfollowProfile(followedUsername: String, followerId: UserId)

suspend fun followProfile(followedUsername: String, followerId: UserId): Either<DomainError, Unit>
}

/** UserPersistence implementation based on SqlDelight and JavaX Crypto */
Expand Down Expand Up @@ -142,6 +145,18 @@ fun userPersistence(
override suspend fun unfollowProfile(followedUsername: String, followerId: UserId): Unit =
followingQueries.delete(followedUsername, followerId.serial)

override suspend fun followProfile(
followedUsername: String,
followerId: UserId
): Either<DomainError, Unit> = either {
catch({ followingQueries.insertByUsername(followedUsername, followerId.serial) }) {
psqlException: PSQLException ->
if (psqlException.sqlState == PSQLState.NOT_NULL_VIOLATION.state)
raise(UserNotFound("username=$followedUsername"))
else throw psqlException
}
}

private fun generateSalt(): ByteArray = UUID.randomUUID().toString().toByteArray()

private fun generateKey(password: String, salt: ByteArray): ByteArray {
Expand Down
1 change: 0 additions & 1 deletion src/main/kotlin/io/github/nomisrev/routes/error.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import kotlinx.serialization.Serializable
@Serializable data class GenericErrorModelErrors(val body: List<String>)

context(PipelineContext<Unit, ApplicationCall>)

suspend inline fun <reified A : Any> Either<DomainError, A>.respond(status: HttpStatusCode): Unit =
when (this) {
is Either.Left -> respond(value)
Expand Down
11 changes: 11 additions & 0 deletions src/main/kotlin/io/github/nomisrev/routes/profile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.ktor.http.HttpStatusCode
import io.ktor.resources.Resource
import io.ktor.server.resources.delete
import io.ktor.server.resources.get
import io.ktor.server.resources.post
import io.ktor.server.routing.Route
import kotlinx.serialization.Serializable

Expand Down Expand Up @@ -55,4 +56,14 @@ fun Route.profileRoutes(userPersistence: UserPersistence, jwtService: JwtService
.respond(HttpStatusCode.OK)
}
}
post<ProfilesResource.Follow> { follow ->
jwtAuth(jwtService) { (_, userId) ->
either {
userPersistence.followProfile(follow.username, userId)
val userFollowed = userPersistence.select(follow.username).bind()
ProfileWrapper(Profile(userFollowed.username, userFollowed.bio, userFollowed.image, true))
}
.respond(HttpStatusCode.OK)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ insert:
INSERT INTO following(followed_id, follower_id)
VALUES (:followedId, :followerId);

insertByUsername:
INSERT INTO following(followed_id, follower_id)
VALUES ((SELECT id FROM users WHERE users.username=:username), :followerId);

delete:
DELETE FROM following
WHERE followed_id=(SELECT id FROM users WHERE users.username=:username) AND follower_id = :followerId;
50 changes: 50 additions & 0 deletions src/test/kotlin/io/github/nomisrev/routes/ProfileRouteSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.kotest.matchers.shouldBe
import io.ktor.client.call.body
import io.ktor.client.plugins.resources.delete
import io.ktor.client.plugins.resources.get
import io.ktor.client.plugins.resources.post
import io.ktor.client.request.bearerAuth
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
Expand All @@ -22,6 +23,31 @@ class ProfileRouteSpec :
val validUsernameFollowed = "username2"
val validEmailFollowed = "[email protected]"

"Can follow profile" {
withServer { dependencies ->
val token =
dependencies.userService
.register(RegisterUser(validUsername, validEmail, validPw))
.shouldBeRight()
dependencies.userService
.register(RegisterUser(validUsernameFollowed, validEmailFollowed, validPw))
.shouldBeRight()

val response =
post(ProfilesResource.Follow(username = validUsernameFollowed)) {
bearerAuth(token.value)
}

response.status shouldBe HttpStatusCode.OK
with(response.body<ProfileWrapper<Profile>>().profile) {
username shouldBe validUsernameFollowed
bio shouldBe ""
image shouldBe ""
following shouldBe true
}
}
}

"Can unfollow profile" {
withServer { dependencies ->
val token =
Expand All @@ -47,6 +73,14 @@ class ProfileRouteSpec :
}
}

"Needs token to follow" {
withServer {
val response = post(ProfilesResource.Follow(username = validUsernameFollowed))

response.status shouldBe HttpStatusCode.Unauthorized
}
}

"Needs token to unfollow" {
withServer {
val response = delete(ProfilesResource.Follow(username = validUsernameFollowed))
Expand All @@ -55,6 +89,22 @@ class ProfileRouteSpec :
}
}

"Username invalid to follow" {
withServer { dependencies ->
val token =
dependencies.userService
.register(RegisterUser(validUsername, validEmail, validPw))
.shouldBeRight()

val response =
post(ProfilesResource.Follow(username = validUsernameFollowed)) {
bearerAuth(token.value)
}

response.status shouldBe HttpStatusCode.UnprocessableEntity
}
}

"Username invalid to unfollow" {
withServer { dependencies ->
val token =
Expand Down

0 comments on commit 0bb8af6

Please sign in to comment.