1package com.peachpasswords.security23import javax.xml.parsers.DocumentBuilderFactory4import org.junit.Assert.assertFalse5import org.junit.Assert.assertTrue6import org.junit.Test7import org.w3c.dom.Element89class SecureFlagCoverageTest {1011 12 13 14 15 private val secretScreens = listOf(16 "app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt",17 "app/src/main/java/com/peachpasswords/ui/vault/EntryEditScreen.kt",18 "app/src/main/java/com/peachpasswords/ui/vault/NotesModeScreen.kt",19 "app/src/main/java/com/peachpasswords/ui/vault/ExportScreen.kt",20 "app/src/main/java/com/peachpasswords/ui/vault/RecoveryWordsScreen.kt",21 "app/src/main/java/com/peachpasswords/ui/recovery/RecoveryWordEntryScreen.kt",22 "app/src/main/java/com/peachpasswords/ui/codex/CodexGenerateScreen.kt",23 )2425 26 27 28 private val secretActivities = listOf(29 "com.peachpasswords.autofill.AutofillEntryPickerActivity",30 "com.peachpasswords.passkeys.PasskeyCredentialSelectionActivity",31 )3233 @Test fun secret_screens_keep_flag_secure_coverage() {34 secretScreens.forEach { relativePath ->35 val file = java.io.File(relativePath).takeIf { it.exists() }36 ?: java.io.File("../$relativePath")37 val source = file.readText()38 assertTrue(39 "$relativePath must protect screenshots while visible",40 source.contains("SecureFlagEffect()") ||41 source.contains("WindowManager.LayoutParams.FLAG_SECURE"),42 )43 }44 }4546 @Test fun secret_bearing_activities_set_flag_secure() {47 48 49 50 val manifestActivities = parseActivityNames(51 "app/src/main/AndroidManifest.xml",52 )53 secretActivities.forEach { fqcn ->54 val shortName = fqcn.substringAfterLast('.')55 assertTrue(56 "$fqcn must be declared in the manifest",57 manifestActivities.any { it == fqcn || it.endsWith(".$shortName") },58 )59 val relativePath = fqcnToSourcePath(fqcn)60 val file = java.io.File(relativePath).takeIf { it.exists() }61 ?: java.io.File("../$relativePath")62 val source = file.readText()63 assertTrue(64 "$fqcn must delegate secure ownership " +65 "(separate window from MainActivity)",66 source.contains("SecureWindowPolicy"),67 )68 }69 }7071 @Test fun all_window_policy_transitions_are_centralized() {72 val helper = java.io.File(73 "app/src/main/java/com/peachpasswords/ui/components/SecureWindowPolicy.kt",74 ).takeIf { it.exists() } ?: java.io.File(75 "../app/src/main/java/com/peachpasswords/ui/components/SecureWindowPolicy.kt",76 )77 val source = helper.readText()78 assertTrue(source.contains("setUserSecure"))79 assertTrue(source.contains("acquire"))80 assertTrue(source.contains("release"))81 assertTrue(source.contains("leases"))82 assertTrue(source.contains("clearFlags"))83 84 85 listOf(86 "app/src/main/java/com/peachpasswords/MainActivity.kt",87 "app/src/main/java/com/peachpasswords/autofill/AutofillEntryPickerActivity.kt",88 "app/src/main/java/com/peachpasswords/passkeys/PasskeyCredentialSelectionActivity.kt",89 ).forEach { relativePath ->90 val file = java.io.File(relativePath).takeIf { it.exists() }91 ?: java.io.File("../$relativePath")92 val activitySource = file.readText()93 assertTrue(94 "$relativePath must delegate secure policy ownership",95 activitySource.contains("SecureWindowPolicy"),96 )97 assertFalse(98 "$relativePath must not clear FLAG_SECURE directly",99 activitySource.contains("clearFlags("),100 )101 }102 }103104 private fun parseActivityNames(relativePath: String): Set<String> {105 val file = java.io.File(relativePath).takeIf { it.exists() }106 ?: java.io.File("../$relativePath")107 val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()108 .parse(file)109 val activities = doc.getElementsByTagName("activity")110 return (0 until activities.length)111 .mapNotNull { (activities.item(it) as? Element)?.getAttribute("android:name") }112 .toSet()113 }114115 116 117 118 private fun fqcnToSourcePath(fqcn: String): String {119 val pkg = fqcn.substringBeforeLast('.')120 val cls = fqcn.substringAfterLast('.')121 return "app/src/main/java/" + pkg.replace('.', '/') + "/$cls.kt"122 }123}