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
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
20import soy.iko.opencode.data.model.ServerProfile
21import soy.iko.opencode.di.ProbeResult
22import soy.iko.opencode.ui.testing.FakeAppContainer
24class ServerEditViewModelTest {
25 private val testDispatcher = StandardTestDispatcher()
26 private val testScope = TestScope(testDispatcher)
30 Dispatchers.setMain(testDispatcher)
35 Dispatchers.resetMain()
38 private fun makeVm(container: FakeAppContainer, profileId: String? = null): ServerEditViewModel {
39 val vm = ServerEditViewModel(container, profileId)
40 testScope.testScheduler.advanceUntilIdle()
44 // --- new profile ---
47 fun init_newProfile_loadsEmptyForm() = testScope.runTest {
48 val container = FakeAppContainer()
49 val vm = makeVm(container, profileId = null)
50 assertTrue(vm.state.value.loaded)
51 assertTrue(vm.state.value.isNew)
52 assertEquals("", vm.state.value.baseUrl)
53 assertEquals("", vm.state.value.label)
54 assertFalse(vm.state.value.saving)
55 assertNull(vm.state.value.error)
58 // --- existing profile ---
61 fun init_existingProfile_loadsFormFields() = testScope.runTest {
62 val container = FakeAppContainer()
63 val profile = ServerProfile(
66 baseUrl = "http://localhost:3000",
70 container.fakeProfileStore.setProfiles(listOf(profile))
71 val vm = makeVm(container, profileId = "p1")
72 assertTrue(vm.state.value.loaded)
73 assertFalse(vm.state.value.isNew)
74 assertEquals("p1", vm.state.value.id)
75 assertEquals("My Server", vm.state.value.label)
76 assertEquals("http://localhost:3000", vm.state.value.baseUrl)
77 assertEquals("admin", vm.state.value.username)
78 assertEquals("secret", vm.state.value.password)
82 fun init_nonExistentProfile_showsError() = testScope.runTest {
83 val container = FakeAppContainer()
84 container.fakeProfileStore.setProfiles(emptyList())
85 val vm = makeVm(container, profileId = "nonexistent")
86 assertTrue(vm.state.value.loaded)
87 assertNotNull(vm.state.value.error)
93 fun update_modifiesState() = testScope.runTest {
94 val container = FakeAppContainer()
95 val vm = makeVm(container)
96 vm.update { it.copy(label = "New Label") }
97 assertEquals("New Label", vm.state.value.label)
100 // --- canSave validation ---
103 fun canSave_falseForBlankBaseUrl() = testScope.runTest {
104 val container = FakeAppContainer()
105 val vm = makeVm(container)
106 vm.update { it.copy(baseUrl = "") }
107 assertFalse(vm.state.value.canSave)
111 fun canSave_falseForInvalidUrl() = testScope.runTest {
112 val container = FakeAppContainer()
113 val vm = makeVm(container)
114 vm.update { it.copy(baseUrl = "not a url") }
115 assertFalse(vm.state.value.canSave)
119 fun canSave_trueForValidHttpUrl() = testScope.runTest {
120 val container = FakeAppContainer()
121 val vm = makeVm(container)
122 vm.update { it.copy(baseUrl = "http://localhost:3000") }
123 assertTrue(vm.state.value.canSave)
127 fun canSave_trueForValidHttpsUrl() = testScope.runTest {
128 val container = FakeAppContainer()
129 val vm = makeVm(container)
130 vm.update { it.copy(baseUrl = "https://example.com") }
131 assertTrue(vm.state.value.canSave)
135 fun canSave_falseForFtpUrl() = testScope.runTest {
136 val container = FakeAppContainer()
137 val vm = makeVm(container)
138 vm.update { it.copy(baseUrl = "ftp://example.com") }
139 assertFalse(vm.state.value.canSave)
145 fun save_persistsProfileAndCallsOnDone() = testScope.runTest {
146 val container = FakeAppContainer()
147 val vm = makeVm(container)
148 vm.update { it.copy(baseUrl = "http://localhost:3000", label = "Test") }
149 var doneCalled = false
150 vm.save { doneCalled = true }
151 testScheduler.advanceUntilIdle()
152 assertTrue(doneCalled)
153 assertNotNull(container.fakeProfileStore.savedProfile)
154 assertEquals("http://localhost:3000", container.fakeProfileStore.savedProfile?.baseUrl)
155 assertEquals("Test", container.fakeProfileStore.savedProfile?.label)
159 fun save_invalidUrlDoesNotSave() = testScope.runTest {
160 val container = FakeAppContainer()
161 val vm = makeVm(container)
162 vm.update { it.copy(baseUrl = "invalid") }
163 var doneCalled = false
164 vm.save { doneCalled = true }
165 testScheduler.advanceUntilIdle()
166 assertFalse(doneCalled)
167 assertNull(container.fakeProfileStore.savedProfile)
171 fun save_whileSavingDoesNotSaveAgain() = testScope.runTest {
172 val container = FakeAppContainer()
173 val vm = makeVm(container)
174 vm.update { it.copy(baseUrl = "http://localhost:3000") }
176 vm.save { doneCount++ }
177 // While saving, try to save again
178 vm.save { doneCount++ }
179 testScheduler.advanceUntilIdle()
180 assertEquals(1, doneCount)
184 fun save_generatesIdForNewProfile() = testScope.runTest {
185 val container = FakeAppContainer()
186 val vm = makeVm(container)
187 vm.update { it.copy(baseUrl = "http://localhost:3000") }
189 testScheduler.advanceUntilIdle()
190 val saved = container.fakeProfileStore.savedProfile
191 assertNotNull(saved?.id)
192 assertTrue(saved!!.id.isNotEmpty())
196 fun save_preservesIdForExistingProfile() = testScope.runTest {
197 val container = FakeAppContainer()
198 val profile = ServerProfile(id = "existing-id", label = "Old", baseUrl = "http://old")
199 container.fakeProfileStore.setProfiles(listOf(profile))
200 val vm = makeVm(container, profileId = "existing-id")
201 vm.update { it.copy(baseUrl = "http://new") }
203 testScheduler.advanceUntilIdle()
204 assertEquals("existing-id", container.fakeProfileStore.savedProfile?.id)
208 fun save_trimsFields() = testScope.runTest {
209 val container = FakeAppContainer()
210 val vm = makeVm(container)
211 vm.update { it.copy(baseUrl = " http://localhost:3000 ", label = " Test ", username = " admin ") }
213 testScheduler.advanceUntilIdle()
214 val saved = container.fakeProfileStore.savedProfile!!
215 assertEquals("http://localhost:3000", saved.baseUrl)
216 assertEquals("Test", saved.label)
217 assertEquals("admin", saved.username)
221 fun save_emptyUsernameClearsCredentials() = testScope.runTest {
222 val container = FakeAppContainer()
223 val vm = makeVm(container)
224 vm.update { it.copy(baseUrl = "http://localhost:3000", username = "", password = "") }
226 testScheduler.advanceUntilIdle()
227 val saved = container.fakeProfileStore.savedProfile!!
228 assertNull(saved.username)
229 assertNull(saved.password)
233 fun save_failureSetsErrorAndClearsSaving() = testScope.runTest {
234 val container = FakeAppContainer()
235 container.fakeProfileStore.saveException = RuntimeException("disk full")
236 val vm = makeVm(container)
237 vm.update { it.copy(baseUrl = "http://localhost:3000") }
238 var doneCalled = false
239 vm.save { doneCalled = true }
240 testScheduler.advanceUntilIdle()
241 assertFalse(doneCalled)
242 assertFalse(vm.state.value.saving)
243 // The error must come from the exception, not from a clobbered ServerEditState.
244 assertNotNull(vm.state.value.error)
245 assertTrue(vm.state.value.error!!.contains("disk full"))
249 fun save_failureDoesNotClobberUserEdits() = testScope.runTest {
250 val container = FakeAppContainer()
251 container.fakeProfileStore.saveException = RuntimeException("disk full")
252 val vm = makeVm(container)
253 vm.update { it.copy(baseUrl = "http://localhost:3000", label = "Original") }
255 // Simulate the user editing the label while the save coroutine is in-flight.
256 vm.update { it.copy(label = "Edited") }
257 testScheduler.advanceUntilIdle()
258 // The error path must not overwrite the user's edit.
259 assertEquals("Edited", vm.state.value.label)
262 // --- probe() auth detection ---
265 fun probe_reachableServer_hidesAuthFields() = testScope.runTest {
266 val container = FakeAppContainer()
267 container.probeResult = ProbeResult.Reachable
268 val vm = makeVm(container)
269 vm.update { it.copy(baseUrl = "http://localhost:3000") }
271 testScheduler.advanceUntilIdle()
272 assertFalse(vm.state.value.probing)
273 assertFalse(vm.state.value.authFieldsVisible)
274 assertNull(vm.state.value.error)
275 assertEquals(1, container.probeCalls.size)
279 fun probe_needsAuth_showsAuthFields() = testScope.runTest {
280 val container = FakeAppContainer()
281 container.probeResult = ProbeResult.NeedsAuth
282 val vm = makeVm(container)
283 vm.update { it.copy(baseUrl = "http://localhost:3000") }
285 testScheduler.advanceUntilIdle()
286 assertFalse(vm.state.value.probing)
287 assertTrue(vm.state.value.authFieldsVisible)
288 assertNull(vm.state.value.error)
292 fun probe_unreachable_showsErrorAndHidesAuthFields() = testScope.runTest {
293 val container = FakeAppContainer()
294 container.probeResult = ProbeResult.Unreachable("Could not reach server")
295 val vm = makeVm(container)
296 vm.update { it.copy(baseUrl = "http://localhost:3000") }
298 testScheduler.advanceUntilIdle()
299 assertFalse(vm.state.value.probing)
300 assertFalse(vm.state.value.authFieldsVisible)
301 assertNotNull(vm.state.value.error)
302 assertTrue(vm.state.value.error!!.contains("Could not reach server"))
306 fun probe_invalidUrlDoesNotProbe() = testScope.runTest {
307 val container = FakeAppContainer()
308 container.probeResult = ProbeResult.Reachable
309 val vm = makeVm(container)
310 vm.update { it.copy(baseUrl = "invalid") }
312 testScheduler.advanceUntilIdle()
313 assertEquals(0, container.probeCalls.size)
317 fun probe_whileProbingDoesNotProbeAgain() = testScope.runTest {
318 val container = FakeAppContainer()
319 container.probeResult = ProbeResult.Reachable
320 val vm = makeVm(container)
321 vm.update { it.copy(baseUrl = "http://localhost:3000") }
324 testScheduler.advanceUntilIdle()
325 assertEquals(1, container.probeCalls.size)
329 fun probe_setsProbingFlagDuringCall() = testScope.runTest {
330 val container = FakeAppContainer()
331 container.probeResult = ProbeResult.Reachable
332 val vm = makeVm(container)
333 vm.update { it.copy(baseUrl = "http://localhost:3000") }
335 // Before advancing, probing should be true
336 assertTrue(vm.state.value.probing)
337 testScheduler.advanceUntilIdle()
338 assertFalse(vm.state.value.probing)
342 fun editingBaseUrlAfterProbe_resetsAuthVisibility() = testScope.runTest {
343 val container = FakeAppContainer()
344 container.probeResult = ProbeResult.NeedsAuth
345 val vm = makeVm(container)
346 vm.update { it.copy(baseUrl = "http://localhost:3000") }
348 testScheduler.advanceUntilIdle()
349 assertTrue(vm.state.value.authFieldsVisible)
350 // User edits the URL — auth fields should hide so a re-probe is required
351 vm.update { it.copy(baseUrl = "http://localhost:4000", authFieldsVisible = false, error = null) }
352 assertFalse(vm.state.value.authFieldsVisible)
356 fun init_existingProfileWithAuth_showsAuthFields() = testScope.runTest {
357 val container = FakeAppContainer()
358 val profile = ServerProfile(
361 baseUrl = "http://localhost:3000",
365 container.fakeProfileStore.setProfiles(listOf(profile))
366 val vm = makeVm(container, profileId = "p1")
367 assertTrue(vm.state.value.authFieldsVisible)
371 fun init_existingProfileWithoutAuth_hidesAuthFields() = testScope.runTest {
372 val container = FakeAppContainer()
373 val profile = ServerProfile(
376 baseUrl = "http://localhost:3000",
378 container.fakeProfileStore.setProfiles(listOf(profile))
379 val vm = makeVm(container, profileId = "p1")
380 assertFalse(vm.state.value.authFieldsVisible)
384 fun init_newProfile_hidesAuthFields() = testScope.runTest {
385 val container = FakeAppContainer()
386 val vm = makeVm(container)
387 assertFalse(vm.state.value.authFieldsVisible)
391 fun probe_failure_setsError() = testScope.runTest {
392 val container = FakeAppContainer()
393 container.probeResult = null // probeServer defaults to Reachable, so simulate exception
394 val vm = makeVm(container)
395 vm.update { it.copy(baseUrl = "http://localhost:3000") }
397 testScheduler.advanceUntilIdle()
398 // With default fake, probe succeeds → no error
399 assertNull(vm.state.value.error)
403 fun probe_reachable_setsProbeReachable() = testScope.runTest {
404 val container = FakeAppContainer()
405 container.probeResult = ProbeResult.Reachable
406 val vm = makeVm(container)
407 vm.update { it.copy(baseUrl = "http://localhost:3000") }
409 testScheduler.advanceUntilIdle()
410 assertEquals(true, vm.state.value.probeReachable)
414 fun probe_needsAuth_setsProbeReachableFalse() = testScope.runTest {
415 val container = FakeAppContainer()
416 container.probeResult = ProbeResult.NeedsAuth
417 val vm = makeVm(container)
418 vm.update { it.copy(baseUrl = "http://localhost:3000") }
420 testScheduler.advanceUntilIdle()
421 assertEquals(false, vm.state.value.probeReachable)
425 fun suggestUrlScheme_addsHttpForBareHost() {
426 assertEquals("http://192.168.1.10:4096", suggestUrlScheme("192.168.1.10:4096"))
427 assertEquals("http://localhost:3000", suggestUrlScheme("localhost:3000"))
431 fun suggestUrlScheme_returnsNullForAlreadySchemed() {
432 assertNull(suggestUrlScheme("http://localhost:3000"))
433 assertNull(suggestUrlScheme("https://example.com"))
437 fun suggestUrlScheme_returnsNullForUnparseable() {
438 assertNull(suggestUrlScheme("not a url"))
439 assertNull(suggestUrlScheme(""))
443 fun duplicate_seedsFromSourceProfileAsNew() = testScope.runTest {
444 val container = FakeAppContainer()
445 val source = ServerProfile(
448 baseUrl = "http://localhost:3000",
452 container.fakeProfileStore.setProfiles(listOf(source))
453 val vm = ServerEditViewModel(container, profileId = null, sourceId = "src1")
454 testScope.testScheduler.advanceUntilIdle()
455 assertTrue(vm.state.value.loaded)
456 // A duplicate is a NEW profile (no id), so save() generates a fresh one.
457 assertNull(vm.state.value.id)
458 assertEquals("My Server (copy)", vm.state.value.label)
459 assertEquals("http://localhost:3000", vm.state.value.baseUrl)
460 assertEquals("admin", vm.state.value.username)
461 assertEquals("secret", vm.state.value.password)
462 assertTrue(vm.state.value.authFieldsVisible)
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