Skip to content

Commit fc4d9db

Browse files
authored
Merge pull request #39 from muhammad7865/feature/testing-implementation
Malware_Testing
2 parents e9bd951 + 72dfcf0 commit fc4d9db

File tree

12 files changed

+385
-24
lines changed

12 files changed

+385
-24
lines changed

app/build.gradle.kts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ android {
5858
buildConfig = true // For API keys
5959
}
6060

61+
testOptions {
62+
unitTests {
63+
isReturnDefaultValues = true
64+
}
65+
}
66+
6167
}
6268

6369
dependencies {
@@ -110,6 +116,10 @@ dependencies {
110116
implementation(libs.androidx.ui.tooling.preview)
111117
implementation(libs.androidx.material3)
112118
testImplementation(libs.junit)
119+
testImplementation(libs.androidx.test.core)
120+
testImplementation(libs.mockk)
121+
testImplementation(libs.kotlinx.coroutines.test)
122+
testImplementation(libs.robolectric)
113123
androidTestImplementation(libs.androidx.junit)
114124
androidTestImplementation(libs.androidx.espresso.core)
115125
androidTestImplementation(platform(libs.androidx.compose.bom))

app/src/main/java/com/droid/cybershield/core/features/OnnxScanner.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class OnnxScanner private constructor(
1313
private val env: OrtEnvironment,
1414
private val session: OrtSession,
1515
private val order: List<String>
16-
) {
16+
) : MalwareScanner {
1717
companion object {
1818
private const val TAG = "OnnxScanner"
1919
@Volatile
@@ -55,7 +55,7 @@ class OnnxScanner private constructor(
5555
}
5656
}
5757

58-
suspend fun predict(payload: Map<String, Any>): Float {
58+
override suspend fun predict(payload: Map<String, Any>): Float {
5959

6060
return try {
6161
val n = order.size
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.droid.cybershield.core.inference
2+
3+
interface MalwareScanner {
4+
suspend fun predict(payload: Map<String, Any>): Float
5+
}

app/src/main/java/com/droid/cybershield/data/repository/MalwareRepositoryImpl.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import com.droid.cybershield.core.features.SafeAppWhitelist
2020
import javax.inject.Inject
2121

2222
class MalwareRepositoryImpl @Inject constructor(
23-
private val malwareDao: MalwareDao
23+
private val malwareDao: MalwareDao,
24+
private val scanner: com.droid.cybershield.core.inference.MalwareScanner,
25+
private val scanHistoryManager: ScanHistoryManager
2426
) : MalwareRepository {
2527
override suspend fun scanInstalledApps(
2628
context: Context,
@@ -36,7 +38,7 @@ class MalwareRepositoryImpl @Inject constructor(
3638

3739
onProgress?.invoke(MalwareRepository.Progress(MalwareRepository.Progress.Phase.Extracting, 100))
3840

39-
val scanner = OnnxScanner.get(context)
41+
// Scanner is injected
4042

4143
val total = apps.size
4244
var processed = 0
@@ -204,7 +206,7 @@ class MalwareRepositoryImpl @Inject constructor(
204206
appResults = appResults
205207
)
206208

207-
ScanHistoryManager(context).saveScanReport(
209+
scanHistoryManager.saveScanReport(
208210
com.droid.cybershield.data.model.ScanReport(
209211
id = report.id,
210212
timestamp = report.timestamp,
@@ -232,7 +234,7 @@ class MalwareRepositoryImpl @Inject constructor(
232234
}
233235

234236
override fun getHistory(context: Context): List<ScanReport> {
235-
val history = ScanHistoryManager(context).getScanHistory()
237+
val history = scanHistoryManager.getScanHistory()
236238
return history.map { report ->
237239
ScanReport(
238240
id = report.id,

app/src/main/java/com/droid/cybershield/di/AppModule.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.droid.cybershield.data.local.dao.PhishingDao
88
import com.droid.cybershield.data.remote.api.SafeBrowsingApi
99
import com.droid.cybershield.data.remote.client.SafeBrowsingClient
1010
import com.droid.cybershield.data.repository.MalwareRepositoryImpl
11+
import com.droid.cybershield.data.model.ScanHistoryManager
1112
import com.droid.cybershield.data.repository.PhishingRepositoryImpl
1213
import com.droid.cybershield.domain.repository.MalwareRepository
1314
import com.droid.cybershield.domain.repository.PhishingRepository
@@ -31,12 +32,26 @@ import javax.inject.Singleton
3132
object AppModule {
3233
@Provides
3334
@Singleton
34-
fun provideMalwareRepository(malwareDao: MalwareDao): MalwareRepository = MalwareRepositoryImpl(malwareDao)
35+
fun provideScanHistoryManager(@ApplicationContext context: Context) = ScanHistoryManager(context)
36+
37+
@Provides
38+
@Singleton
39+
fun provideMalwareRepository(
40+
malwareDao: MalwareDao,
41+
scanner: com.droid.cybershield.core.inference.MalwareScanner,
42+
scanHistoryManager: ScanHistoryManager
43+
): MalwareRepository = MalwareRepositoryImpl(malwareDao, scanner, scanHistoryManager)
3544

3645
@Provides
3746
@Singleton
3847
fun provideScanInstalledApps(repo: MalwareRepository) = ScanInstalledApps(repo)
3948

49+
@Provides
50+
@Singleton
51+
fun provideMalwareScanner(@ApplicationContext context: Context): com.droid.cybershield.core.inference.MalwareScanner {
52+
return com.droid.cybershield.core.inference.OnnxScanner.get(context)
53+
}
54+
4055
@Provides
4156
@Singleton
4257
fun provideGetHistory(repo: MalwareRepository) = GetHistory(repo)

app/src/test/java/com/droid/cybershield/ExampleUnitTest.kt

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.droid.cybershield.data.local.dao
2+
3+
import android.content.Context
4+
import androidx.room.Room
5+
import androidx.test.core.app.ApplicationProvider
6+
import com.droid.cybershield.data.local.CyberShieldDatabase
7+
import com.droid.cybershield.data.local.entity.MalwareCacheEntity
8+
import kotlinx.coroutines.test.runTest
9+
import org.junit.After
10+
import org.junit.Assert.assertEquals
11+
import org.junit.Assert.assertNotNull
12+
import org.junit.Before
13+
import org.junit.Test
14+
import org.junit.runner.RunWith
15+
import org.robolectric.RobolectricTestRunner
16+
import org.robolectric.annotation.Config
17+
18+
@RunWith(RobolectricTestRunner::class)
19+
@Config(sdk = [33]) // Use a recent SDK
20+
class MalwareDaoTest {
21+
22+
private lateinit var database: CyberShieldDatabase
23+
private lateinit var dao: MalwareDao
24+
25+
@Before
26+
fun setup() {
27+
val context = ApplicationProvider.getApplicationContext<Context>()
28+
// In-memory database for testing
29+
database = Room.inMemoryDatabaseBuilder(context, CyberShieldDatabase::class.java)
30+
.allowMainThreadQueries() // Allowed for testing
31+
.build()
32+
dao = database.malwareDao()
33+
}
34+
35+
@After
36+
fun tearDown() {
37+
database.close()
38+
}
39+
40+
@Test
41+
fun insertAndGetCache() = runTest {
42+
// Arrange
43+
val entity = MalwareCacheEntity(
44+
packageName = "com.test.app",
45+
isSafe = true,
46+
timestamp = 123456789L,
47+
versionCode = 1,
48+
predictionScore = 0.1f
49+
)
50+
51+
// Act
52+
dao.insertCache(entity)
53+
val retrieved = dao.getCache("com.test.app")
54+
55+
// Assert
56+
assertNotNull(retrieved)
57+
assertEquals(entity.packageName, retrieved?.packageName)
58+
assertEquals(entity.isSafe, retrieved?.isSafe)
59+
assertEquals(entity.predictionScore, retrieved?.predictionScore)
60+
}
61+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.droid.cybershield.data.repository
2+
3+
import android.content.Context
4+
import android.content.pm.ApplicationInfo
5+
import android.content.pm.PackageManager
6+
import com.droid.cybershield.core.inference.MalwareScanner
7+
import com.droid.cybershield.data.local.dao.MalwareDao
8+
import com.droid.cybershield.data.model.ScanHistoryManager
9+
import io.mockk.coEvery
10+
import io.mockk.every
11+
import io.mockk.mockk
12+
import kotlinx.coroutines.test.runTest
13+
import org.junit.Assert.assertTrue
14+
import org.junit.Test
15+
16+
class MalwareRepositoryStressTest {
17+
18+
private val malwareDao: MalwareDao = mockk(relaxed = true)
19+
private val scanner: MalwareScanner = mockk()
20+
private val scanHistoryManager: ScanHistoryManager = mockk(relaxed = true)
21+
private val context: Context = mockk()
22+
private val packageManager: PackageManager = mockk()
23+
24+
@Test
25+
fun `stress test 1000 apps performance`() = runTest {
26+
// Arrange
27+
val repository = MalwareRepositoryImpl(malwareDao, scanner, scanHistoryManager)
28+
every { context.packageManager } returns packageManager
29+
every { context.packageName } returns "com.test.app" // Exclude self
30+
31+
// Generate 1000 fake apps
32+
val apps = (1..1000).map { i ->
33+
ApplicationInfo().apply {
34+
packageName = "com.fake.app.$i"
35+
flags = 0 // Not system app
36+
sourceDir = "/data/app/com.fake.app.$i.apk"
37+
}
38+
}
39+
every { packageManager.getInstalledApplications(0) } returns apps
40+
every { packageManager.getApplicationLabel(any()) } returns "Fake App"
41+
// Mock getPackageInfo to throw exception (simulating app not found or simple path)
42+
// OR mock it properly to return permissions.
43+
// Let's mock it to return null to hit the catch block and use default logic (faster setup)
44+
// or return a basic PackageInfo.
45+
every { packageManager.getPackageInfo(any<String>(), any<Int>()) } throws RuntimeException("Skip detail")
46+
47+
// Mock Scanner to be fast
48+
coEvery { scanner.predict(any()) } returns 0.1f
49+
50+
// Act
51+
val startTime = System.currentTimeMillis()
52+
val result = repository.scanInstalledApps(context)
53+
val endTime = System.currentTimeMillis()
54+
val duration = endTime - startTime
55+
56+
// Assert
57+
println("Scanned 1000 apps in ${duration}ms")
58+
assertTrue("Scanning 1000 apps took too long: ${duration}ms", duration < 2000)
59+
assertTrue(result.totalApps >= 1000)
60+
}
61+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.droid.cybershield.data.repository
2+
3+
import com.droid.cybershield.core.features.FeatureExtractor
4+
import com.droid.cybershield.core.inference.MalwareScanner
5+
import com.droid.cybershield.data.local.dao.MalwareDao
6+
import com.droid.cybershield.data.local.entity.MalwareCacheEntity
7+
import com.droid.cybershield.data.model.ScanHistoryManager
8+
import io.mockk.coEvery
9+
import io.mockk.mockk
10+
import kotlinx.coroutines.test.runTest
11+
import org.junit.Assert.assertEquals
12+
import org.junit.Before
13+
import org.junit.Test
14+
import android.content.Context
15+
import android.content.pm.PackageManager
16+
import android.content.pm.ApplicationInfo
17+
import android.content.pm.PackageInfo
18+
import io.mockk.every
19+
import io.mockk.mockkStatic
20+
import io.mockk.unmockkAll
21+
22+
class MalwareRepositoryTest {
23+
24+
private lateinit var repository: MalwareRepositoryImpl
25+
private val malwareDao: MalwareDao = mockk()
26+
private val context: Context = mockk()
27+
private val packageManager: PackageManager = mockk()
28+
private val scanner: MalwareScanner = mockk()
29+
private val scanHistoryManager: ScanHistoryManager = mockk(relaxed = true)
30+
31+
@Before
32+
fun setup() {
33+
repository = MalwareRepositoryImpl(malwareDao, scanner, scanHistoryManager)
34+
every { context.packageManager } returns packageManager
35+
every { context.packageName } returns "com.test.app"
36+
}
37+
38+
@Test
39+
fun `scanInstalledApps should skip Whitelisted apps`() = runTest {
40+
// Arrange
41+
val app = ApplicationInfo().apply {
42+
packageName = "com.whatsapp"
43+
flags = 0
44+
sourceDir = "/data/app/com.whatsapp.apk"
45+
}
46+
every { packageManager.getInstalledApplications(0) } returns listOf(app)
47+
every { packageManager.getApplicationLabel(app) } returns "WhatsApp"
48+
49+
// Act
50+
val result = repository.scanInstalledApps(context)
51+
52+
// Assert
53+
assertEquals(0, result.maliciousApps)
54+
assertEquals(1, result.benignApps) // WhatsApp is benign
55+
}
56+
57+
@Test
58+
fun `scanInstalledApps should use Cache when version matches`() = runTest {
59+
// Arrange
60+
val app = ApplicationInfo().apply {
61+
packageName = "com.unknown.app"
62+
flags = 0
63+
sourceDir = "/data/app/com.unknown.app.apk"
64+
}
65+
val packageInfo = PackageInfo().apply {
66+
versionCode = 100
67+
}
68+
69+
every { packageManager.getInstalledApplications(0) } returns listOf(app)
70+
every { packageManager.getApplicationLabel(app) } returns "Unknown App"
71+
every { packageManager.getPackageInfo("com.unknown.app", 0) } returns packageInfo
72+
73+
// Cache says it is SAFE
74+
coEvery { malwareDao.getCache("com.unknown.app") } returns MalwareCacheEntity(
75+
packageName = "com.unknown.app",
76+
isSafe = true,
77+
timestamp = System.currentTimeMillis(),
78+
versionCode = 100,
79+
predictionScore = 0.1f
80+
)
81+
82+
// Act
83+
val result = repository.scanInstalledApps(context)
84+
85+
// Assert
86+
assertEquals(0, result.maliciousApps)
87+
assertEquals(1, result.benignApps)
88+
// Verify we NEVER called the AI model
89+
coEvery { scanner.predict(any()) } answers { throw IllegalStateException("AI should not be called!") }
90+
}
91+
}

0 commit comments

Comments
 (0)