11package com.adsearch.domain.service
22
33import com.adsearch.domain.enums.TokenTypeEnum
4- import com.adsearch.domain.enums.UserRoleEnum
54import com.adsearch.domain.exception.InvalidCredentialsException
65import com.adsearch.domain.exception.InvalidTokenException
76import com.adsearch.domain.exception.TokenExpiredException
@@ -17,146 +16,166 @@ import com.adsearch.domain.port.out.persistence.UserPersistencePort
1716import io.mockk.every
1817import io.mockk.mockk
1918import io.mockk.verify
20- import io.mockk.verifyOrder
2119import org.assertj.core.api.Assertions.assertThat
2220import org.assertj.core.api.Assertions.assertThatThrownBy
21+ import org.junit.jupiter.api.BeforeEach
22+ import org.junit.jupiter.api.Nested
2323import org.junit.jupiter.api.Test
24+ import org.junit.jupiter.params.ParameterizedTest
25+ import org.junit.jupiter.params.provider.ValueSource
2426import java.time.Instant
2527
2628class AuthenticationServiceTest {
2729
28- private val authenticationProvider = mockk<AuthenticationProviderPort >(relaxed = true )
29- private val tokenGenerator = mockk<TokenGeneratorPort >(relaxed = true )
30- private val configurationProvider = mockk<ConfigurationProviderPort >(relaxed = true )
30+ private val authenticationProvider = mockk<AuthenticationProviderPort >()
31+ private val tokenGenerator = mockk<TokenGeneratorPort >()
32+ private val configurationProvider = mockk<ConfigurationProviderPort >()
3133 private val tokenPersistence = mockk<TokenPersistencePort >(relaxed = true )
32- private val userPersistence = mockk<UserPersistencePort >(relaxed = true )
33-
34- private val service = AuthenticationService (
35- authenticationProvider,
36- tokenGenerator,
37- configurationProvider,
38- tokenPersistence,
39- userPersistence
40- )
41-
42- @Test
43- fun `login should return tokens when credentials valid` () {
44- // Given
45- val username = " john"
46- val pwd = " pwd"
47- val user = User (10 , username, " john@mail.com" , " hpass" , setOf (UserRoleEnum .ROLE_USER .type), true )
48- every { authenticationProvider.authenticate(username, pwd) } returns username
49- every { userPersistence.findByUsername(username) } returns user
50- every { configurationProvider.getRefreshTokenExpiration() } returns 3600L
51- every { tokenGenerator.generateAccessToken(user) } returns " access-token"
52-
53- // When
54- val resp: LoginUserUseCase .LoginUser = service.login(LoginUserUseCase .LoginUserCommand (username, pwd))
55-
56- // Then
57- assertThat(resp.accessToken).isEqualTo(" access-token" )
58- assertThat(resp.refreshToken).isNotBlank()
59-
60- verifyOrder {
61- authenticationProvider.authenticate(" john" , " pwd" )
62- userPersistence.findByUsername(" john" )
63- tokenPersistence.save(withArg { rt: RefreshToken ->
64- assertThat(rt.userId).isEqualTo(user.id)
65- assertThat(rt.token).isNotBlank()
66- assertThat(rt.expiryDate).isAfterOrEqualTo(Instant .now())
67- })
68- tokenGenerator.generateAccessToken(user)
69- }
34+ private val userPersistence = mockk<UserPersistencePort >()
35+
36+ private lateinit var authenticationService: AuthenticationService
37+
38+ @BeforeEach
39+ fun setUp () {
40+ authenticationService = AuthenticationService (
41+ authenticationProvider,
42+ tokenGenerator,
43+ configurationProvider,
44+ tokenPersistence,
45+ userPersistence
46+ )
7047 }
7148
72- @Test
73- fun `login should throw InvalidCredentialsException when authentication fails` () {
74- val username = " john"
75- val pwd = " bad"
76- every { authenticationProvider.authenticate(username, pwd) } throws RuntimeException (" bad creds" )
49+ @Nested
50+ inner class LoginTests {
51+ @Test
52+ fun `login should return tokens and save refresh token when credentials are valid` () {
53+ // given
54+ val user = User (1 , " user" , " user@example.com" , " hashed" , emptySet(), true )
55+
56+ every { authenticationProvider.authenticate(" user" , " password" ) } returns " user"
57+ every { userPersistence.findByUsername(" user" ) } returns user
58+ every { configurationProvider.getRefreshTokenExpiration() } returns 3600L
59+ every { tokenGenerator.generateAccessToken(user) } returns " access-token"
60+ every { tokenPersistence.save(any()) } returns Unit
61+
62+ // when
63+ val result = authenticationService.login(LoginUserUseCase .LoginUserCommand (" user" , " password" ))
64+
65+ // then
66+ assertThat(result.accessToken).isEqualTo(" access-token" )
67+ assertThat(result.refreshToken).isNotEmpty()
68+
69+ verify {
70+ tokenPersistence.save(withArg { refreshToken ->
71+ assertThat(refreshToken.userId).isEqualTo(1L )
72+ assertThat(refreshToken.expiryDate).isAfter(Instant .now())
73+ assertThat(refreshToken.revoked).isFalse()
74+ })
75+ }
76+ }
7777
78- assertThatThrownBy { service.login( LoginUserUseCase . LoginUserCommand (username, pwd)) }
79- .isInstanceOf( InvalidCredentialsException :: class .java)
80- .hasMessageContaining( " Authentication failed for user $username " )
81- }
78+ @Test
79+ fun `login should throw InvalidCredentialsException when authentication fails` () {
80+ // given
81+ every { authenticationProvider.authenticate(any(), any()) } throws RuntimeException ( " Auth failed " )
8282
83- @Test
84- fun `logout should delete token when provided` () {
85- val token = " rt-token"
86- service.logout(token)
87- verify {
88- tokenPersistence.deleteTokenAndType(
89- " dcf752bf51d5062c0f24312ec8002c370371def660cb3a3406c2eba9cc30da0affd44d42c6e7d8a541a4940174cb19113c4e17d8e042b533d701e443fdcff360" ,
90- TokenTypeEnum .REFRESH
91- )
83+ // when / then
84+ assertThatThrownBy { authenticationService.login(LoginUserUseCase .LoginUserCommand (" user" , " wrong-password" )) }
85+ .isInstanceOf(InvalidCredentialsException ::class .java)
86+ .hasMessageContaining(" Authentication failed for user user" )
9287 }
9388 }
9489
95- @Test
96- fun `logout should throw InvalidTokenException when token is null` () {
97- assertThatThrownBy { service.logout(null ) }
98- .isInstanceOf(InvalidTokenException ::class .java)
99- .hasMessageContaining(" Logout attempted without refresh token" )
100- }
90+ @Nested
91+ inner class LogoutTests {
92+ @Test
93+ fun `logout should delete refresh token when token is provided` () {
94+ // given
95+
96+ // when
97+ authenticationService.logout(" refresh-token" )
10198
102- @Test
103- fun `refreshAccessToken should throw InvalidTokenException when token missing or invalid` () {
104- assertThatThrownBy { service.refreshAccessToken(null ) }
105- .isInstanceOf(InvalidTokenException ::class .java)
99+ // then
100+ verify { tokenPersistence.deleteTokenAndType(any(), TokenTypeEnum .REFRESH ) }
101+ }
106102
107- every { tokenPersistence.findByTokenAndType(any(), TokenTypeEnum .REFRESH ) } returns null
108- assertThatThrownBy { service.refreshAccessToken(" missing" ) }
109- .isInstanceOf(InvalidTokenException ::class .java)
103+ @Test
104+ fun `logout should throw InvalidTokenException when token is null` () {
105+ // when / then
106+ assertThatThrownBy { authenticationService.logout(null ) }
107+ .isInstanceOf(InvalidTokenException ::class .java)
108+ .hasMessageContaining(" Logout attempted without refresh token" )
109+ }
110110 }
111111
112- @Test
113- fun `refreshAccessToken should throw TokenExpiredException when token expired or revoked` () {
114- val rt = RefreshToken (5 , " t" , Instant .now().minusSeconds(10 ), false )
115- every { tokenPersistence.findByTokenAndType(any(), TokenTypeEnum .REFRESH ) } returns rt
116-
117- assertThatThrownBy { service.refreshAccessToken(" t" ) }
118- .isInstanceOf(TokenExpiredException ::class .java)
119-
120- // revoked case
121- val rt2 = RefreshToken (5 , " t2" , Instant .now().plusSeconds(1000 ), true )
122- every { tokenPersistence.findByTokenAndType(" t2" , TokenTypeEnum .REFRESH ) } returns rt2
123- assertThatThrownBy { service.refreshAccessToken(" t2" ) }
124- .isInstanceOf(TokenExpiredException ::class .java)
125- verify {
126- tokenPersistence.deleteTokenAndType(
127- " 99f97d455d5d62b24f3a942a1abc3fa8863fc0ce2037f52f09bd785b22b800d4f2e7b2b614cb600ffc2a4fe24679845b24886d69bb776fcfa46e54d188889c6f" ,
128- TokenTypeEnum .REFRESH
129- )
112+ @Nested
113+ inner class RefreshTokenTests {
114+ @Test
115+ fun `refreshAccessToken should return new access token when refresh token is valid` () {
116+ // given
117+ val refreshToken = RefreshToken (1L , " hashed-token" , Instant .now().plusSeconds(3600 ), false )
118+ val user = User (1 , " user" , " user@example.com" , " hashed" , emptySet(), true )
119+
120+ every { tokenPersistence.findByTokenAndType(any(), TokenTypeEnum .REFRESH ) } returns refreshToken
121+ every { userPersistence.findById(1L ) } returns user
122+ every { tokenGenerator.generateAccessToken(user) } returns " new-access-token"
123+
124+ // when
125+ val result = authenticationService.refreshAccessToken(" valid-refresh-token" )
126+
127+ // then
128+ assertThat(result.accessToken).isEqualTo(" new-access-token" )
130129 }
131- verify {
132- tokenPersistence.deleteTokenAndType(
133- " dbabd9bd5c26b441bf9cd7c07b82b9974d9a71e1379253b9f644e7554287e2d155eb369e081e7ad2cf1594fdae4f6b0385260376f44f20b01ca0a8c05b32fafc" ,
134- TokenTypeEnum .REFRESH
135- )
130+
131+ @Test
132+ fun `refreshAccessToken should throw InvalidTokenException when token is null` () {
133+ // when / then
134+ assertThatThrownBy { authenticationService.refreshAccessToken(null ) }
135+ .isInstanceOf(InvalidTokenException ::class .java)
136+ .hasMessageContaining(" Token refresh failed - refresh token missing" )
136137 }
137- }
138138
139- @Test
140- fun `refreshAccessToken should throw UserNotFoundException when user not found` () {
141- val rt = RefreshToken (99 , " t3" , Instant .now().plusSeconds(1000 ), false )
142- every {
143- tokenPersistence.findByTokenAndType(any(), TokenTypeEnum .REFRESH )
144- } returns rt
145- every { userPersistence.findById(99 ) } returns null
139+ @Test
140+ fun `refreshAccessToken should throw InvalidTokenException when refresh token is not found` () {
141+ // given
142+ every { tokenPersistence.findByTokenAndType(any(), TokenTypeEnum .REFRESH ) } returns null
146143
147- assertThatThrownBy { service.refreshAccessToken(" t3" ) }
148- .isInstanceOf(UserNotFoundException ::class .java)
149- }
144+ // when / then
145+ assertThatThrownBy { authenticationService.refreshAccessToken(" unknown-token" ) }
146+ .isInstanceOf(InvalidTokenException ::class .java)
147+ .hasMessageContaining(" invalid refresh token provided" )
148+ }
149+
150+ @ParameterizedTest
151+ @ValueSource(strings = [" expired" , " revoked" ])
152+ fun `refreshAccessToken should throw TokenExpiredException when token is expired or revoked` (reason : String ) {
153+ // given
154+ val expiry = if (reason == " expired" ) Instant .now().minusSeconds(60 ) else Instant .now().plusSeconds(3600 )
155+ val revoked = reason == " revoked"
156+ val refreshToken = RefreshToken (1L , " hashed-token" , expiry, revoked)
150157
151- @Test
152- fun `refreshAccessToken should return new access token when valid` () {
153- val user = User (20 , " alice" , " a@a.com" , " p" , setOf (UserRoleEnum .ROLE_USER .type), true )
154- val rt = RefreshToken (user.id, " good" , Instant .now().plusSeconds(1000 ), false )
155- every { tokenPersistence.findByTokenAndType(any(), TokenTypeEnum .REFRESH ) } returns rt
156- every { userPersistence.findById(user.id) } returns user
157- every { tokenGenerator.generateAccessToken(user) } returns " new-access"
158+ every { tokenPersistence.findByTokenAndType(any(), TokenTypeEnum .REFRESH ) } returns refreshToken
158159
159- val resp = service.refreshAccessToken(" good" )
160- assertThat(resp.accessToken).isEqualTo(" new-access" )
160+ // when / then
161+ assertThatThrownBy { authenticationService.refreshAccessToken(" some-token" ) }
162+ .isInstanceOf(TokenExpiredException ::class .java)
163+ .hasMessageContaining(" token expired or revoked" )
164+
165+ verify { tokenPersistence.deleteTokenAndType(any(), TokenTypeEnum .REFRESH ) }
166+ }
167+
168+ @Test
169+ fun `refreshAccessToken should throw UserNotFoundException when user no longer exists` () {
170+ // given
171+ val refreshToken = RefreshToken (1L , " hashed-token" , Instant .now().plusSeconds(3600 ), false )
172+ every { tokenPersistence.findByTokenAndType(any(), TokenTypeEnum .REFRESH ) } returns refreshToken
173+ every { userPersistence.findById(1L ) } returns null
174+
175+ // when / then
176+ assertThatThrownBy { authenticationService.refreshAccessToken(" valid-token" ) }
177+ .isInstanceOf(UserNotFoundException ::class .java)
178+ .hasMessageContaining(" user not found with user id: 1" )
179+ }
161180 }
162181}
0 commit comments