Skip to content

Commit 505c4b0

Browse files
Paul ScottPaul Scott
authored andcommitted
Added some possible changes to capture more errors and remove Option<User>
1 parent a9e8de9 commit 505c4b0

File tree

15 files changed

+132
-110
lines changed

15 files changed

+132
-110
lines changed

http4k-example/src/main/kotlin/za/co/ee/learning/domain/security/JWTProvider.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ data class TokenInfo(
1010
)
1111

1212
interface JWTProvider {
13-
fun generate(user: User): TokenInfo
13+
fun generate(user: User): DomainResult<TokenInfo>
1414

1515
fun verify(jwt: String): DomainResult<UUID>
1616
}

http4k-example/src/main/kotlin/za/co/ee/learning/domain/security/PasswordProvider.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package za.co.ee.learning.domain.security
22

3+
import za.co.ee.learning.domain.DomainResult
4+
35
interface PasswordProvider {
4-
fun encode(password: String): String
6+
fun encode(password: String): DomainResult<String>
57

68
fun matches(
79
password: String,
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package za.co.ee.learning.domain.users
22

3+
import za.co.ee.learning.domain.DomainResult
34
import java.util.UUID
45

56
data class User(
67
val id: UUID,
78
val email: String,
8-
val passwordHash: String,
9+
val passwordHash: DomainResult<String>,
910
)
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package za.co.ee.learning.domain.users
22

3-
import arrow.core.Option
3+
import arrow.core.Either
4+
import za.co.ee.learning.domain.DomainError
45
import za.co.ee.learning.domain.DomainResult
56

67
interface UserRepository {
7-
fun findByEmail(email: String): DomainResult<Option<User>>
8+
fun findByEmail(email: String): DomainResult<User>
89

910
fun findAll(): DomainResult<List<User>>
1011
}

http4k-example/src/main/kotlin/za/co/ee/learning/domain/users/usecases/Authenticate.kt

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package za.co.ee.learning.domain.users.usecases
22

3-
import arrow.core.Option
43
import arrow.core.left
54
import arrow.core.raise.either
65
import arrow.core.right
@@ -31,9 +30,10 @@ class Authenticate(
3130
operator fun invoke(request: AuthenticateRequest): DomainResult<AuthenticateResponse> =
3231
either {
3332
val validatedRequest = validate(request).bind()
34-
val user = findUser(validatedRequest.email).bind()
35-
val authenticatedUser = checkPassword(user, validatedRequest).bind()
36-
createToken(authenticatedUser).bind()
33+
val user = userRepository.findByEmail(validatedRequest.email).bind()
34+
val pwdHashString = user.passwordHash.bind()
35+
checkPassword(pwdHashString, validatedRequest).bind()
36+
createToken(user).bind()
3737
}
3838

3939
private fun validate(request: AuthenticateRequest): DomainResult<AuthenticateRequest> {
@@ -56,31 +56,22 @@ class Authenticate(
5656
return request.right()
5757
}
5858

59-
private fun findUser(email: String): DomainResult<User> =
60-
either {
61-
val optUser: Option<User> = userRepository.findByEmail(email).bind()
62-
return optUser.fold(
63-
ifEmpty = { DomainError.InvalidCredentials.left() },
64-
ifSome = { user -> user.right() },
65-
)
66-
}
67-
6859
private fun checkPassword(
69-
user: User,
60+
passwordHash: String,
7061
validatedRequest: AuthenticateRequest,
71-
): DomainResult<User> {
72-
if (passwordProvider.matches(validatedRequest.password, user.passwordHash)) {
73-
return user.right()
62+
): DomainResult<Boolean> {
63+
if (passwordProvider.matches(validatedRequest.password, passwordHash)) {
64+
return true.right()
7465
}
7566

7667
return DomainError.InvalidCredentials.left()
7768
}
7869

79-
private fun createToken(authenticatedUser: User): DomainResult<AuthenticateResponse> {
80-
val tokenInfo: TokenInfo = jwtProvider.generate(authenticatedUser)
81-
return AuthenticateResponse(
70+
private fun createToken(authenticatedUser: User): DomainResult<AuthenticateResponse> = either {
71+
val tokenInfo: TokenInfo = jwtProvider.generate(authenticatedUser).bind()
72+
AuthenticateResponse(
8273
token = tokenInfo.token,
8374
expires = tokenInfo.expires,
84-
).right()
75+
)
8576
}
8677
}

http4k-example/src/main/kotlin/za/co/ee/learning/infrastructure/database/InMemoryUserRepository.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package za.co.ee.learning.infrastructure.database
22

3+
import arrow.core.Either
34
import arrow.core.Option
45
import arrow.core.right
6+
import za.co.ee.learning.domain.DomainError
57
import za.co.ee.learning.domain.DomainResult
68
import za.co.ee.learning.domain.users.User
79
import za.co.ee.learning.domain.users.UserRepository
@@ -16,10 +18,10 @@ class InMemoryUserRepository : UserRepository {
1618
}
1719

1820
// Find the first user in the list that has the matching email, wrap it in an option and return a Either.right()
19-
override fun findByEmail(email: String): DomainResult<Option<User>> =
21+
override fun findByEmail(email: String): DomainResult<User> =
2022
Option
2123
.fromNullable(users.firstOrNull { it.email == email })
22-
.right()
24+
.toEither { DomainError.InvalidCredentials }
2325

2426
override fun findAll(): DomainResult<List<User>> = users.toList().right()
2527
}

http4k-example/src/main/kotlin/za/co/ee/learning/infrastructure/security/BCryptPasswordProvider.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
package za.co.ee.learning.infrastructure.security
22

3+
import arrow.core.raise.either
34
import org.mindrot.jbcrypt.BCrypt
5+
import za.co.ee.learning.domain.DomainError
6+
import za.co.ee.learning.domain.DomainResult
47
import za.co.ee.learning.domain.security.PasswordProvider
58

69
class BCryptPasswordProvider : PasswordProvider {
7-
override fun encode(password: String): String = BCrypt.hashpw(password, BCrypt.gensalt())
10+
override fun encode(password: String): DomainResult<String> =
11+
either {
12+
try {
13+
BCrypt.hashpw(password, BCrypt.gensalt())
14+
} catch (e: Exception) {
15+
raise(DomainError.ValidationError("Error encoding password: ${e.message}"))
16+
}
17+
}
818

919
override fun matches(
1020
password: String,

http4k-example/src/main/kotlin/za/co/ee/learning/infrastructure/security/DefaultJWTProvider.kt

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ package za.co.ee.learning.infrastructure.security
33
import arrow.core.raise.either
44
import com.auth0.jwt.JWT
55
import com.auth0.jwt.algorithms.Algorithm
6+
import com.auth0.jwt.exceptions.JWTCreationException
67
import com.auth0.jwt.exceptions.JWTVerificationException
78
import za.co.ee.learning.domain.DomainError
89
import za.co.ee.learning.domain.DomainResult
910
import za.co.ee.learning.domain.security.JWTProvider
1011
import za.co.ee.learning.domain.security.TokenInfo
1112
import za.co.ee.learning.domain.users.User
1213
import java.time.Instant
13-
import java.util.Date
14-
import java.util.UUID
14+
import java.util.*
1515

1616
class DefaultJWTProvider(
1717
private val secret: String,
@@ -26,24 +26,30 @@ class DefaultJWTProvider(
2626
.withIssuer(issuer)
2727
.build()
2828

29-
override fun generate(user: User): TokenInfo {
30-
val now = Instant.now()
31-
val expiresAt = now.plusSeconds(expirationSeconds)
29+
override fun generate(user: User): DomainResult<TokenInfo> =
30+
either {
31+
try {
32+
val now = Instant.now()
33+
val expiresAt = now.plusSeconds(expirationSeconds)
3234

33-
val token =
34-
JWT
35-
.create()
36-
.withIssuer(issuer)
37-
.withSubject(user.id.toString())
38-
.withIssuedAt(Date.from(now))
39-
.withExpiresAt(Date.from(expiresAt))
40-
.sign(algorithm)
35+
val token = JWT
36+
.create()
37+
.withIssuer(issuer)
38+
.withSubject(user.id.toString())
39+
.withIssuedAt(Date.from(now))
40+
.withExpiresAt(Date.from(expiresAt))
41+
.sign(algorithm)
4142

42-
return TokenInfo(
43-
token = token,
44-
expires = expiresAt.epochSecond,
45-
)
46-
}
43+
TokenInfo(
44+
token = token,
45+
expires = expiresAt.epochSecond,
46+
)
47+
} catch (e: JWTCreationException) {
48+
raise(DomainError.JWTError("Token creation error: ${e.message}"))
49+
} catch (e: Exception) {
50+
raise(DomainError.JWTError("Token creation failed: ${e.message}"))
51+
}
52+
}
4753

4854
override fun verify(jwt: String): DomainResult<UUID> =
4955
either {

http4k-example/src/test/kotlin/za/co/ee/learning/domain/users/usecases/AuthenticateTest.kt

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package za.co.ee.learning.domain.users.usecases
22

3+
import arrow.core.Either
34
import arrow.core.left
45
import arrow.core.right
56
import arrow.core.some
@@ -28,17 +29,19 @@ class AuthenticateTest :
2829

2930
val validEmail = "[email protected]"
3031
val validPassword = "SecurePass123"
31-
val passwordHash = "hashed_password"
32+
val passwordHash = Either.Right("hashed_password")
3233
val testUser =
3334
User(
3435
id = UUID.randomUUID(),
3536
email = validEmail,
3637
passwordHash = passwordHash,
3738
)
3839
val tokenInfo =
39-
TokenInfo(
40-
token = "jwt.token.here",
41-
expires = 1234567890L,
40+
Either.Right(
41+
TokenInfo(
42+
token = "jwt.token.here",
43+
expires = 1234567890L,
44+
)
4245
)
4346

4447
beforeTest {
@@ -49,21 +52,21 @@ class AuthenticateTest :
4952
test("should return token when credentials are valid") {
5053
val request = AuthenticateRequest(email = validEmail, password = validPassword)
5154

52-
every { userRepository.findByEmail(validEmail) } returns testUser.some().right()
53-
every { passwordProvider.matches(validPassword, passwordHash) } returns true
55+
every { userRepository.findByEmail(validEmail) } returns testUser.right()
56+
every { passwordProvider.matches(validPassword, passwordHash.getOrNull()!!) } returns true
5457
every { jwtProvider.generate(testUser) } returns tokenInfo
5558

5659
val result = authenticate(request)
5760

5861
val value = result.shouldBeRight()
5962
value shouldBe
60-
AuthenticateResponse(
61-
token = tokenInfo.token,
62-
expires = tokenInfo.expires,
63-
)
63+
AuthenticateResponse(
64+
token = tokenInfo.getOrNull()!!.token,
65+
expires = tokenInfo.getOrNull()!!.expires,
66+
)
6467

6568
verify { userRepository.findByEmail(validEmail) }
66-
verify { passwordProvider.matches(validPassword, passwordHash) }
69+
verify { passwordProvider.matches(validPassword, passwordHash.getOrNull()!!) }
6770
verify { jwtProvider.generate(testUser) }
6871
}
6972
}
@@ -113,7 +116,7 @@ class AuthenticateTest :
113116
test("should return InvalidCredentials when user does not exist") {
114117
val request = AuthenticateRequest(email = "[email protected]", password = validPassword)
115118

116-
every { userRepository.findByEmail("[email protected]") } returns arrow.core.none<User>().right()
119+
every { userRepository.findByEmail("[email protected]") } returns Either.Left(DomainError.InvalidCredentials)
117120

118121
val result = authenticate(request)
119122

@@ -126,7 +129,7 @@ class AuthenticateTest :
126129
test("should return InvalidCredentials when repository returns None") {
127130
val request = AuthenticateRequest(email = validEmail, password = validPassword)
128131

129-
every { userRepository.findByEmail(validEmail) } returns arrow.core.none<User>().right()
132+
every { userRepository.findByEmail(validEmail) } returns Either.Left(DomainError.InvalidCredentials)
130133

131134
val result = authenticate(request)
132135

@@ -139,16 +142,16 @@ class AuthenticateTest :
139142
test("should return InvalidCredentials when password does not match") {
140143
val request = AuthenticateRequest(email = validEmail, password = "WrongPassword123")
141144

142-
every { userRepository.findByEmail(validEmail) } returns testUser.some().right()
143-
every { passwordProvider.matches("WrongPassword123", passwordHash) } returns false
145+
every { userRepository.findByEmail(validEmail) } returns testUser.right()
146+
every { passwordProvider.matches("WrongPassword123", passwordHash.getOrNull()!!) } returns false
144147

145148
val result = authenticate(request)
146149

147150
val error = result.shouldBeLeft()
148151
error shouldBe DomainError.InvalidCredentials
149152

150153
verify { userRepository.findByEmail(validEmail) }
151-
verify { passwordProvider.matches("WrongPassword123", passwordHash) }
154+
verify { passwordProvider.matches("WrongPassword123", passwordHash.getOrNull()!!) }
152155
}
153156
}
154157

http4k-example/src/test/kotlin/za/co/ee/learning/domain/users/usecases/GetUsersTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package za.co.ee.learning.domain.users.usecases
22

3+
import arrow.core.Either
34
import arrow.core.left
45
import arrow.core.right
56
import io.kotest.assertions.arrow.core.shouldBeLeft
@@ -26,13 +27,13 @@ class GetUsersTest :
2627
User(
2728
id = UUID.randomUUID(),
2829
email = "[email protected]",
29-
passwordHash = "hash1",
30+
passwordHash = Either.Right("hash1"),
3031
)
3132
val user2 =
3233
User(
3334
id = UUID.randomUUID(),
3435
email = "[email protected]",
35-
passwordHash = "hash2",
36+
passwordHash = Either.Right("hash2"),
3637
)
3738

3839
beforeTest {

0 commit comments

Comments
 (0)