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 assertTrue(54 "$fqcn must be declared in the manifest",55 manifestActivities.contains(fqcn),56 )57 val relativePath = fqcnToSourcePath(fqcn)58 val file = java.io.File(relativePath).takeIf { it.exists() }59 ?: java.io.File("../$relativePath")60 val source = file.readText()61 assertTrue(62 "$fqcn must set WindowManager.LayoutParams.FLAG_SECURE " +63 "(separate window from MainActivity)",64 source.contains("WindowManager.LayoutParams.FLAG_SECURE"),65 )66 }67 }6869 private fun parseActivityNames(relativePath: String): Set<String> {70 val file = java.io.File(relativePath).takeIf { it.exists() }71 ?: java.io.File("../$relativePath")72 val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()73 .parse(file)74 val activities = doc.getElementsByTagName("activity")75 return (0 until activities.length)76 .mapNotNull { (activities.item(it) as? Element)?.getAttribute("android:name") }77 .toSet()78 }7980 81 82 83 private fun fqcnToSourcePath(fqcn: String): String {84 val pkg = fqcn.substringBeforeLast('.')85 val cls = fqcn.substringAfterLast('.')86 return "app/src/main/java/" + pkg.replace('.', '/') + "/$cls.kt"87 }88}