git.iko.soy/parseable-android
on master help sign in
155 lines 6.1 kB history raw
1package com.parseable.android
3import com.parseable.android.data.local.FavoriteStreamDao
4import com.parseable.android.data.model.*
5import com.parseable.android.data.repository.ParseableRepository
6import com.parseable.android.data.repository.SettingsRepository
7import com.parseable.android.ui.screens.streams.StreamsViewModel
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 StreamsViewModelTest {
21 private lateinit var repository: ParseableRepository
22 private lateinit var settingsRepository: SettingsRepository
23 private lateinit var favoriteDao: FavoriteStreamDao
24 private lateinit var viewModel: StreamsViewModel
25 private val testDispatcher = UnconfinedTestDispatcher()
27 @Before
28 fun setup() {
29 Dispatchers.setMain(testDispatcher)
30 repository = mockk(relaxed = true)
31 settingsRepository = mockk(relaxed = true)
32 favoriteDao = mockk(relaxed = true)
33 every { favoriteDao.getAllNames() } returns flowOf(emptyList())
34 }
36 @After
37 fun tearDown() {
38 Dispatchers.resetMain()
39 }
41 @Test
42 fun `refresh loads streams and about info`() = runTest {
43 val streams = listOf(LogStream("stream1"), LogStream("stream2"))
44 val about = AboutInfo(version = "1.0.0")
45 coEvery { repository.listStreams(forceRefresh = true) } returns ApiResult.Success(streams)
46 coEvery { repository.getAbout() } returns ApiResult.Success(about)
47 coEvery { repository.getStreamStats(any()) } returns ApiResult.Error("skip", 0)
49 viewModel = StreamsViewModel(repository, settingsRepository, favoriteDao)
50 viewModel.refresh()
52 val state = viewModel.state.value
53 assertEquals(2, state.streams.size)
54 assertEquals("stream1", state.streams[0].name)
55 assertEquals("1.0.0", state.aboutInfo?.version)
56 assertFalse(state.isLoading)
57 assertNull(state.error)
58 }
60 @Test
61 fun `refresh sets error when streams fail`() = runTest {
62 coEvery { repository.listStreams(forceRefresh = true) } returns ApiResult.Error("Network error", 0)
63 coEvery { repository.getAbout() } returns ApiResult.Error("fail", 0)
65 viewModel = StreamsViewModel(repository, settingsRepository, favoriteDao)
66 viewModel.refresh()
68 val state = viewModel.state.value
69 assertTrue(state.streams.isEmpty())
70 assertNotNull(state.error)
71 assertFalse(state.isLoading)
72 }
74 @Test
75 fun `refresh loads stats for each stream`() = runTest {
76 val streams = listOf(LogStream("s1"), LogStream("s2"))
77 coEvery { repository.listStreams(forceRefresh = true) } returns ApiResult.Success(streams)
78 coEvery { repository.getAbout() } returns ApiResult.Success(AboutInfo())
79 coEvery { repository.getStreamStats("s1") } returns ApiResult.Success(
80 StreamStats(
81 ingestion = IngestionStats(count = 100, size = "1MB"),
82 storage = StorageStats(size = "500KB"),
83 )
84 )
85 coEvery { repository.getStreamStats("s2") } returns ApiResult.Error("fail", 500)
87 viewModel = StreamsViewModel(repository, settingsRepository, favoriteDao)
88 viewModel.refresh()
90 val state = viewModel.state.value
91 assertEquals(1, state.streamStats.size)
92 assertNotNull(state.streamStats["s1"])
93 assertEquals(100L, state.streamStats["s1"]?.eventCount)
94 assertTrue(state.failedStats.contains("s2"))
95 }
97 @Test
98 fun `logout calls clearConfig`() = runTest {
99 coEvery { repository.listStreams(forceRefresh = true) } returns ApiResult.Success(emptyList())
100 coEvery { repository.getAbout() } returns ApiResult.Success(AboutInfo())
102 viewModel = StreamsViewModel(repository, settingsRepository, favoriteDao)
103 viewModel.logout()
105 coVerify { settingsRepository.clearConfig() }
106 }
108 @Test
109 fun `favorites are loaded from DAO on init`() = runTest {
110 every { favoriteDao.getAllNames() } returns flowOf(listOf("stream1", "stream2"))
112 viewModel = StreamsViewModel(repository, settingsRepository, favoriteDao)
114 val state = viewModel.state.value
115 assertTrue(state.favoriteNames.contains("stream1"))
116 assertTrue(state.favoriteNames.contains("stream2"))
117 }
119 @Test
120 fun `toggleFavorite adds when not favorited`() = runTest {
121 every { favoriteDao.getAllNames() } returns flowOf(emptyList())
123 viewModel = StreamsViewModel(repository, settingsRepository, favoriteDao)
124 viewModel.toggleFavorite("new_stream")
126 coVerify { favoriteDao.insert(match { it.streamName == "new_stream" }) }
127 }
129 @Test
130 fun `refresh resets isLoading on unexpected exception`() = runTest {
131 coEvery { repository.getAbout() } throws RuntimeException("boom")
132 coEvery { repository.listStreams(forceRefresh = true) } returns ApiResult.Success(emptyList())
134 viewModel = StreamsViewModel(repository, settingsRepository, favoriteDao)
135 viewModel.refresh()
137 val state = viewModel.state.value
138 assertFalse(state.isLoading)
139 assertNotNull(state.error)
140 assertTrue(state.error!!.contains("boom"))
141 }
143 @Test
144 fun `toggleFavorite removes when already favorited`() = runTest {
145 every { favoriteDao.getAllNames() } returns flowOf(listOf("existing"))
146 // toggleFavorite decides insert-vs-delete from the committed DB state (isFavoriteNow),
147 // not the async-mirrored favoriteNames flow, so stub that as the source of truth.
148 coEvery { favoriteDao.isFavoriteNow("existing") } returns true
150 viewModel = StreamsViewModel(repository, settingsRepository, favoriteDao)
151 viewModel.toggleFavorite("existing")
153 coVerify { favoriteDao.deleteByName("existing") }
154 }