Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ android {
compose = true
}

testOptions {
unitTests.isReturnDefaultValues = true
}

lint {
disable += setOf("ProtectedPermissions")
abortOnError = false // Optional: prevents build failures due to other lint issues
Expand All @@ -59,6 +63,8 @@ dependencies {
implementation(libs.androidx.security.crypto)
implementation(libs.androidx.appcompat)
testImplementation(libs.junit)
testImplementation(libs.robolectric)
testImplementation(libs.kotlin.test)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
Expand Down
17 changes: 0 additions & 17 deletions app/src/test/java/xyz/block/gosling/ExampleUnitTest.kt

This file was deleted.

100 changes: 100 additions & 0 deletions app/src/test/java/xyz/block/gosling/FakeAndroidKeyStore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package xyz.block.gosling

import java.io.InputStream
import java.io.OutputStream
import java.security.Key
import java.security.KeyStore
import java.security.KeyStoreSpi
import java.security.Provider
import java.security.SecureRandom
import java.security.Security
import java.security.cert.Certificate
import java.security.spec.AlgorithmParameterSpec
import java.util.Date
import java.util.Enumeration
import javax.crypto.KeyGenerator
import javax.crypto.KeyGeneratorSpi
import javax.crypto.SecretKey

/**
* The fake implementation of Android's KeyStore for Robolectric.
*
* See https://stackoverflow.com/questions/38213748/using-the-android-keystore-in-robolectric-tests.
*/
internal object FakeAndroidKeyStore {
private const val PROVIDER_NAME = "AndroidKeyStore"

/**
* Initialize fake AndroidKeyStore for local testing.
*/
fun setUp() {
Security.addProvider(object : Provider(PROVIDER_NAME, 1.0, "") {
init {
put("KeyStore.AndroidKeyStore", FakeKeyStore::class.java.name)
put("KeyGenerator.AES", FakeAesKeyGenerator::class.java.name)
}
})
}

/**
* Remove fake AndroidKeyStore to clean up environment.
*/
fun tearDown() {
Security.removeProvider(PROVIDER_NAME)
}

class FakeKeyStore : KeyStoreSpi() {
private val wrapped = KeyStore.getInstance(KeyStore.getDefaultType())

override fun engineIsKeyEntry(alias: String?): Boolean =
wrapped.isKeyEntry(alias)
override fun engineIsCertificateEntry(alias: String?): Boolean =
wrapped.isCertificateEntry(alias)
override fun engineGetCertificate(alias: String?): Certificate =
wrapped.getCertificate(alias)
override fun engineGetCreationDate(alias: String?): Date =
wrapped.getCreationDate(alias)
override fun engineDeleteEntry(alias: String?) =
wrapped.deleteEntry(alias)
override fun engineSetKeyEntry(
alias: String?, key: Key?, password: CharArray?, chain: Array<out Certificate>?) =
wrapped.setKeyEntry(alias, key, password, chain)

override fun engineSetKeyEntry(
alias: String?, key: ByteArray?, chain: Array<out Certificate>?) =
wrapped.setKeyEntry(alias, key, chain)
override fun engineStore(stream: OutputStream?, password: CharArray?) =
wrapped.store(stream, password)
override fun engineSize(): Int = wrapped.size()
override fun engineAliases(): Enumeration<String> = wrapped.aliases()
override fun engineContainsAlias(alias: String?): Boolean =
wrapped.containsAlias(alias)
override fun engineLoad(stream: InputStream?, password: CharArray?) =
wrapped.load(stream, password)
override fun engineGetCertificateChain(alias: String?): Array<Certificate> =
wrapped.getCertificateChain(alias)
override fun engineSetCertificateEntry(alias: String?, cert: Certificate?) =
wrapped.setCertificateEntry(alias, cert)
override fun engineGetCertificateAlias(cert: Certificate?): String =
wrapped.getCertificateAlias(cert)
override fun engineGetKey(alias: String?, password: CharArray?): Key {
var key: Key? = wrapped.getKey(alias, password)
if (key == null) {
// If key is null, generate one for it to make this fake method work.
engineSetKeyEntry(alias, KeyGenerator.getInstance("AES")
.generateKey(), password, null)
key = wrapped.getKey(alias, password)
}
return key!!
}
}

class FakeAesKeyGenerator : KeyGeneratorSpi() {
private val wrapped = KeyGenerator.getInstance("AES")

override fun engineInit(random: SecureRandom?) = Unit
override fun engineInit(params: AlgorithmParameterSpec?, random: SecureRandom?) = Unit
override fun engineInit(keysize: Int, random: SecureRandom?) = Unit
override fun engineGenerateKey(): SecretKey = wrapped.generateKey()
}
}
104 changes: 104 additions & 0 deletions app/src/test/java/xyz/block/gosling/GoslingApplicationTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package xyz.block.gosling

import org.junit.After
import org.junit.Before
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import xyz.block.gosling.features.app.MainActivity
import xyz.block.gosling.features.launcher.LauncherActivity
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [35])
class GoslingApplicationTest {
@Before
fun setUp() {
FakeAndroidKeyStore.setUp()
}

@After
fun tearDown() {
FakeAndroidKeyStore.tearDown()
}

@Test
fun overlay_withoutAnyActivityRunning_shouldShow() {
assertFalse { GoslingApplication.shouldHideOverlay() }
}

@Test
fun overlay_withMainActivityRunning_shouldHide() {
Robolectric.buildActivity(MainActivity::class.java).use {
it.create().start().resume()
assertTrue { GoslingApplication.shouldHideOverlay() }
}
}

@Test
fun overlay_withLauncherActivityRunning_shouldHide() {
Robolectric.buildActivity(LauncherActivity::class.java).use {
it.create().start().resume()
assertTrue { GoslingApplication.shouldHideOverlay() }
}
}

@Test
fun runningState_mainActivityResumed_mainActivityRunningIsTrue() {
Robolectric.buildActivity(MainActivity::class.java).use {
it.create().start().resume()
assertTrue { GoslingApplication.isMainActivityRunning }
}
}

@Test
fun runningState_mainActivityNotResumed_mainActivityRunningIsFalse() {
Robolectric.buildActivity(MainActivity::class.java).use {
it.create()
assertFalse { GoslingApplication.isMainActivityRunning }

it.start()
assertFalse { GoslingApplication.isMainActivityRunning }

it.resume().pause()
assertFalse { GoslingApplication.isMainActivityRunning }

it.stop()
assertFalse { GoslingApplication.isMainActivityRunning }

it.destroy()
assertFalse { GoslingApplication.isMainActivityRunning }
}
}

@Test
fun runningState_launcherActivityResumed_launcherActivityRunningIsTrue() {
Robolectric.buildActivity(LauncherActivity::class.java).use {
it.create().start().resume()
assertTrue { GoslingApplication.isLauncherActivityRunning }
}
}

@Test
fun runningState_launcherActivityNotResumed_launcherActivityIsFalse() {
Robolectric.buildActivity(LauncherActivity::class.java).use {
it.create()
assertFalse { GoslingApplication.isLauncherActivityRunning }

it.start()
assertFalse { GoslingApplication.isLauncherActivityRunning }

it.resume().pause()
assertFalse { GoslingApplication.isLauncherActivityRunning }

it.stop()
assertFalse { GoslingApplication.isLauncherActivityRunning }

it.destroy()
assertFalse { GoslingApplication.isLauncherActivityRunning }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package xyz.block.gosling.features.launcher

import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.android.controller.ActivityController
import xyz.block.gosling.GoslingApplication
import kotlin.test.assertFalse
import kotlin.test.assertTrue

@RunWith(RobolectricTestRunner::class)
class LauncherActivityTest {
private lateinit var launcherActivity: ActivityController<LauncherActivity>

@Before
fun setUp() {
launcherActivity = Robolectric.buildActivity(LauncherActivity::class.java)
}

@After
fun tearDown() {
launcherActivity.close()
}

@Test
fun lifecycle_runningState_succeed() {
assertFalse { GoslingApplication.isLauncherActivityRunning }

launcherActivity.resume()
assertTrue { GoslingApplication.isLauncherActivityRunning }

launcherActivity.pause()
assertFalse { GoslingApplication.isLauncherActivityRunning }

launcherActivity.resume()
assertTrue { GoslingApplication.isLauncherActivityRunning }

launcherActivity.pause()
launcherActivity.stop()
assertFalse { GoslingApplication.isLauncherActivityRunning }

launcherActivity.destroy()
assertFalse { GoslingApplication.isLauncherActivityRunning }
}


}
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ kotlin = "2.0.0"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
robolectric = "4.16"
espressoCore = "3.6.1"
kotlinxSerializationJson = "1.6.3"
lifecycleRuntimeKtx = "2.8.7"
Expand All @@ -22,6 +23,8 @@ androidx-navigation-compose = { module = "androidx.navigation:navigation-compose
androidx-savedstate = { module = "androidx.savedstate:savedstate", version.ref = "savedstate" }
androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCrypto" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
Expand Down