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 "com.peachpasswords.credentials.CredentialProviderUnlockActivity",32 )3334 @Test fun secret_screens_keep_flag_secure_coverage() {35 secretScreens.forEach { relativePath ->36 val file = java.io.File(relativePath).takeIf { it.exists() }37 ?: java.io.File("../$relativePath")38 val source = file.readText()39 assertTrue(40 "$relativePath must protect screenshots while visible",41 source.contains("SecureFlagEffect()") ||42 source.contains("WindowManager.LayoutParams.FLAG_SECURE"),43 )44 }45 }4647 @Test fun secret_bearing_activities_set_flag_secure() {48 49 50 51 val manifestActivities = parseActivityNames(52 "app/src/main/AndroidManifest.xml",53 )54 secretActivities.forEach { fqcn ->55 val shortName = fqcn.substringAfterLast('.')56 assertTrue(57 "$fqcn must be declared in the manifest",58 manifestActivities.any { it == fqcn || it.endsWith(".$shortName") },59 )60 val relativePath = fqcnToSourcePath(fqcn)61 val file = java.io.File(relativePath).takeIf { it.exists() }62 ?: java.io.File("../$relativePath")63 val source = file.readText()64 assertTrue(65 "$fqcn must delegate secure ownership " +66 "(separate window from MainActivity)",67 source.contains("SecureWindowPolicy"),68 )69 }70 }7172 @Test fun all_window_policy_transitions_are_centralized() {73 val helper = java.io.File(74 "app/src/main/java/com/peachpasswords/ui/components/SecureWindowPolicy.kt",75 ).takeIf { it.exists() } ?: java.io.File(76 "../app/src/main/java/com/peachpasswords/ui/components/SecureWindowPolicy.kt",77 )78 val source = helper.readText()79 assertTrue(source.contains("setUserSecure"))80 assertTrue(source.contains("acquire"))81 assertTrue(source.contains("release"))82 assertTrue(source.contains("userChoiceApplied"))83 assertTrue(source.contains("leases"))84 assertTrue(source.contains("clearFlags"))85 86 87 listOf(88 "app/src/main/java/com/peachpasswords/MainActivity.kt",89 "app/src/main/java/com/peachpasswords/autofill/AutofillEntryPickerActivity.kt",90 "app/src/main/java/com/peachpasswords/passkeys/PasskeyCredentialSelectionActivity.kt",91 "app/src/main/java/com/peachpasswords/credentials/CredentialProviderUnlockActivity.kt",92 ).forEach { relativePath ->93 val file = java.io.File(relativePath).takeIf { it.exists() }94 ?: java.io.File("../$relativePath")95 val activitySource = file.readText()96 assertTrue(97 "$relativePath must delegate secure policy ownership",98 activitySource.contains("SecureWindowPolicy"),99 )100 assertFalse(101 "$relativePath must not clear FLAG_SECURE directly",102 activitySource.contains("clearFlags("),103 )104 }105 }106107 private fun parseActivityNames(relativePath: String): Set<String> {108 val file = java.io.File(relativePath).takeIf { it.exists() }109 ?: java.io.File("../$relativePath")110 val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()111 .parse(file)112 val activities = doc.getElementsByTagName("activity")113 return (0 until activities.length)114 .mapNotNull { (activities.item(it) as? Element)?.getAttribute("android:name") }115 .toSet()116 }117118 119 120 121 private fun fqcnToSourcePath(fqcn: String): String {122 val pkg = fqcn.substringBeforeLast('.')123 val cls = fqcn.substringAfterLast('.')124 return "app/src/main/java/" + pkg.replace('.', '/') + "/$cls.kt"125 }126}