From 390dcb8b2d011d1e7395e79c24223b99eea8b6bd Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 8 Mar 2026 22:22:04 +0300 Subject: [PATCH] android: add unit tests for token store and auth login mapping --- android/CHANGELOG.md | 4 + .../token/DataStoreTokenRepositoryTest.kt | 55 ++++++++ .../repository/NetworkAuthRepositoryTest.kt | 119 ++++++++++++++++++ 3 files changed, 178 insertions(+) create mode 100644 android/app/src/test/java/ru/daemonlord/messenger/core/token/DataStoreTokenRepositoryTest.kt create mode 100644 android/app/src/test/java/ru/daemonlord/messenger/data/auth/repository/NetworkAuthRepositoryTest.kt diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 1fab839..1c1f2e1 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -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. diff --git a/android/app/src/test/java/ru/daemonlord/messenger/core/token/DataStoreTokenRepositoryTest.kt b/android/app/src/test/java/ru/daemonlord/messenger/core/token/DataStoreTokenRepositoryTest.kt new file mode 100644 index 0000000..aeb3cd4 --- /dev/null +++ b/android/app/src/test/java/ru/daemonlord/messenger/core/token/DataStoreTokenRepositoryTest.kt @@ -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 { + val file = File.createTempFile("tokens", ".preferences_pb") + file.deleteOnExit() + return PreferenceDataStoreFactory.createWithPath( + produceFile = { file.absolutePath.toPath() } + ) + } +} diff --git a/android/app/src/test/java/ru/daemonlord/messenger/data/auth/repository/NetworkAuthRepositoryTest.kt b/android/app/src/test/java/ru/daemonlord/messenger/data/auth/repository/NetworkAuthRepositoryTest.kt new file mode 100644 index 0000000..05c63e4 --- /dev/null +++ b/android/app/src/test/java/ru/daemonlord/messenger/data/auth/repository/NetworkAuthRepositoryTest.kt @@ -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(null) + + override fun observeTokens(): Flow = 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 + } + } +}