Skip to content

Commit 40acc63

Browse files
Paul ScottPaul Scott
authored andcommitted
Changes based on PR comments
1 parent 505c4b0 commit 40acc63

File tree

8 files changed

+88
-82
lines changed

8 files changed

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

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

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

109
fun findAll(): DomainResult<List<User>>
1110
}

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ class Authenticate(
3030
operator fun invoke(request: AuthenticateRequest): DomainResult<AuthenticateResponse> =
3131
either {
3232
val validatedRequest = validate(request).bind()
33-
val user = userRepository.findByEmail(validatedRequest.email).bind()
33+
val userOption = userRepository.findByEmail(validatedRequest.email).bind()
34+
val user = userOption.toEither { DomainError.InvalidCredentials }.bind()
3435
val pwdHashString = user.passwordHash.bind()
35-
checkPassword(pwdHashString, validatedRequest).bind()
36-
createToken(user).bind()
36+
val authenticatedUser = authenticateUser(user, pwdHashString, validatedRequest).bind()
37+
createToken(authenticatedUser).bind()
3738
}
3839

3940
private fun validate(request: AuthenticateRequest): DomainResult<AuthenticateRequest> {
@@ -56,14 +57,14 @@ class Authenticate(
5657
return request.right()
5758
}
5859

59-
private fun checkPassword(
60+
private fun authenticateUser(
61+
user: User,
6062
passwordHash: String,
6163
validatedRequest: AuthenticateRequest,
62-
): DomainResult<Boolean> {
64+
): DomainResult<User> {
6365
if (passwordProvider.matches(validatedRequest.password, passwordHash)) {
64-
return true.right()
66+
return user.right()
6567
}
66-
6768
return DomainError.InvalidCredentials.left()
6869
}
6970

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ class InMemoryUserRepository : UserRepository {
1818
}
1919

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

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

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

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,20 @@ class AuthenticateTest :
2929

3030
val validEmail = "[email protected]"
3131
val validPassword = "SecurePass123"
32-
val passwordHash = Either.Right("hashed_password")
32+
val passwordHash = "hashed_password"
3333
val testUser =
3434
User(
3535
id = UUID.randomUUID(),
3636
email = validEmail,
37-
passwordHash = passwordHash,
37+
passwordHash = Either.Right(passwordHash),
3838
)
39+
val testToken = "jwt.token.here"
40+
val testExpires = 1234567890L
3941
val tokenInfo =
4042
Either.Right(
4143
TokenInfo(
42-
token = "jwt.token.here",
43-
expires = 1234567890L,
44+
token = testToken,
45+
expires = testExpires,
4446
)
4547
)
4648

@@ -52,21 +54,21 @@ class AuthenticateTest :
5254
test("should return token when credentials are valid") {
5355
val request = AuthenticateRequest(email = validEmail, password = validPassword)
5456

55-
every { userRepository.findByEmail(validEmail) } returns testUser.right()
56-
every { passwordProvider.matches(validPassword, passwordHash.getOrNull()!!) } returns true
57+
every { userRepository.findByEmail(validEmail) } returns testUser.some().right()
58+
every { passwordProvider.matches(validPassword, passwordHash) } returns true
5759
every { jwtProvider.generate(testUser) } returns tokenInfo
5860

5961
val result = authenticate(request)
6062

6163
val value = result.shouldBeRight()
6264
value shouldBe
6365
AuthenticateResponse(
64-
token = tokenInfo.getOrNull()!!.token,
65-
expires = tokenInfo.getOrNull()!!.expires,
66+
token = testToken,
67+
expires = testExpires,
6668
)
6769

6870
verify { userRepository.findByEmail(validEmail) }
69-
verify { passwordProvider.matches(validPassword, passwordHash.getOrNull()!!) }
71+
verify { passwordProvider.matches(validPassword, passwordHash) }
7072
verify { jwtProvider.generate(testUser) }
7173
}
7274
}
@@ -116,7 +118,7 @@ class AuthenticateTest :
116118
test("should return InvalidCredentials when user does not exist") {
117119
val request = AuthenticateRequest(email = "[email protected]", password = validPassword)
118120

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

121123
val result = authenticate(request)
122124

@@ -129,7 +131,7 @@ class AuthenticateTest :
129131
test("should return InvalidCredentials when repository returns None") {
130132
val request = AuthenticateRequest(email = validEmail, password = validPassword)
131133

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

134136
val result = authenticate(request)
135137

@@ -142,16 +144,16 @@ class AuthenticateTest :
142144
test("should return InvalidCredentials when password does not match") {
143145
val request = AuthenticateRequest(email = validEmail, password = "WrongPassword123")
144146

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

148150
val result = authenticate(request)
149151

150152
val error = result.shouldBeLeft()
151153
error shouldBe DomainError.InvalidCredentials
152154

153155
verify { userRepository.findByEmail(validEmail) }
154-
verify { passwordProvider.matches("WrongPassword123", passwordHash.getOrNull()!!) }
156+
verify { passwordProvider.matches("WrongPassword123", passwordHash) }
155157
}
156158
}
157159

http4k-example/src/test/kotlin/za/co/ee/learning/infrastructure/api/AuthenticateEndpointTest.kt

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,20 @@ class AuthenticateEndpointTest :
3232

3333
val validEmail = "[email protected]"
3434
val validPassword = "SecurePass123"
35-
val passwordHash = Either.Right("hashed_password")
35+
val passwordHash = "hashed_password"
3636
val testUser =
3737
User(
3838
id = UUID.randomUUID(),
3939
email = validEmail,
40-
passwordHash = passwordHash,
40+
passwordHash = Either.Right(passwordHash),
4141
)
42+
val testToken = "jwt.token.here"
43+
val testExpires = 1234567890L
4244
val tokenInfo =
4345
Either.Right(
4446
TokenInfo(
45-
token = "jwt.token.here",
46-
expires = 1234567890L,
47+
token = testToken,
48+
expires = testExpires,
4749
)
4850
)
4951
beforeTest {
@@ -55,18 +57,18 @@ class AuthenticateEndpointTest :
5557
val requestBody = """{"email":"$validEmail","password":"$validPassword"}"""
5658
val request = Request(Method.POST, "/auth/login").body(requestBody)
5759

58-
every { userRepository.findByEmail(validEmail) } returns testUser.right()
59-
every { passwordProvider.matches(validPassword, passwordHash.getOrNull()!!) } returns true
60+
every { userRepository.findByEmail(validEmail) } returns testUser.some().right()
61+
every { passwordProvider.matches(validPassword, passwordHash) } returns true
6062
every { jwtProvider.generate(testUser) } returns tokenInfo
6163

6264
val response = endpoint.handler(request)
6365

6466
response.status shouldBe Status.OK
65-
response.bodyString() shouldContain "\"token\":\"${tokenInfo.getOrNull()!!.token}\""
66-
response.bodyString() shouldContain "\"expires\":${tokenInfo.getOrNull()!!.expires}"
67+
response.bodyString() shouldContain "\"token\":\"$testToken\""
68+
response.bodyString() shouldContain "\"expires\":$testExpires"
6769

6870
verify { userRepository.findByEmail(validEmail) }
69-
verify { passwordProvider.matches(validPassword, passwordHash.getOrNull()!!) }
71+
verify { passwordProvider.matches(validPassword, passwordHash) }
7072
verify { jwtProvider.generate(testUser) }
7173
}
7274
}
@@ -118,7 +120,7 @@ class AuthenticateEndpointTest :
118120
val requestBody = """{"email":"[email protected]","password":"$validPassword"}"""
119121
val request = Request(Method.POST, "/auth/login").body(requestBody)
120122

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

123125
val response = endpoint.handler(request)
124126

@@ -132,16 +134,16 @@ class AuthenticateEndpointTest :
132134
val requestBody = """{"email":"$validEmail","password":"WrongPassword123"}"""
133135
val request = Request(Method.POST, "/auth/login").body(requestBody)
134136

135-
every { userRepository.findByEmail(validEmail) } returns testUser.right()
136-
every { passwordProvider.matches("WrongPassword123", passwordHash.getOrNull()!!) } returns false
137+
every { userRepository.findByEmail(validEmail) } returns testUser.some().right()
138+
every { passwordProvider.matches("WrongPassword123", passwordHash) } returns false
137139

138140
val response = endpoint.handler(request)
139141

140142
response.status shouldBe Status.BAD_REQUEST
141143
response.bodyString() shouldContain "Invalid email or password"
142144

143145
verify { userRepository.findByEmail(validEmail) }
144-
verify { passwordProvider.matches("WrongPassword123", passwordHash.getOrNull()!!) }
146+
verify { passwordProvider.matches("WrongPassword123", passwordHash) }
145147
}
146148
}
147149

@@ -167,8 +169,8 @@ class AuthenticateEndpointTest :
167169
val requestBody = """{"email":"$validEmail","password":"$validPassword"}"""
168170
val request = Request(Method.POST, "/auth/login").body(requestBody)
169171

170-
every { userRepository.findByEmail(validEmail) } returns testUser.right()
171-
every { passwordProvider.matches(validPassword, passwordHash.getOrNull()!!) } returns true
172+
every { userRepository.findByEmail(validEmail) } returns testUser.some().right()
173+
every { passwordProvider.matches(validPassword, passwordHash) } returns true
172174
every { jwtProvider.generate(testUser) } returns tokenInfo
173175

174176
val response = endpoint.handler(request)

http4k-example/src/test/kotlin/za/co/ee/learning/infrastructure/database/InMemoryUserRepositoryTest.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
package za.co.ee.learning.infrastructure.database
22

3-
import io.kotest.assertions.arrow.core.shouldBeLeft
43
import io.kotest.assertions.arrow.core.shouldBeRight
54
import io.kotest.assertions.arrow.core.shouldBeSome
65
import io.kotest.core.spec.style.FunSpec
76
import io.kotest.matchers.collections.shouldHaveAtLeastSize
87
import io.kotest.matchers.shouldBe
9-
import io.kotest.matchers.types.shouldBeSameInstanceAs
10-
import za.co.ee.learning.domain.DomainError
118

129
class InMemoryUserRepositoryTest :
1310
FunSpec({
@@ -27,22 +24,25 @@ class InMemoryUserRepositoryTest :
2724
test("should return Some when user exists") {
2825
val result = repository.findByEmail("[email protected]")
2926

30-
val user = result.shouldBeRight()
27+
val option = result.shouldBeRight()
28+
val user = option.shouldBeSome()
3129
user.email shouldBe "[email protected]"
3230
}
3331

3432
test("should have password hash for existing user") {
3533
val result = repository.findByEmail("[email protected]")
3634

37-
val user = result.shouldBeRight()
38-
user.passwordHash.getOrNull()!!.isNotEmpty() shouldBe true
35+
val option = result.shouldBeRight()
36+
val user = option.shouldBeSome()
37+
val pwdHash = user.passwordHash.shouldBeRight()
38+
pwdHash.isNotEmpty() shouldBe true
3939
}
4040

4141
test("should return None for non-existent user") {
4242
val result = repository.findByEmail("[email protected]")
4343

44-
val err = result.shouldBeLeft()
45-
err shouldBeSameInstanceAs DomainError.InvalidCredentials
44+
val option = result.shouldBeRight()
45+
option.isNone() shouldBe true
4646
}
4747
}
4848

http4k-example/src/test/kotlin/za/co/ee/learning/infrastructure/security/BCryptPasswordProviderTest.kt

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package za.co.ee.learning.infrastructure.security
22

3+
import io.kotest.assertions.arrow.core.shouldBeRight
34
import io.kotest.core.spec.style.FunSpec
45
import io.kotest.matchers.shouldBe
56
import io.kotest.matchers.shouldNotBe
@@ -15,8 +16,9 @@ class BCryptPasswordProviderTest :
1516

1617
val hash = passwordProvider.encode(password)
1718

18-
hash.getOrNull()!! shouldStartWith "\$2a\$"
19-
hash.getOrNull()!!.length shouldBe 60
19+
val hashResult = hash.shouldBeRight()
20+
hashResult shouldStartWith "\$2a\$"
21+
hashResult.length shouldBe 60
2022
}
2123

2224
test("should generate different hashes for the same password") {
@@ -42,54 +44,54 @@ class BCryptPasswordProviderTest :
4244
val password = ""
4345

4446
val hash = passwordProvider.encode(password)
45-
46-
hash.getOrNull()!! shouldStartWith "\$2a\$"
47+
val hashResult = hash.shouldBeRight()
48+
hashResult shouldStartWith "\$2a\$"
4749
}
4850
}
4951

5052
context("matches") {
5153
test("should return true when password matches hash") {
5254
val password = "correctPassword123"
53-
val hash = passwordProvider.encode(password)
55+
val hash = passwordProvider.encode(password).shouldBeRight()
5456

55-
val result = passwordProvider.matches(password, hash.getOrNull()!!)
57+
val result = passwordProvider.matches(password, hash)
5658

5759
result shouldBe true
5860
}
5961

6062
test("should return false when password does not match hash") {
6163
val correctPassword = "correctPassword123"
6264
val wrongPassword = "wrongPassword456"
63-
val hash = passwordProvider.encode(correctPassword)
65+
val hash = passwordProvider.encode(correctPassword).shouldBeRight()
6466

65-
val result = passwordProvider.matches(wrongPassword, hash.getOrNull()!!)
67+
val result = passwordProvider.matches(wrongPassword, hash)
6668

6769
result shouldBe false
6870
}
6971

7072
test("should return false for empty password when hash is for non-empty password") {
7173
val password = "actualPassword"
72-
val hash = passwordProvider.encode(password)
74+
val hash = passwordProvider.encode(password).shouldBeRight()
7375

74-
val result = passwordProvider.matches("", hash.getOrNull()!!)
76+
val result = passwordProvider.matches("", hash)
7577

7678
result shouldBe false
7779
}
7880

7981
test("should be case sensitive") {
8082
val password = "Password123"
81-
val hash = passwordProvider.encode(password)
83+
val hash = passwordProvider.encode(password).shouldBeRight()
8284

83-
val result = passwordProvider.matches("password123", hash.getOrNull()!!)
85+
val result = passwordProvider.matches("password123", hash)
8486

8587
result shouldBe false
8688
}
8789

8890
test("should handle special characters in password") {
8991
val password = "P@ssw0rd!#\$%^&*()"
90-
val hash = passwordProvider.encode(password)
92+
val hash = passwordProvider.encode(password).shouldBeRight()
9193

92-
val result = passwordProvider.matches(password, hash.getOrNull()!!)
94+
val result = passwordProvider.matches(password, hash)
9395

9496
result shouldBe true
9597
}

0 commit comments

Comments
 (0)