1package com.parseable.android
3import com.parseable.android.data.model.AboutInfo
4import com.parseable.android.data.model.ApiResult
5import com.parseable.android.data.repository.ParseableRepository
6import com.parseable.android.data.repository.SettingsRepository
7import com.parseable.android.ui.screens.login.LoginViewModel
9import kotlinx.coroutines.Dispatchers
10import kotlinx.coroutines.ExperimentalCoroutinesApi
11import kotlinx.coroutines.flow.flowOf
12import kotlinx.coroutines.test.*
14import org.junit.Assert.*
15import org.junit.Before
18@OptIn(ExperimentalCoroutinesApi::class)
19class LoginViewModelTest {
21 private lateinit var repository: ParseableRepository
22 private lateinit var settingsRepository: SettingsRepository
23 private lateinit var viewModel: LoginViewModel
24 private val testDispatcher = UnconfinedTestDispatcher()
28 Dispatchers.setMain(testDispatcher)
29 repository = mockk(relaxed = true)
30 settingsRepository = mockk(relaxed = true)
31 coEvery { settingsRepository.serverConfig } returns flowOf(null)
32 viewModel = LoginViewModel(repository, settingsRepository)
37 Dispatchers.resetMain()
41 fun `onLogin validates required fields`() {
44 val state = viewModel.state.value
45 assertNotNull(state.serverUrlError)
46 assertNotNull(state.usernameError)
47 assertNotNull(state.passwordError)
48 assertFalse(state.isLoading)
52 fun `onLogin validates URL only when others filled`() {
53 viewModel.onServerUrlChange("")
54 viewModel.onUsernameChange("admin")
55 viewModel.onPasswordChange("password")
58 val state = viewModel.state.value
59 assertNotNull(state.serverUrlError)
60 assertNull(state.usernameError)
61 assertNull(state.passwordError)
65 fun `onLogin succeeds with valid credentials`() = runTest {
66 coEvery { repository.testConnection() } returns ApiResult.Success("ok")
67 coEvery { repository.getAbout() } returns ApiResult.Success(AboutInfo(version = "1.0"))
69 viewModel.onServerUrlChange("https://test.parseable.com")
70 viewModel.onUsernameChange("admin")
71 viewModel.onPasswordChange("password")
74 val state = viewModel.state.value
75 assertTrue(state.loginSuccess)
76 assertFalse(state.isLoading)
77 coVerify { settingsRepository.saveServerConfig(any()) }
81 fun `onLogin fails when connection fails`() = runTest {
82 coEvery { repository.testConnection() } returns ApiResult.Error("Connection refused", 0)
84 viewModel.onServerUrlChange("https://bad.server")
85 viewModel.onUsernameChange("admin")
86 viewModel.onPasswordChange("password")
89 val state = viewModel.state.value
90 assertFalse(state.loginSuccess)
91 assertNotNull(state.error)
92 assertTrue(state.error!!.contains("Connection failed"))
96 fun `onLogin fails when about check fails`() = runTest {
97 coEvery { repository.testConnection() } returns ApiResult.Success("ok")
98 coEvery { repository.getAbout() } returns ApiResult.Error("not parseable", 404)
100 viewModel.onServerUrlChange("https://test.com")
101 viewModel.onUsernameChange("admin")
102 viewModel.onPasswordChange("password")
105 val state = viewModel.state.value
106 assertFalse(state.loginSuccess)
107 assertNotNull(state.error)
108 assertTrue(state.error!!.contains("doesn't appear to be Parseable"))
112 fun `onServerUrlChange clears error`() {
113 viewModel.onLogin() // triggers error
114 assertNotNull(viewModel.state.value.serverUrlError)
116 viewModel.onServerUrlChange("test")
117 assertNull(viewModel.state.value.serverUrlError)
118 assertNull(viewModel.state.value.error)
122 fun `onAllowInsecureChange updates state`() {
123 assertFalse(viewModel.state.value.allowInsecure)
124 viewModel.onAllowInsecureChange(true)
125 assertTrue(viewModel.state.value.allowInsecure)
129 fun `does not auto-populate password from saved config`() = runTest {
130 val config = com.parseable.android.data.model.ServerConfig(
131 serverUrl = "https://test.com",
136 coEvery { settingsRepository.serverConfig } returns flowOf(config)
138 viewModel = LoginViewModel(repository, settingsRepository)
140 val state = viewModel.state.value
141 assertEquals("https://test.com", state.serverUrl)
142 assertEquals("admin", state.username)
143 assertEquals("", state.password) // Password should NOT be auto-populated
144 assertTrue(state.hasSavedCredentials)
148 fun `onLogin prepends https when not insecure`() = runTest {
149 coEvery { repository.testConnection() } returns ApiResult.Success("ok")
150 coEvery { repository.getAbout() } returns ApiResult.Success(AboutInfo())
152 viewModel.onServerUrlChange("test.parseable.com")
153 viewModel.onUsernameChange("admin")
154 viewModel.onPasswordChange("pass")
158 repository.configure(match { it.serverUrl == "https://test.parseable.com" })
163 fun `onLogin prepends http when insecure`() = runTest {
164 coEvery { repository.testConnection() } returns ApiResult.Success("ok")
165 coEvery { repository.getAbout() } returns ApiResult.Success(AboutInfo())
167 viewModel.onServerUrlChange("test.parseable.com")
168 viewModel.onAllowInsecureChange(true)
169 viewModel.onUsernameChange("admin")
170 viewModel.onPasswordChange("pass")
174 repository.configure(match { it.serverUrl == "http://test.parseable.com" })
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list