1package com.peachpasswords.security23import javax.xml.parsers.DocumentBuilderFactory4import org.junit.Assert.assertTrue5import org.junit.Test6import org.w3c.dom.Element78class SecureFlagCoverageTest {910 11 12 13 14 private val secretScreens = listOf(15 "app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt",16 "app/src/main/java/com/peachpasswords/ui/vault/EntryEditScreen.kt",17 "app/src/main/java/com/peachpasswords/ui/vault/NotesModeScreen.kt",18 "app/src/main/java/com/peachpasswords/ui/vault/ExportScreen.kt",19 "app/src/main/java/com/peachpasswords/ui/vault/RecoveryWordsScreen.kt",20 "app/src/main/java/com/peachpasswords/ui/recovery/RecoveryWordEntryScreen.kt",21 "app/src/main/java/com/peachpasswords/ui/codex/CodexGenerateScreen.kt",22 )2324 25 26 27 private val secretActivities = listOf(28 "com.peachpasswords.autofill.AutofillEntryPickerActivity",29 "com.peachpasswords.passkeys.PasskeyCredentialSelectionActivity",30 )3132 @Test fun secret_screens_keep_flag_secure_coverage() {33 secretScreens.forEach { relativePath ->34 val file = java.io.File(relativePath).takeIf { it.exists() }35 ?: java.io.File("../$relativePath")36 val source = file.readText()37 assertTrue(38 "$relativePath must protect screenshots while visible",39 source.contains("SecureFlagEffect()") ||40 source.contains("WindowManager.LayoutParams.FLAG_SECURE"),41 )42 }43 }4445 @Test fun secret_bearing_activities_set_flag_secure() {46 47 48 49 val manifestActivities = parseActivityNames(50 "app/src/main/AndroidManifest.xml",51 )52 secretActivities.forEach { fqcn ->53 val shortName = fqcn.substringAfterLast('.')54 assertTrue(55 "$fqcn must be declared in the manifest",56 manifestActivities.any { it == fqcn || it.endsWith(".$shortName") },57 )58 val relativePath = fqcnToSourcePath(fqcn)59 val file = java.io.File(relativePath).takeIf { it.exists() }60 ?: java.io.File("../$relativePath")61 val source = file.readText()62 assertTrue(63 "$fqcn must set WindowManager.LayoutParams.FLAG_SECURE " +64 "(separate window from MainActivity)",65 source.contains("WindowManager.LayoutParams.FLAG_SECURE"),66 )67 }68 }6970 private fun parseActivityNames(relativePath: String): Set<String> {71 val file = java.io.File(relativePath).takeIf { it.exists() }72 ?: java.io.File("../$relativePath")73 val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()74 .parse(file)75 val activities = doc.getElementsByTagName("activity")76 return (0 until activities.length)77 .mapNotNull { (activities.item(it) as? Element)?.getAttribute("android:name") }78 .toSet()79 }8081 82 83 84 private fun fqcnToSourcePath(fqcn: String): String {85 val pkg = fqcn.substringBeforeLast('.')86 val cls = fqcn.substringAfterLast('.')87 return "app/src/main/java/" + pkg.replace('.', '/') + "/$cls.kt"88 }89}