tauri-plugin-background-service 0.7.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.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

/**
 * Unit tests for DurableState:
 * - Round-trip: save → load produces identical state
 * - Defaults: fresh load returns correct default values
 * - Clear: removes all keys from SharedPreferences
 * - Nullable fields: null values survive round-trip
 * - Non-default values: all fields persist correctly
 */
@RunWith(RobolectricTestRunner::class)
class DurableStateTest {

    private lateinit var context: Context

    @Before
    fun setup() {
        context = ApplicationProvider.getApplicationContext()
        DurableState.clear(context)
    }

    // ── Defaults ──────────────────────────────────────────────────────────

    @Test
    fun load_defaults_areCorrect() {
        val state = DurableState.load(context)

        assertFalse(state.desiredRunning)
        assertEquals("", state.lastServiceLabel)
        assertEquals("dataSync", state.lastServiceType)
        assertEquals(0L, state.lastStartEpochMs)
        assertEquals(0L, state.lastHeartbeatEpochMs)
        assertEquals("unknown", state.lastNativeState)
        assertNull(state.lastPlatformError)
        assertEquals(0, state.restartAttempt)
        assertFalse(state.recoveryPending)
        assertNull(state.recoveryReason)
    }

    // ── Round-trip: all fields populated ──────────────────────────────────

    @Test
    fun saveLoad_roundTrip_allFieldsPopulated() {
        val original = DurableState(
            desiredRunning = true,
            lastServiceLabel = "Syncing data",
            lastServiceType = "specialUse",
            lastStartEpochMs = 1716057600000L,
            lastHeartbeatEpochMs = 1716057660000L,
            lastNativeState = "running",
            lastPlatformError = "timeout",
            restartAttempt = 3,
            recoveryPending = true,
            recoveryReason = "boot_fgs_type_restricted",
        )

        DurableState.save(context, original)
        val loaded = DurableState.load(context)

        assertEquals(original, loaded)
    }

    // ── Round-trip: nullable fields are null ───────────────────────────────

    @Test
    fun saveLoad_roundTrip_nullableFieldsNull() {
        val original = DurableState(
            desiredRunning = true,
            lastServiceLabel = "Active",
            lastPlatformError = null,
            recoveryReason = null,
        )

        DurableState.save(context, original)
        val loaded = DurableState.load(context)

        assertEquals(original, loaded)
        assertNull(loaded.lastPlatformError)
        assertNull(loaded.recoveryReason)
    }

    // ── Clear ─────────────────────────────────────────────────────────────

    @Test
    fun clear_removesAllKeys() {
        val state = DurableState(
            desiredRunning = true,
            lastServiceLabel = "Sync",
            lastServiceType = "location",
            lastStartEpochMs = 12345L,
            lastHeartbeatEpochMs = 67890L,
            lastNativeState = "running",
            lastPlatformError = "error",
            restartAttempt = 5,
            recoveryPending = true,
            recoveryReason = "test",
        )

        DurableState.save(context, state)
        DurableState.clear(context)
        val loaded = DurableState.load(context)

        // After clear, should return defaults
        val defaults = DurableState()
        assertEquals(defaults, loaded)
    }

    @Test
    fun clear_onEmptyPrefs_isNoOp() {
        // Clear on already-empty prefs should not throw
        DurableState.clear(context)
        DurableState.clear(context)

        val loaded = DurableState.load(context)
        assertEquals(DurableState(), loaded)
    }

    // ── Overwrite ─────────────────────────────────────────────────────────

    @Test
    fun save_overwritesPreviousState() {
        val first = DurableState(
            desiredRunning = true,
            lastServiceLabel = "First",
            lastPlatformError = "old error",
        )
        val second = DurableState(
            desiredRunning = false,
            lastServiceLabel = "Second",
            lastPlatformError = null,
        )

        DurableState.save(context, first)
        DurableState.save(context, second)
        val loaded = DurableState.load(context)

        assertEquals(second, loaded)
        assertFalse(loaded.desiredRunning)
        assertEquals("Second", loaded.lastServiceLabel)
        assertNull(loaded.lastPlatformError)
    }

    // ── Specific field updates ────────────────────────────────────────────

    @Test
    fun saveLoad_heartbeatUpdatePreservesOtherFields() {
        val original = DurableState(
            desiredRunning = true,
            lastServiceLabel = "Syncing",
            lastServiceType = "dataSync",
            lastStartEpochMs = 1000L,
        )
        DurableState.save(context, original)

        val updated = original.copy(lastHeartbeatEpochMs = 2000L)
        DurableState.save(context, updated)

        val loaded = DurableState.load(context)
        assertEquals(2000L, loaded.lastHeartbeatEpochMs)
        assertTrue(loaded.desiredRunning)
        assertEquals("Syncing", loaded.lastServiceLabel)
        assertEquals("dataSync", loaded.lastServiceType)
        assertEquals(1000L, loaded.lastStartEpochMs)
    }

    @Test
    fun saveLoad_nativeStateValues() {
        val states = listOf("idle", "starting", "running", "stopping", "timeout", "expired", "recovering", "error")

        for (nativeState in states) {
            DurableState.clear(context)
            val state = DurableState(lastNativeState = nativeState)
            DurableState.save(context, state)
            val loaded = DurableState.load(context)
            assertEquals(nativeState, loaded.lastNativeState)
        }
    }

    // ── SharedPreferences file name ───────────────────────────────────────

    @Test
    fun usesCorrectSharedPreferencesFile() {
        DurableState.save(context, DurableState(desiredRunning = true))

        val prefs = context.getSharedPreferences("tauri_bg_service_state", Context.MODE_PRIVATE)
        assertTrue(prefs.getBoolean("desired_running", false))
    }
}