android: add unit tests for token store and auth login mapping

This commit is contained in:
Codex
2026-03-08 22:22:04 +03:00
parent 54b0d4eb8c
commit 390dcb8b2d
3 changed files with 178 additions and 0 deletions

View File

@@ -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.

View File

@@ -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() }
)
}
}

View File

@@ -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
}
}
}