git.iko.soy/parseable-android
on icon help sign in
229 lines 8.1 kB history raw
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
8import io.mockk.*
9import kotlinx.coroutines.Dispatchers
10import kotlinx.coroutines.ExperimentalCoroutinesApi
11import kotlinx.coroutines.flow.flowOf
12import kotlinx.coroutines.test.*
13import org.junit.After
14import org.junit.Assert.*
15import org.junit.Before
16import org.junit.Test
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()
26 @Before
27 fun setup() {
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)
33 }
35 @After
36 fun tearDown() {
37 Dispatchers.resetMain()
38 }
40 @Test
41 fun `onLogin validates required fields`() {
42 viewModel.onLogin()
44 val state = viewModel.state.value
45 assertNotNull(state.serverUrlError)
46 assertNotNull(state.usernameError)
47 assertNotNull(state.passwordError)
48 assertFalse(state.isLoading)
49 }
51 @Test
52 fun `onLogin validates URL only when others filled`() {
53 viewModel.onServerUrlChange("")
54 viewModel.onUsernameChange("admin")
55 viewModel.onPasswordChange("password")
56 viewModel.onLogin()
58 val state = viewModel.state.value
59 assertNotNull(state.serverUrlError)
60 assertNull(state.usernameError)
61 assertNull(state.passwordError)
62 }
64 @Test
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")
72 viewModel.onLogin()
74 val state = viewModel.state.value
75 assertTrue(state.loginSuccess)
76 assertFalse(state.isLoading)
77 coVerify { settingsRepository.saveServerConfig(any()) }
78 }
80 @Test
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")
87 viewModel.onLogin()
89 val state = viewModel.state.value
90 assertFalse(state.loginSuccess)
91 assertNotNull(state.error)
92 assertTrue(state.error!!.contains("Connection failed"))
93 }
95 @Test
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")
103 viewModel.onLogin()
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"))
109 }
111 @Test
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")
119 viewModel.onLogin()
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"))
126 }
128 @Test
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)
136 }
138 @Test
139 fun `onAllowInsecureChange updates state`() {
140 assertFalse(viewModel.state.value.allowInsecure)
141 viewModel.onAllowInsecureChange(true)
142 assertTrue(viewModel.state.value.allowInsecure)
143 }
145 @Test
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",
149 username = "admin",
150 password = "secret",
151 useTls = true,
152 )
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)
162 }
164 @Test
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")
172 viewModel.onLogin()
174 coVerify {
175 repository.configure(match { it.serverUrl == "https://test.parseable.com" })
176 }
177 }
179 @Test
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")
188 viewModel.onLogin()
190 coVerify {
191 repository.configure(match { it.serverUrl == "http://test.parseable.com" })
192 }
193 }
195 @Test
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")
203 viewModel.onLogin()
205 coVerify {
206 repository.configure(match {
207 it.serverUrl == "HTTPS://test.parseable.com" && it.useTls
208 })
209 }
210 }
212 @Test
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")
221 viewModel.onLogin()
223 coVerify {
224 repository.configure(match {
225 it.serverUrl == "Http://test.parseable.com" && !it.useTls
226 })
227 }
228 }