android: add unit tests for token store and auth login mapping
This commit is contained in:
@@ -20,3 +20,7 @@
|
||||
- Added loading/error states for login and startup session restore.
|
||||
- Added navigation graph: `AuthGraph (login)` to placeholder `Chats` screen after successful auth.
|
||||
- Implemented automatic session restore on app start using stored tokens.
|
||||
|
||||
### Step 4 - Unit tests
|
||||
- Added `DataStoreTokenRepositoryTest` for token save/read and clear behavior.
|
||||
- Added `NetworkAuthRepositoryTest` for login success path and 401 -> `InvalidCredentials` error mapping.
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package ru.daemonlord.messenger.core.token
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okio.Path.Companion.toPath
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DataStoreTokenRepositoryTest {
|
||||
|
||||
@Test
|
||||
fun saveThenReadTokens_returnsSameBundle() = runTest {
|
||||
val repository = DataStoreTokenRepository(createTestDataStore())
|
||||
val expected = TokenBundle(
|
||||
accessToken = "access-1",
|
||||
refreshToken = "refresh-1",
|
||||
savedAtMillis = 1_726_000_000_000L,
|
||||
)
|
||||
|
||||
repository.saveTokens(expected)
|
||||
|
||||
val actual = repository.getTokens()
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clearTokens_removesSavedTokens() = runTest {
|
||||
val repository = DataStoreTokenRepository(createTestDataStore())
|
||||
repository.saveTokens(
|
||||
TokenBundle(
|
||||
accessToken = "access-2",
|
||||
refreshToken = "refresh-2",
|
||||
savedAtMillis = 1_726_000_000_001L,
|
||||
)
|
||||
)
|
||||
|
||||
repository.clearTokens()
|
||||
|
||||
assertNull(repository.getTokens())
|
||||
}
|
||||
|
||||
private fun createTestDataStore(): DataStore<Preferences> {
|
||||
val file = File.createTempFile("tokens", ".preferences_pb")
|
||||
file.deleteOnExit()
|
||||
return PreferenceDataStoreFactory.createWithPath(
|
||||
produceFile = { file.absolutePath.toPath() }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package ru.daemonlord.messenger.data.auth.repository
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import retrofit2.Retrofit
|
||||
import ru.daemonlord.messenger.core.token.TokenBundle
|
||||
import ru.daemonlord.messenger.core.token.TokenRepository
|
||||
import ru.daemonlord.messenger.data.auth.api.AuthApiService
|
||||
import ru.daemonlord.messenger.domain.common.AppError
|
||||
import ru.daemonlord.messenger.domain.common.AppResult
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class NetworkAuthRepositoryTest {
|
||||
|
||||
private lateinit var server: MockWebServer
|
||||
private lateinit var authApiService: AuthApiService
|
||||
private lateinit var tokenRepository: InMemoryTokenRepository
|
||||
private val dispatcher = StandardTestDispatcher()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
server = MockWebServer()
|
||||
server.start()
|
||||
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(server.url("/"))
|
||||
.addConverterFactory(
|
||||
Json { ignoreUnknownKeys = true }.asConverterFactory("application/json".toMediaType())
|
||||
)
|
||||
.build()
|
||||
|
||||
authApiService = retrofit.create(AuthApiService::class.java)
|
||||
tokenRepository = InMemoryTokenRepository()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
server.shutdown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loginSuccess_savesTokensAndReturnsUser() = runTest(dispatcher) {
|
||||
server.enqueue(
|
||||
MockResponse().setResponseCode(200).setBody(
|
||||
"""
|
||||
{"access_token":"new-access","refresh_token":"new-refresh","token_type":"bearer"}
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
server.enqueue(
|
||||
MockResponse().setResponseCode(200).setBody(
|
||||
"""
|
||||
{"id":1,"email":"user@example.com","name":"User","username":"user","avatar_url":null,"email_verified":true}
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
|
||||
val repository = NetworkAuthRepository(
|
||||
authApiService = authApiService,
|
||||
tokenRepository = tokenRepository,
|
||||
ioDispatcher = dispatcher,
|
||||
)
|
||||
|
||||
val result = repository.login(email = "user@example.com", password = "secret")
|
||||
assertTrue(result is AppResult.Success)
|
||||
val success = result as AppResult.Success
|
||||
assertEquals("user@example.com", success.data.email)
|
||||
assertEquals("new-access", tokenRepository.getTokens()?.accessToken)
|
||||
assertEquals("new-refresh", tokenRepository.getTokens()?.refreshToken)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun login401_returnsInvalidCredentials() = runTest(dispatcher) {
|
||||
server.enqueue(
|
||||
MockResponse().setResponseCode(401).setBody("""{"detail":"Invalid credentials"}""")
|
||||
)
|
||||
|
||||
val repository = NetworkAuthRepository(
|
||||
authApiService = authApiService,
|
||||
tokenRepository = tokenRepository,
|
||||
ioDispatcher = dispatcher,
|
||||
)
|
||||
|
||||
val result = repository.login(email = "user@example.com", password = "wrong")
|
||||
assertTrue(result is AppResult.Error)
|
||||
val error = result as AppResult.Error
|
||||
assertEquals(AppError.InvalidCredentials, error.reason)
|
||||
}
|
||||
|
||||
private class InMemoryTokenRepository : TokenRepository {
|
||||
private val state = MutableStateFlow<TokenBundle?>(null)
|
||||
|
||||
override fun observeTokens(): Flow<TokenBundle?> = state.asStateFlow()
|
||||
|
||||
override suspend fun getTokens(): TokenBundle? = state.value
|
||||
|
||||
override suspend fun saveTokens(tokens: TokenBundle) {
|
||||
state.value = tokens
|
||||
}
|
||||
|
||||
override suspend fun clearTokens() {
|
||||
state.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user