Skip to content

Commit a61c0a7

Browse files
committed
feat: add unit tests for repositories and use cases
1 parent 4a11732 commit a61c0a7

11 files changed

Lines changed: 468 additions & 3 deletions

File tree

app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainScreen.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ import androidx.compose.material3.Scaffold
1919
import androidx.compose.material3.SnackbarDuration
2020
import androidx.compose.material3.SnackbarHost
2121
import androidx.compose.material3.SnackbarHostState
22+
import androidx.compose.material3.TopAppBarDefaults
23+
import androidx.compose.material3.rememberTopAppBarState
2224
import androidx.compose.runtime.Composable
2325
import androidx.compose.runtime.LaunchedEffect
2426
import androidx.compose.runtime.getValue
2527
import androidx.compose.runtime.remember
2628
import androidx.compose.ui.Modifier
29+
import androidx.compose.ui.input.nestedscroll.nestedScroll
2730
import androidx.compose.ui.tooling.preview.PreviewDynamicColors
2831
import androidx.compose.ui.tooling.preview.PreviewLightDark
2932
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -60,6 +63,7 @@ fun MainScreen(
6063
MainScreenContent(state, navController, snackbarHostState, startDestination)
6164
}
6265

66+
@OptIn(ExperimentalMaterial3Api::class)
6367
@Composable
6468
private fun MainScreenContent(
6569
state: MainViewState,
@@ -69,8 +73,14 @@ private fun MainScreenContent(
6973
) {
7074
val onBackPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
7175
val isLandscape = isLandscape()
76+
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
7277

7378
Scaffold(
79+
modifier = if (state.useTopBarScrollBehavior) {
80+
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
81+
} else {
82+
Modifier
83+
},
7484
topBar = {
7585
AnimatedVisibility(
7686
visible = state.showTopBar,

app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ data class MainViewState(
77
val topBarTitle: String = "Sysctl GUI",
88
val showTopBar: Boolean = true,
99
val showNavBar: Boolean = true,
10+
val useTopBarScrollBehavior: Boolean = true,
1011
val showBackButton: Boolean = false,
1112
val showSearchAction: Boolean = true
1213
)

data/src/main/java/com/androidvip/sysctlgui/data/utils/RootUtils.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ class RootUtils(private val shellDispatcher: CoroutineDispatcher = Dispatchers.D
1717
}
1818

1919
suspend fun isRootAvailable(): Boolean = withContext(shellDispatcher) {
20-
//Shell.isAppGrantedRoot() == true
21-
true
20+
Shell.isAppGrantedRoot() == true
2221
}
2322

2423
suspend fun isBusyboxAvailable(): Boolean = withContext(shellDispatcher) {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.androidvip.sysctlgui.data.repository
2+
3+
import android.util.Log
4+
import com.androidvip.sysctlgui.data.utils.RootUtils
5+
import io.mockk.coEvery
6+
import io.mockk.every
7+
import io.mockk.mockk
8+
import io.mockk.mockkStatic
9+
import kotlinx.coroutines.flow.first
10+
import kotlinx.coroutines.flow.flowOf
11+
import kotlinx.coroutines.test.StandardTestDispatcher
12+
import kotlinx.coroutines.test.runTest
13+
import org.junit.Assert.assertEquals
14+
import org.junit.Assert.assertNotNull
15+
import org.junit.Assert.assertNull
16+
import org.junit.Before
17+
import org.junit.Test
18+
19+
class ParamsRepositoryImplTest {
20+
21+
private lateinit var repository: ParamsRepositoryImpl
22+
private val rootUtils: RootUtils = mockk()
23+
private val testDispatcher = StandardTestDispatcher()
24+
25+
@Before
26+
fun setUp() {
27+
repository = ParamsRepositoryImpl(rootUtils, testDispatcher)
28+
mockkStatic(Log::class)
29+
every { Log.e(any(), any(), any()) } returns 0
30+
}
31+
32+
@Test
33+
fun `given valid sysctl output, when getRuntimeParams is called, then return kernel params`() = runTest(testDispatcher) {
34+
// Given
35+
val commandOutput = flowOf("net.ipv4.ip_forward = 1", "vm.swappiness = 60")
36+
coEvery { rootUtils.executeCommandAndStreamOutput(any()) } returns commandOutput
37+
38+
// When
39+
val params = repository.getRuntimeParams(useBusybox = false, userParams = emptyList()).first()
40+
41+
// Then
42+
assertEquals(2, params.size)
43+
assertEquals("net.ipv4.ip_forward", params[0].name)
44+
assertEquals("1", params[0].value)
45+
assertEquals("vm.swappiness", params[1].name)
46+
assertEquals("60", params[1].value)
47+
}
48+
49+
@Test
50+
fun `given valid sysctl output, when getRuntimeParam is called, then return kernel param`() = runTest(testDispatcher) {
51+
// Given
52+
val paramName = "net.ipv4.ip_forward"
53+
val commandOutput = flowOf("1")
54+
coEvery { rootUtils.executeCommandAndStreamOutput(any()) } returns commandOutput
55+
56+
// When
57+
val param = repository.getRuntimeParam(paramName, useBusybox = false)
58+
59+
// Then
60+
assertNotNull(param)
61+
assertEquals(paramName, param?.name)
62+
assertEquals("1", param?.value)
63+
}
64+
65+
@Test
66+
fun `given empty sysctl output, when getRuntimeParam is called, then return null`() = runTest(testDispatcher) {
67+
// Given
68+
val paramName = "net.ipv4.ip_forward"
69+
coEvery { rootUtils.executeCommandAndStreamOutput(any()) } returns flowOf()
70+
71+
// When
72+
val param = repository.getRuntimeParam(paramName, useBusybox = false)
73+
74+
// Then
75+
assertNull(param)
76+
}
77+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package com.androidvip.sysctlgui.data.repository
2+
3+
import com.androidvip.sysctlgui.domain.exceptions.EmptyFileException
4+
import com.androidvip.sysctlgui.domain.exceptions.MalformedLineException
5+
import kotlinx.coroutines.ExperimentalCoroutinesApi
6+
import kotlinx.coroutines.test.StandardTestDispatcher
7+
import kotlinx.coroutines.test.runTest
8+
import org.intellij.lang.annotations.Language
9+
import org.junit.Assert.assertEquals
10+
import org.junit.Before
11+
import org.junit.Test
12+
import java.io.ByteArrayInputStream
13+
14+
@OptIn(ExperimentalCoroutinesApi::class)
15+
class PresetRepositoryImplTest {
16+
17+
private lateinit var repository: PresetRepositoryImpl
18+
private val testDispatcher = StandardTestDispatcher()
19+
20+
@Before
21+
fun setUp() {
22+
repository = PresetRepositoryImpl(testDispatcher)
23+
}
24+
25+
@Test
26+
fun `given valid preset stream, when readPreset is called, then return kernel params`() = runTest(testDispatcher) {
27+
// Given
28+
val presetContent = "net.ipv4.ip_forward=1\nvm.swappiness=60"
29+
val inputStream = ByteArrayInputStream(presetContent.toByteArray())
30+
31+
// When
32+
val params = repository.readPreset(inputStream)
33+
34+
// Then
35+
assertEquals(2, params.size)
36+
assertEquals("net.ipv4.ip_forward", params[0].name)
37+
assertEquals("1", params[0].value)
38+
assertEquals("vm.swappiness", params[1].name)
39+
assertEquals("60", params[1].value)
40+
}
41+
42+
@Test(expected = EmptyFileException::class)
43+
fun `given empty preset stream, when readPreset is called, then throw EmptyFileException`() = runTest(testDispatcher) {
44+
// Given
45+
val inputStream = ByteArrayInputStream(byteArrayOf())
46+
// When
47+
repository.readPreset(inputStream)
48+
}
49+
50+
@Test(expected = MalformedLineException::class)
51+
fun `given preset with malformed line, when readPreset is called, then throw MalformedLineException`() = runTest(testDispatcher) {
52+
// Given
53+
@Language("conf")
54+
val presetContent = """
55+
net.ipv4.ip_forward=1
56+
vm.swappiness
57+
""".trimIndent()
58+
val inputStream = ByteArrayInputStream(presetContent.toByteArray())
59+
60+
// When
61+
repository.readPreset(inputStream)
62+
}
63+
64+
@Test
65+
fun `given preset with comments, when readPreset is called, then ignore comments`() = runTest(testDispatcher) {
66+
// Given
67+
@Language("conf")
68+
val presetContent = """
69+
# This is a comment
70+
net.ipv4.ip_forward=1
71+
; another comment
72+
vm.swappiness=60
73+
""".trimIndent()
74+
val inputStream = ByteArrayInputStream(presetContent.toByteArray())
75+
76+
// When
77+
val params = repository.readPreset(inputStream)
78+
79+
// Then
80+
assertEquals(2, params.size)
81+
assertEquals("net.ipv4.ip_forward", params[0].name)
82+
assertEquals("1", params[0].value)
83+
assertEquals("vm.swappiness", params[1].name)
84+
assertEquals("60", params[1].value)
85+
}
86+
87+
@Test
88+
fun `given preset with empty lines, when readPreset is called, then ignore empty lines`() = runTest(testDispatcher) {
89+
// Given
90+
@Language("conf")
91+
val presetContent = """
92+
93+
net.ipv4.ip_forward=1
94+
95+
vm.swappiness=60
96+
97+
""".trimIndent()
98+
val inputStream = ByteArrayInputStream(presetContent.toByteArray())
99+
100+
// When
101+
val params = repository.readPreset(inputStream)
102+
103+
// Then
104+
assertEquals(2, params.size)
105+
}
106+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
2+
package com.androidvip.sysctlgui.data.repository
3+
4+
import com.androidvip.sysctlgui.data.db.ParamDao
5+
import com.androidvip.sysctlgui.data.models.KernelParamDTO
6+
import io.mockk.coEvery
7+
import io.mockk.coVerify
8+
import io.mockk.mockk
9+
import kotlinx.coroutines.test.StandardTestDispatcher
10+
import kotlinx.coroutines.test.runTest
11+
import org.junit.Before
12+
import org.junit.Test
13+
14+
class UserRepositoryImplTest {
15+
16+
private val paramDao: ParamDao = mockk(relaxed = true)
17+
private val testDispatcher = StandardTestDispatcher()
18+
private lateinit var repository: UserRepositoryImpl
19+
20+
@Before
21+
fun setUp() {
22+
repository = UserRepositoryImpl(paramDao, testDispatcher)
23+
}
24+
25+
@Test
26+
fun `given a new parameter, when upsertUserParam is called, then DAO is called with id 0`() = runTest(testDispatcher) {
27+
// Given
28+
val newParam = KernelParamDTO(name = "net.ipv4.ip_forward", value = "1")
29+
coEvery { paramDao.getParamByName(newParam.name) } returns null
30+
31+
// When
32+
repository.upsertUserParam(newParam)
33+
34+
// Then
35+
val expectedDto = KernelParamDTO.fromKernelParam(newParam).copy(id = 0)
36+
coVerify { paramDao.upsert(expectedDto) }
37+
}
38+
39+
@Test
40+
fun `when removeUserParam is called, then DAO delete is called with converted DTO`() = runTest(testDispatcher) {
41+
// Given
42+
val paramToRemove = KernelParamDTO(id = 1, name = "net.ipv4.ip_forward", value = "1")
43+
44+
// When
45+
repository.removeUserParam(paramToRemove)
46+
47+
// Then
48+
val expectedDto = KernelParamDTO.fromKernelParam(paramToRemove)
49+
coVerify { paramDao.delete(expectedDto) }
50+
}
51+
}

domain/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,6 @@ dependencies {
3131
implementation(libs.koin)
3232

3333
testImplementation(libs.junit)
34+
testImplementation(libs.mockk.android)
35+
testImplementation(libs.kotlinx.coroutines.test)
3436
}

domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ApplyParamUseCase.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package com.androidvip.sysctlgui.domain.usecase
22

3-
import com.androidvip.sysctlgui.domain.models.KernelParam
43
import com.androidvip.sysctlgui.domain.enums.CommitMode
54
import com.androidvip.sysctlgui.domain.exceptions.ApplyValueException
65
import com.androidvip.sysctlgui.domain.exceptions.BlankValueNotAllowedException
76
import com.androidvip.sysctlgui.domain.exceptions.CommitModeException
87
import com.androidvip.sysctlgui.domain.exceptions.ShellCommandException
8+
import com.androidvip.sysctlgui.domain.models.KernelParam
99
import com.androidvip.sysctlgui.domain.repository.AppPrefs
1010
import com.androidvip.sysctlgui.domain.repository.ParamsRepository
1111

@@ -46,6 +46,8 @@ class ApplyParamUseCase(
4646
}
4747
}
4848

49+
} catch (e: CommitModeException) {
50+
throw e
4951
} catch (e: ShellCommandException) {
5052
val message = e.cause?.message.orEmpty()
5153
throwApplyValueException(
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.androidvip.sysctlgui.domain.usecase
2+
3+
import com.androidvip.sysctlgui.domain.exceptions.BlankValueNotAllowedException
4+
import com.androidvip.sysctlgui.domain.models.KernelParam
5+
import com.androidvip.sysctlgui.domain.repository.AppPrefs
6+
import com.androidvip.sysctlgui.domain.repository.UserRepository
7+
import io.mockk.coVerify
8+
import io.mockk.every
9+
import io.mockk.mockk
10+
import kotlinx.coroutines.runBlocking
11+
import kotlinx.coroutines.test.runTest
12+
import org.junit.Assert.assertThrows
13+
import org.junit.Before
14+
import org.junit.Test
15+
16+
class AddUserParamsUseCaseTest {
17+
18+
private val repository: UserRepository = mockk(relaxed = true)
19+
private val appPrefs: AppPrefs = mockk()
20+
private lateinit var useCase: AddUserParamsUseCase
21+
22+
@Before
23+
fun setUp() {
24+
useCase = AddUserParamsUseCase(repository, appPrefs)
25+
}
26+
27+
@Test
28+
fun `given allow blanks is false and params contain blank value, when invoked, then throw exception`() = runTest {
29+
// Given
30+
every { appPrefs.allowBlankValues } returns false
31+
val params = listOf(KernelParam(name = "net.ipv4.ip_forward", value = "", path = ""))
32+
33+
// Then
34+
assertThrows(BlankValueNotAllowedException::class.java) {
35+
// When
36+
runBlocking { useCase(params) }
37+
}
38+
coVerify(exactly = 0) { repository.upsertUserParams(any()) }
39+
}
40+
41+
@Test
42+
fun `given allow blanks is false and params do not contain blank value, when invoked, then upsert params`() = runTest {
43+
// Given
44+
every { appPrefs.allowBlankValues } returns false
45+
val params = listOf(KernelParam(name = "net.ipv4.ip_forward", value = "1", path = ""))
46+
47+
// When
48+
useCase(params)
49+
50+
// Then
51+
coVerify(exactly = 1) { repository.upsertUserParams(params) }
52+
}
53+
54+
@Test
55+
fun `given allow blanks is true and params contain blank value, when invoked, then upsert params`() = runTest {
56+
// Given
57+
every { appPrefs.allowBlankValues } returns true
58+
val params = listOf(KernelParam(name = "net.ipv4.ip_forward", value = "", path = ""))
59+
60+
// When
61+
useCase(params)
62+
63+
// Then
64+
coVerify(exactly = 1) { repository.upsertUserParams(params) }
65+
}
66+
}

0 commit comments

Comments
 (0)