tauri-plugin-background-service 1.0.1

Background service lifecycle plugin for Tauri v2 — run long-lived tasks on Android, iOS, and desktop
Documentation
package app.tauri.backgroundservice

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.io.File
import java.util.Properties

/**
 * AND-09: the library manifest must NOT declare
 * `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` — it is Play-policy-restricted and must
 * be opted in by the HOST application. The Tauri `requestBatteryExemption`
 * command still ships (it launches the system dialog); the host-declared
 * permission is what authorizes that dialog.
 *
 * This gate is **load-bearing**: re-adding the `<uses-permission>` to the plugin
 * source manifest REDs `libraryManifest_doesNotDeclareRequestIgnoreBatteryOptimizations`,
 * and the merged-manifest corroboration REDs
 * `mergedLibraryManifest_doesNotContainRequestIgnoreBatteryOptimizations`.
 *
 * The host-fixture leg proves the assertion is not vacuous: a manifest fragment
 * that DOES declare the permission is detectable by the same text scan, so a
 * host opt-in (merged on top of this library) remains observable.
 */
@RunWith(RobolectricTestRunner::class)
class BatteryOptimizationPermissionTest {

    private val permission = "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"

    private fun libraryManifestText(): String {
        val file = File("src/main/AndroidManifest.xml")
        assertTrue(
            "src/main/AndroidManifest.xml not found relative to " +
                "cwd=${File(".").absolutePath} (did :testDebugUnitTest change its working dir?)",
            file.isFile,
        )
        return file.readText()
    }

    private fun mergedManifestText(): String {
        readViaTestConfig()?.let { return it }
        val fallback = File(
            "build/intermediates/packaged_manifests/debugUnitTest/" +
                "processDebugUnitTestManifest/AndroidManifest.xml",
        )
        assertTrue(
            "merged manifest not found via test_config.properties nor at " +
                "'${fallback.path}' — did processDebugUnitTestManifest run?",
            fallback.isFile,
        )
        return fallback.readText()
    }

    private fun readViaTestConfig(): String? {
        val loader = javaClass.classLoader ?: return null
        val candidates = listOf(
            "com/android/tools/test_config.properties",
            "com.android.tools.test_config.properties",
        )
        for (name in candidates) {
            val stream = loader.getResourceAsStream(name) ?: continue
            val props = Properties()
            stream.use { props.load(it) }
            val path = props.getProperty("android_merged_manifest") ?: continue
            val file = File(path)
            if (file.isFile) return file.readText()
        }
        return null
    }

    @Test
    fun libraryManifest_doesNotDeclareRequestIgnoreBatteryOptimizations() {
        val src = libraryManifestText()
        assertFalse(
            "AND-09: the library AndroidManifest.xml still declares " +
                "$permission — it is Play-policy-restricted and must be opted in " +
                "by the HOST app, not the library.",
            src.contains("\"$permission\""),
        )
    }

    @Test
    fun mergedLibraryManifest_doesNotContainRequestIgnoreBatteryOptimizations() {
        // The plugin module's merged manifest is the merge the host *can* run;
        // with the source permission removed it must not surface either.
        val xml = mergedManifestText()
        assertFalse(
            "AND-09: the MERGED library manifest still carries $permission — " +
                "the source removal did not propagate through manifest merge.",
            xml.contains(permission),
        )
    }

    /**
     * Non-vacuousness leg: a host fragment that opts IN (declares the
     * permission) is detectable by the same text scan, proving a real host
     * opt-in remains observable after the library drops it. This mirrors how a
     * host would carry the permission in its own manifest.
     */
    @Test
    fun hostOptInFixture_isDetectableByTheSameScan() {
        val hostFragment = """
            <manifest xmlns:android="http://schemas.android.com/apk/res/android">
                <uses-permission android:name="$permission" />
            </manifest>
        """.trimIndent()
        assertTrue(
            "host opt-in fixture must be detectable (else the absence assertion is vacuous)",
            hostFragment.contains("\"$permission\""),
        )
        assertNotNull(hostFragment)
    }
}