BackupRulesCoverageTest.kt

3.8 KB · raw

1package com.peachpasswords.backup23import java.io.File4import javax.xml.parsers.DocumentBuilderFactory5import org.junit.Assert.assertTrue6import org.junit.Test78/** Every declared SharedPreferences file must be excluded from both transfer paths. */9class BackupRulesCoverageTest {10    @Test11    fun every_shared_preferences_filename_is_excluded_from_both_rule_files() {12        val sourceRoot = projectFile("src/main/java")13        val source = sourceRoot.walkTopDown()14            .filter { it.isFile && it.extension == "kt" }15            .map { it.readText() }16            .toList()17        val declarations = source18            .flatMap { SHARED_PREFERENCES_DECLARATION.findAll(it).map { match -> match.groupValues[1] to match.groupValues[2] } }19            .toMap()20        val preferences = source21            .flatMap { text ->22                SHARED_PREFERENCES_CALLS.findAll(text).mapNotNull { match ->23                    val reference = (match.groups[1] ?: match.groups[2])?.value?.trim() ?: return@mapNotNull null24                    val literal = reference.removeSurrounding("\"")25                    if (literal != reference) return@mapNotNull literal.ifBlank { null }26                    // A bare identifier is either a declared constant or a27                    // helper parameter. Constants resolve here; the one28                    // indirection in this codebase (SyncGenerationStore's29                    // private encryptedPrefs(context, name)) is covered by30                    // scanning its call sites instead, so a new preference31                    // opened through it is still collected.32                    declarations[reference]33                } + SHARED_PREFERENCES_HELPER_CALLS.findAll(text).mapNotNull { match ->34                    val reference = match.groupValues[1].trim()35                    val literal = reference.removeSurrounding("\"")36                    if (literal != reference) literal.ifBlank { null } else declarations[reference]37                }38            }39            .map { "$it.xml" }40            .toSet()41        assertTrue("no SharedPreferences file names were found", preferences.isNotEmpty())4243        val backup = excludedPaths(projectFile("src/main/res/xml/backup_rules.xml"))44        val extraction = excludedPaths(projectFile("src/main/res/xml/data_extraction_rules.xml"))45        preferences.forEach { filename ->46            assertTrue("$filename is missing from backup_rules.xml", filename in backup)47            assertTrue("$filename is missing from data_extraction_rules.xml", filename in extraction)48        }49    }5051    private fun excludedPaths(file: File): Set<String> {52        val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file)53        return (0 until document.getElementsByTagName("exclude").length)54            .map { index -> document.getElementsByTagName("exclude").item(index).attributes.getNamedItem("path").nodeValue }55            .toSet()56    }5758    private fun projectFile(moduleRelativePath: String): File =59        listOf(File(moduleRelativePath), File("app/$moduleRelativePath"))60            .firstOrNull { it.exists() }61            ?: error("could not locate $moduleRelativePath from ${File(".").absolutePath}")6263    private companion object {64        val SHARED_PREFERENCES_DECLARATION = Regex(65            "(?m)\\b(?:private\\s+|internal\\s+|public\\s+)?(?:const\\s+)?val\\s+(\\w+)\\s*=\\s*\"([^\"]+)\"",66        )67        val SHARED_PREFERENCES_CALLS = Regex(68            "(?:getSharedPreferences|deleteSharedPreferences)\\s*\\(\\s*([^,\\)]+)|EncryptedSharedPreferences\\.create\\s*\\(\\s*[^,]+,\\s*([^,\\)]+)",69        )7071        /** The one indirection: a private helper that takes the prefs file name. */72        val SHARED_PREFERENCES_HELPER_CALLS = Regex(73            "encryptedPrefs\\s*\\(\\s*[^,]+,\\s*([^,\\)]+)",74        )75    }76}77