git.iko.soy/opencode-android
on e66c7ed help sign in
opencode-android/app/src/test/java/soy/iko/opencode/ui/server/ServerSettingsViewModelTest.kt
287 lines 11 kB history raw
1@file:OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
3package soy.iko.opencode.ui.server
5import kotlinx.coroutines.Dispatchers
6import kotlinx.coroutines.ExperimentalCoroutinesApi
7import kotlinx.coroutines.test.StandardTestDispatcher
8import kotlinx.coroutines.test.TestScope
9import kotlinx.coroutines.test.resetMain
10import kotlinx.coroutines.test.runTest
11import kotlinx.coroutines.test.setMain
12import org.junit.After
13import org.junit.Assert.assertEquals
14import org.junit.Assert.assertFalse
15import org.junit.Assert.assertNotNull
16import org.junit.Assert.assertNull
17import org.junit.Assert.assertTrue
18import org.junit.Before
19import org.junit.Test
20import soy.iko.opencode.data.model.ServerProfile
21import soy.iko.opencode.ui.testing.FakeAppContainer
22import soy.iko.opencode.ui.testing.FakeOpencodeApiClient
23import soy.iko.opencode.ui.testing.FakeOpencodeConnection
24import soy.iko.opencode.ui.testing.FakeEventStreamClient
25import soy.iko.opencode.ui.testing.FakeSessionRepository
27class ServerSettingsViewModelTest {
28 private val testDispatcher = StandardTestDispatcher()
29 private val testScope = TestScope(testDispatcher)
31 @Before
32 fun setUp() {
33 Dispatchers.setMain(testDispatcher)
34 }
36 @After
37 fun tearDown() {
38 Dispatchers.resetMain()
39 }
41 private fun makeVm(container: FakeAppContainer, profileId: String): ServerSettingsViewModel {
42 val vm = ServerSettingsViewModel(container, profileId)
43 testScope.testScheduler.advanceUntilIdle()
44 return vm
45 }
47 private fun profile(
48 id: String = "p1",
49 label: String = "My Server",
50 baseUrl: String = "http://localhost:3000",
51 username: String? = null,
52 password: String? = null,
53 requireHttps: Boolean = false,
54 certPin: String? = null,
55 ): ServerProfile = ServerProfile(id, label, baseUrl, username, password, requireHttps = requireHttps, certPin = certPin)
57 // --- init: load existing profile ---
59 @Test
60 fun init_loadsExistingProfileFields() = testScope.runTest {
61 val container = FakeAppContainer()
62 container.fakeProfileStore.setProfiles(listOf(profile(label = "My Server", baseUrl = "http://localhost:3000")))
63 val vm = makeVm(container, "p1")
64 assertTrue(vm.state.value.loaded)
65 assertEquals("p1", vm.state.value.id)
66 assertEquals("My Server", vm.state.value.label)
67 assertEquals("http://localhost:3000", vm.state.value.baseUrl)
68 assertFalse(vm.state.value.isDirty)
69 }
71 @Test
72 fun init_authedProfile_showsAuthFields() = testScope.runTest {
73 val container = FakeAppContainer()
74 container.fakeProfileStore.setProfiles(listOf(profile(username = "admin", password = "secret")))
75 val vm = makeVm(container, "p1")
76 assertTrue(vm.state.value.authFieldsVisible)
77 assertEquals("admin", vm.state.value.username)
78 assertEquals("secret", vm.state.value.password)
79 }
81 @Test
82 fun init_nonExistentProfile_showsError() = testScope.runTest {
83 val container = FakeAppContainer()
84 container.fakeProfileStore.setProfiles(emptyList())
85 val vm = makeVm(container, "nonexistent")
86 assertTrue(vm.state.value.loaded)
87 assertNotNull(vm.state.value.error)
88 }
90 @Test
91 fun init_securitySettingsLoaded() = testScope.runTest {
92 val container = FakeAppContainer()
93 container.fakeProfileStore.setProfiles(
94 listOf(profile(requireHttps = true, certPin = "sha256/abcdefg=")),
95 )
96 val vm = makeVm(container, "p1")
97 assertTrue(vm.state.value.requireHttps)
98 assertEquals("sha256/abcdefg=", vm.state.value.certPin)
99 }
101 // --- update / dirty detection ---
103 @Test
104 fun update_marksDirty() = testScope.runTest {
105 val container = FakeAppContainer()
106 container.fakeProfileStore.setProfiles(listOf(profile(label = "Old")))
107 val vm = makeVm(container, "p1")
108 assertFalse(vm.state.value.isDirty)
109 vm.update { it.copy(label = "New") }
110 assertTrue(vm.state.value.isDirty)
111 }
113 @Test
114 fun update_clearsError() = testScope.runTest {
115 val container = FakeAppContainer()
116 container.fakeProfileStore.setProfiles(emptyList())
117 val vm = makeVm(container, "nonexistent")
118 assertNotNull(vm.state.value.error)
119 vm.update { it.copy(label = "x") }
120 assertNull(vm.state.value.error)
121 }
123 // --- save() ---
125 @Test
126 fun save_persistsAndCallsOnDone() = testScope.runTest {
127 val container = FakeAppContainer()
128 container.fakeProfileStore.setProfiles(listOf(profile(label = "Old")))
129 val vm = makeVm(container, "p1")
130 vm.update { it.copy(label = "New") }
131 var doneCalled = false
132 vm.save { doneCalled = true }
133 testScheduler.advanceUntilIdle()
134 assertTrue(doneCalled)
135 assertEquals("New", container.fakeProfileStore.savedProfile?.label)
136 }
138 @Test
139 fun save_derivesLabelFromHostnameWhenBlank() = testScope.runTest {
140 val container = FakeAppContainer()
141 container.fakeProfileStore.setProfiles(listOf(profile(label = "Old", baseUrl = "http://localhost:3000")))
142 val vm = makeVm(container, "p1")
143 vm.update { it.copy(label = "") }
144 vm.save { }
145 testScheduler.advanceUntilIdle()
146 assertEquals("localhost:3000", container.fakeProfileStore.savedProfile?.label)
147 }
149 @Test
150 fun save_normalizesBaseUrl() = testScope.runTest {
151 val container = FakeAppContainer()
152 container.fakeProfileStore.setProfiles(listOf(profile(baseUrl = "http://old")))
153 val vm = makeVm(container, "p1")
154 vm.update { it.copy(baseUrl = "localhost:4096") }
155 vm.save { }
156 testScheduler.advanceUntilIdle()
157 assertEquals("https://localhost:4096", container.fakeProfileStore.savedProfile?.baseUrl)
158 }
160 @Test
161 fun save_preservesIdAndLastUsed() = testScope.runTest {
162 val container = FakeAppContainer()
163 val original = profile(id = "p1", baseUrl = "http://old").copy(lastUsed = 12345L)
164 container.fakeProfileStore.setProfiles(listOf(original))
165 val vm = makeVm(container, "p1")
166 vm.update { it.copy(baseUrl = "http://new") }
167 vm.save { }
168 testScheduler.advanceUntilIdle()
169 assertEquals("p1", container.fakeProfileStore.savedProfile?.id)
170 assertEquals(12345L, container.fakeProfileStore.savedProfile?.lastUsed)
171 }
173 @Test
174 fun save_reconnectsWhenActiveProfile() = testScope.runTest {
175 val container = FakeAppContainer()
176 container.fakeProfileStore.setProfiles(listOf(profile(id = "p1")))
177 val vm = makeVm(container, "p1")
178 // Make p1 the active connection.
179 val api = FakeOpencodeApiClient()
180 val events = FakeEventStreamClient()
181 val conn = FakeOpencodeConnection(
182 api, events, FakeSessionRepository(api, events), profile(id = "p1"),
183 )
184 container.setActiveConnection(conn)
185 vm.update { it.copy(label = "Renamed") }
186 vm.save { }
187 testScheduler.advanceUntilIdle()
188 // Saving an active profile reconnects (connect is called again with the saved profile).
189 assertEquals(1, container.connectCalls.size)
190 assertEquals("p1", container.connectCalls[0].id)
191 }
193 @Test
194 fun save_doesNotReconnectWhenNotActive() = testScope.runTest {
195 val container = FakeAppContainer()
196 container.fakeProfileStore.setProfiles(listOf(profile(id = "p1")))
197 val vm = makeVm(container, "p1")
198 // No active connection set.
199 vm.update { it.copy(label = "Renamed") }
200 vm.save { }
201 testScheduler.advanceUntilIdle()
202 assertEquals(0, container.connectCalls.size)
203 assertNotNull(container.fakeProfileStore.savedProfile)
204 }
206 @Test
207 fun save_failureSetsErrorAndClearsSaving() = testScope.runTest {
208 val container = FakeAppContainer()
209 container.fakeProfileStore.setProfiles(listOf(profile()))
210 container.fakeProfileStore.saveException = RuntimeException("disk full")
211 val vm = makeVm(container, "p1")
212 vm.update { it.copy(label = "New") }
213 var doneCalled = false
214 vm.save { doneCalled = true }
215 testScheduler.advanceUntilIdle()
216 assertFalse(doneCalled)
217 assertFalse(vm.state.value.saving)
218 assertNotNull(vm.state.value.error)
219 assertTrue(vm.state.value.error!!.contains("disk full"))
220 }
222 @Test
223 fun save_whileSavingDoesNotSaveAgain() = testScope.runTest {
224 val container = FakeAppContainer()
225 container.fakeProfileStore.setProfiles(listOf(profile()))
226 val vm = makeVm(container, "p1")
227 vm.update { it.copy(label = "New") }
228 var doneCount = 0
229 vm.save { doneCount++ }
230 vm.save { doneCount++ }
231 testScheduler.advanceUntilIdle()
232 assertEquals(1, doneCount)
233 }
235 @Test
236 fun save_invalidUrlDoesNotSave() = testScope.runTest {
237 val container = FakeAppContainer()
238 container.fakeProfileStore.setProfiles(listOf(profile(baseUrl = "http://localhost:3000")))
239 val vm = makeVm(container, "p1")
240 vm.update { it.copy(baseUrl = "not a url") }
241 var doneCalled = false
242 vm.save { doneCalled = true }
243 testScheduler.advanceUntilIdle()
244 assertFalse(doneCalled)
245 // savedProfile stays null because save() never reached profileStore.save().
246 assertNull(container.fakeProfileStore.savedProfile)
247 }
249 // --- delete() ---
251 @Test
252 fun delete_removesProfileAndCallsOnDone() = testScope.runTest {
253 val container = FakeAppContainer()
254 container.fakeProfileStore.setProfiles(listOf(profile(id = "p1")))
255 val vm = makeVm(container, "p1")
256 var doneCalled = false
257 vm.delete { doneCalled = true }
258 testScheduler.advanceUntilIdle()
259 assertTrue(doneCalled)
260 }
262 @Test
263 fun delete_activeProfile_disconnects() = testScope.runTest {
264 val container = FakeAppContainer()
265 container.fakeProfileStore.setProfiles(listOf(profile(id = "p1")))
266 val vm = makeVm(container, "p1")
267 val api = FakeOpencodeApiClient()
268 val events = FakeEventStreamClient()
269 val conn = FakeOpencodeConnection(
270 api, events, FakeSessionRepository(api, events), profile(id = "p1"),
271 )
272 container.setActiveConnection(conn)
273 vm.delete { }
274 testScheduler.advanceUntilIdle()
275 assertEquals(1, container.disconnectCalls)
276 }
278 @Test
279 fun delete_nonActiveProfile_doesNotDisconnect() = testScope.runTest {
280 val container = FakeAppContainer()
281 container.fakeProfileStore.setProfiles(listOf(profile(id = "p1")))
282 val vm = makeVm(container, "p1")
283 vm.delete { }
284 testScheduler.advanceUntilIdle()
285 assertEquals(0, container.disconnectCalls)
286 }