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.verifyServer() } 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.verifyServer() } 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 `onLogin shows invalid credentials on 401`() = runTest {
113 coEvery { repository.testConnection() } returns ApiResult.Success("ok")
114 coEvery { repository.verifyServer() } returns ApiResult.Error("Unauthorized", 401)
116 viewModel.onServerUrlChange("https://test.parseable.com")
117 viewModel.onUsernameChange("admin")
118 viewModel.onPasswordChange("wrongpass")
121 val state = viewModel.state.value
122 assertFalse(state.loginSuccess)
123 assertNotNull(state.error)
124 assertTrue(state.error!!.contains("Invalid credentials"))
125 assertFalse(state.error!!.contains("doesn't appear to be Parseable"))
129 fun `onServerUrlChange clears error`() {
130 viewModel.onLogin() // triggers error
131 assertNotNull(viewModel.state.value.serverUrlError)
133 viewModel.onServerUrlChange("test")
134 assertNull(viewModel.state.value.serverUrlError)
135 assertNull(viewModel.state.value.error)
139 fun `onAllowInsecureChange updates state`() {
140 assertFalse(viewModel.state.value.allowInsecure)
141 viewModel.onAllowInsecureChange(true)
142 assertTrue(viewModel.state.value.allowInsecure)
146 fun `does not auto-populate password from saved config`() = runTest {
147 val config = com.parseable.android.data.model.ServerConfig(
148 serverUrl = "https://test.com",
153 coEvery { settingsRepository.serverConfig } returns flowOf(config)
155 viewModel = LoginViewModel(repository, settingsRepository)
157 val state = viewModel.state.value
158 assertEquals("https://test.com", state.serverUrl)
159 assertEquals("admin", state.username)
160 assertEquals("", state.password) // Password should NOT be auto-populated
161 assertTrue(state.hasSavedCredentials)
165 fun `onLogin prepends https when not insecure`() = runTest {
166 coEvery { repository.testConnection() } returns ApiResult.Success("ok")
167 coEvery { repository.verifyServer() } returns ApiResult.Success(AboutInfo())
169 viewModel.onServerUrlChange("test.parseable.com")
170 viewModel.onUsernameChange("admin")
171 viewModel.onPasswordChange("pass")
175 repository.configure(match { it.serverUrl == "https://test.parseable.com" })
180 fun `onLogin prepends http when insecure`() = runTest {
181 coEvery { repository.testConnection() } returns ApiResult.Success("ok")
182 coEvery { repository.verifyServer() } returns ApiResult.Success(AboutInfo())
184 viewModel.onServerUrlChange("test.parseable.com")
185 viewModel.onAllowInsecureChange(true)
186 viewModel.onUsernameChange("admin")
187 viewModel.onPasswordChange("pass")
191 repository.configure(match { it.serverUrl == "http://test.parseable.com" })
196 fun `onLogin handles uppercase URL scheme`() = runTest {
197 coEvery { repository.testConnection() } returns ApiResult.Success("ok")
198 coEvery { repository.verifyServer() } returns ApiResult.Success(AboutInfo())
200 viewModel.onServerUrlChange("HTTPS://test.parseable.com")
201 viewModel.onUsernameChange("admin")
202 viewModel.onPasswordChange("pass")
206 repository.configure(match {
207 it.serverUrl == "HTTPS://test.parseable.com" && it.useTls
213 fun `onLogin handles mixed case URL scheme`() = runTest {
214 coEvery { repository.testConnection() } returns ApiResult.Success("ok")
215 coEvery { repository.verifyServer() } returns ApiResult.Success(AboutInfo())
217 viewModel.onServerUrlChange("Http://test.parseable.com")
218 viewModel.onAllowInsecureChange(true)
219 viewModel.onUsernameChange("admin")
220 viewModel.onPasswordChange("pass")
224 repository.configure(match {
225 it.serverUrl == "Http://test.parseable.com" && !it.useTls
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