test(settings): ChangePasswordViewModelTest — validation + typed exception mapping
This commit is contained in:
parent
e585856228
commit
cbffaeca9f
|
|
@ -0,0 +1,132 @@
|
||||||
|
package app.closer.ui.settings
|
||||||
|
|
||||||
|
import app.closer.domain.repository.AuthRepository
|
||||||
|
import app.closer.domain.repository.ChangePasswordException
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Covers [ChangePasswordViewModel]: client-side validation and the typed-exception → user-copy mapping
|
||||||
|
* for the reauth-gated password change.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class ChangePasswordViewModelTest {
|
||||||
|
|
||||||
|
private val dispatcher = StandardTestDispatcher()
|
||||||
|
private val authRepository: AuthRepository = mockk()
|
||||||
|
|
||||||
|
@Before fun setUp() = Dispatchers.setMain(dispatcher)
|
||||||
|
@After fun tearDown() = Dispatchers.resetMain()
|
||||||
|
|
||||||
|
private fun vm() = ChangePasswordViewModel(authRepository)
|
||||||
|
|
||||||
|
private fun ChangePasswordViewModel.enter(current: String, new: String, confirm: String) {
|
||||||
|
updateCurrent(current); updateNew(new); updateConfirm(confirm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- validation (runs before any repository call) ----
|
||||||
|
|
||||||
|
@Test fun `blank current password is rejected`() {
|
||||||
|
val vm = vm(); vm.enter("", "newpass1", "newpass1"); vm.submit()
|
||||||
|
assertEquals("Enter your current password.", vm.uiState.value.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `short new password is rejected`() {
|
||||||
|
val vm = vm(); vm.enter("oldpass1", "ab1", "ab1"); vm.submit()
|
||||||
|
assertEquals("New password must be at least 8 characters.", vm.uiState.value.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `new password without letters and numbers is rejected`() {
|
||||||
|
val vm = vm(); vm.enter("oldpass1", "abcdefgh", "abcdefgh"); vm.submit()
|
||||||
|
assertEquals("New password must include both letters and numbers.", vm.uiState.value.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `mismatched confirmation is rejected`() {
|
||||||
|
val vm = vm(); vm.enter("oldpass1", "newpass1", "different1"); vm.submit()
|
||||||
|
assertEquals("New passwords don't match.", vm.uiState.value.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `new password equal to current is rejected`() {
|
||||||
|
val vm = vm(); vm.enter("samepass1", "samepass1", "samepass1"); vm.submit()
|
||||||
|
assertEquals("New password must be different from your current one.", vm.uiState.value.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `invalid input never calls the repository`() {
|
||||||
|
val vm = vm(); vm.enter("", "x", "y"); vm.submit()
|
||||||
|
coVerify(exactly = 0) { authRepository.changePassword(any(), any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- outcome mapping ----
|
||||||
|
|
||||||
|
private fun submitValid(result: Result<Unit>): ChangePasswordViewModel {
|
||||||
|
coEvery { authRepository.changePassword("oldpass1", "newpass1") } returns result
|
||||||
|
val vm = vm(); vm.enter("oldpass1", "newpass1", "newpass1"); vm.submit()
|
||||||
|
dispatcher.scheduler.advanceUntilIdle()
|
||||||
|
return vm
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `success sets done and clears the entered passwords`() = runTest(dispatcher) {
|
||||||
|
val vm = submitValid(Result.success(Unit))
|
||||||
|
val s = vm.uiState.value
|
||||||
|
assertTrue(s.done)
|
||||||
|
assertFalse(s.isLoading)
|
||||||
|
assertEquals("", s.currentPassword)
|
||||||
|
assertEquals("", s.newPassword)
|
||||||
|
assertEquals("", s.confirmPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `wrong current password maps to a clear message`() = runTest(dispatcher) {
|
||||||
|
assertEquals(
|
||||||
|
"Current password is incorrect.",
|
||||||
|
submitValid(Result.failure(ChangePasswordException.WrongCurrentPassword())).uiState.value.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `weak new password maps to guidance`() = runTest(dispatcher) {
|
||||||
|
assertEquals(
|
||||||
|
"That password is too weak — use at least 8 characters with letters and numbers.",
|
||||||
|
submitValid(Result.failure(ChangePasswordException.WeakNewPassword())).uiState.value.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `too many attempts maps to a back-off message`() = runTest(dispatcher) {
|
||||||
|
assertEquals(
|
||||||
|
"Too many attempts. Please wait a bit and try again.",
|
||||||
|
submitValid(Result.failure(ChangePasswordException.TooManyAttempts())).uiState.value.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `reauth required maps to a sign-in-again message`() = runTest(dispatcher) {
|
||||||
|
assertEquals(
|
||||||
|
"For your security, please sign out and back in, then change your password.",
|
||||||
|
submitValid(Result.failure(ChangePasswordException.ReauthRequired())).uiState.value.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `google-only account maps to the Google message`() = runTest(dispatcher) {
|
||||||
|
assertEquals(
|
||||||
|
"This account signs in with Google — there's no password to change.",
|
||||||
|
submitValid(Result.failure(ChangePasswordException.NoPassword())).uiState.value.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `unknown failure falls back to its own message`() = runTest(dispatcher) {
|
||||||
|
assertEquals(
|
||||||
|
"Boom",
|
||||||
|
submitValid(Result.failure(RuntimeException("Boom"))).uiState.value.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue