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