SyncServiceStartTest.kt

3.4 KB · raw

1package com.peachpasswords.sync23import org.junit.Assert.assertEquals4import org.junit.Assert.assertArrayEquals5import org.junit.Assert.assertTrue6import org.junit.Test78/**9 * The service start is the first step of every sync path, and it was the one10 * step that could throw straight out of a button press: Android 12+ refuses a11 * foreground service started from the background, and `startForegroundService`12 * was called bare.13 */14class SyncServiceStartTest {1516    /** The real thing on API 31+, whose class is not on this app's minSdk. */17    private fun backgroundStartRefused(): IllegalStateException = IllegalStateException(18        "Not allowed to start service Intent { cmp=com.peachpasswords/.sync.PeachSyncService }: " +19            "app is in background",20    )2122    @Test fun `a start Android refuses is reported, not thrown`() {23        val reported = mutableListOf<SyncOutcomeSummary>()24        val started = startOrReportBlocked({ reported += it }) { throw backgroundStartRefused() }2526        assertEquals(false, started)27        assertEquals(listOf(SyncOutcomeReason.FOREGROUND_START_BLOCKED), reported.map { it.reason })28        assertTrue(reported.single().detail?.contains("app is in background") == true)29    }3031    @Test fun `the refusal says what the person holding the phone can do`() {32        assertEquals(33            "Android blocked Peach from starting sync in the background — open Peach and sync again",34            describeSyncOutcome(SyncOutcomeSummary(reason = SyncOutcomeReason.FOREGROUND_START_BLOCKED)),35        )36        assertEquals(37            SyncIndicator.ERROR,38            syncIndicator(39                SyncOutcomeSummary(reason = SyncOutcomeReason.FOREGROUND_START_BLOCKED),40                pairedCount = 1,41            ),42        )43    }4445    /** A start that fails some other way is still a sync that did not happen. */46    @Test fun `anything else the platform throws is reported too, and never escapes`() {47        val reported = mutableListOf<SyncOutcomeSummary>()48        val started = startOrReportBlocked({ reported += it }) {49            throw SecurityException("missing FOREGROUND_SERVICE_DATA_SYNC")50        }5152        assertEquals(false, started)53        assertEquals(listOf(SyncOutcomeReason.RUN_FAILED), reported.map { it.reason })54    }5556    @Test fun `a start that works reports nothing`() {57        val reported = mutableListOf<SyncOutcomeSummary>()58        var ran = false5960        assertEquals(true, startOrReportBlocked({ reported += it }) { ran = true })61        assertTrue(ran)62        assertTrue(reported.isEmpty())63    }6465    @Test fun `only a nudge to an existing listener replaces its waiters`() {66        assertEquals(false, shouldRefreshExistingWaiters(listenerStarted = true))67        assertEquals(true, shouldRefreshExistingWaiters(listenerStarted = false))68    }6970    @Test fun `an unlocked vault derives transient listener credentials`() {71        val credentials = randomLocalIdentityCredentials("phone-id")72        val expected = localSyncKeypair(credentials)73        val actual = deriveUnlockedListenerCredentials(credentials, "Phone")7475        assertEquals("phone-id", actual.identity.deviceId)76        assertEquals("Phone", actual.identity.deviceName)77        assertEquals(pubkeyToHex(expected.publicKey), actual.identity.longTermPublicKeyHex)78        assertArrayEquals(expected.publicKey, actual.keys.publicKey)79        assertArrayEquals(expected.secretKey, actual.keys.secretKey)80    }81}