package com.peachpasswords.backup import java.io.File import javax.xml.parsers.DocumentBuilderFactory import org.junit.Assert.assertTrue import org.junit.Test /** Every declared SharedPreferences file must be excluded from both transfer paths. */ class BackupRulesCoverageTest { @Test fun every_shared_preferences_filename_is_excluded_from_both_rule_files() { val sourceRoot = projectFile("src/main/java") val source = sourceRoot.walkTopDown() .filter { it.isFile && it.extension == "kt" } .map { it.readText() } .toList() val declarations = source .flatMap { SHARED_PREFERENCES_DECLARATION.findAll(it).map { match -> match.groupValues[1] to match.groupValues[2] } } .toMap() val preferences = source .flatMap { text -> SHARED_PREFERENCES_CALLS.findAll(text).mapNotNull { match -> val reference = (match.groups[1] ?: match.groups[2])?.value?.trim() ?: return@mapNotNull null val literal = reference.removeSurrounding("\"") if (literal != reference) return@mapNotNull literal.ifBlank { null } // A bare identifier is either a declared constant or a // helper parameter. Constants resolve here; the one // indirection in this codebase (SyncGenerationStore's // private encryptedPrefs(context, name)) is covered by // scanning its call sites instead, so a new preference // opened through it is still collected. declarations[reference] } + SHARED_PREFERENCES_HELPER_CALLS.findAll(text).mapNotNull { match -> val reference = match.groupValues[1].trim() val literal = reference.removeSurrounding("\"") if (literal != reference) literal.ifBlank { null } else declarations[reference] } } .map { "$it.xml" } .toSet() assertTrue("no SharedPreferences file names were found", preferences.isNotEmpty()) val backup = excludedPaths(projectFile("src/main/res/xml/backup_rules.xml")) val extraction = excludedPaths(projectFile("src/main/res/xml/data_extraction_rules.xml")) preferences.forEach { filename -> assertTrue("$filename is missing from backup_rules.xml", filename in backup) assertTrue("$filename is missing from data_extraction_rules.xml", filename in extraction) } } private fun excludedPaths(file: File): Set { val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file) return (0 until document.getElementsByTagName("exclude").length) .map { index -> document.getElementsByTagName("exclude").item(index).attributes.getNamedItem("path").nodeValue } .toSet() } private fun projectFile(moduleRelativePath: String): File = listOf(File(moduleRelativePath), File("app/$moduleRelativePath")) .firstOrNull { it.exists() } ?: error("could not locate $moduleRelativePath from ${File(".").absolutePath}") private companion object { val SHARED_PREFERENCES_DECLARATION = Regex( "(?m)\\b(?:private\\s+|internal\\s+|public\\s+)?(?:const\\s+)?val\\s+(\\w+)\\s*=\\s*\"([^\"]+)\"", ) val SHARED_PREFERENCES_CALLS = Regex( "(?:getSharedPreferences|deleteSharedPreferences)\\s*\\(\\s*([^,\\)]+)|EncryptedSharedPreferences\\.create\\s*\\(\\s*[^,]+,\\s*([^,\\)]+)", ) /** The one indirection: a private helper that takes the prefs file name. */ val SHARED_PREFERENCES_HELPER_CALLS = Regex( "encryptedPrefs\\s*\\(\\s*[^,]+,\\s*([^,\\)]+)", ) } }