package com.peachpasswords.sync import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test /** * §10.5: every relay adapter must forward every v2 callback, and a callback must * never be one that only fails once a peer asks for it. * * The v2 interfaces extend the v1 surface, so a *missing* member is a compile * error. What the compiler cannot see is a member that is present and returns * without calling through — which is exactly the defect this exists to catch: * the host's `onConsent(RequestV2)` once returned `true` without reaching the * SAS prompt, so the source-side confirmation never appeared. * * [bindSourceSessionDeps] is the whole forwarding surface for both relay source * adapters, so dirtying every member here covers both. */ class AdoptionAdapterCoverageTest { private val request = AdoptionFrame.Request( sessionRequestId = "session-request", deviceName = "Phone", deviceType = "android", ) private val requestV2 = AdoptionFrame.RequestV2( adoptionVersion = ADOPTION_VERSION, deviceId = "receiver-id", deviceName = "Phone", deviceType = "android", longTermPublicKeyHex = "aa".repeat(32), signingPublicKeyHex = "bb".repeat(32), ) private val device = PairedDevice( deviceId = "receiver-id", deviceName = "Phone", deviceType = "android", longTermPublicKeyHex = "aa".repeat(32), pairedAt = 1L, ) private val presented = LocalDeviceIdentity( deviceId = "presented-id", deviceName = "This browser", deviceType = DeviceType.ANDROID, longTermPublicKeyHex = "cc".repeat(32), signingPublicKeyHex = "dd".repeat(32), ) /// Records which members the binder actually reached. private class RecordingSourceDeps : AdoptionSourceV2Deps { val calls = mutableSetOf() override val sourceIdentity = LocalDeviceIdentity( deviceId = "probe-id", deviceName = "Probe", deviceType = DeviceType.ANDROID, longTermPublicKeyHex = "ee".repeat(32), signingPublicKeyHex = "ff".repeat(32), ) override val pairState = AdoptionPairState("01".repeat(32), "wss://probe.test") override suspend fun rootKeyHex() = "11".repeat(32).also { calls += "rootKeyHex" } override suspend fun vaultId() = "22".repeat(32).also { calls += "vaultId" } override suspend fun license() = "PPK-probe".also { calls += "license" } override suspend fun applyLicense(license: String) { calls += "applyLicense" } override suspend fun onConsent(request: AdoptionFrame.Request) = true.also { calls += "onConsentV1" } override suspend fun onConsent(request: AdoptionFrame.RequestV2) = true.also { calls += "onConsentV2" } override suspend fun persistPendingHolder(request: AdoptionFrame.Request, nowMs: Long) = true.also { calls += "persistPendingHolder" } override suspend fun completePendingHolder(request: AdoptionFrame.Request, device: PairedDevice) = true.also { calls += "completePendingHolder" } override suspend fun persistPendingIdentity(device: PairedDevice, nowMs: Long) = true.also { calls += "persistPendingIdentity" } override suspend fun completePendingIdentity(device: PairedDevice) = true.also { calls += "completePendingIdentity" } override suspend fun upsertPairedDevice(device: PairedDevice) { calls += "upsertPairedDevice" } override fun hasDeviceIdentityCollision(device: PairedDevice) = false.also { calls += "hasDeviceIdentityCollision" } override suspend fun rotateSourceIdentity() = sourceIdentity.also { calls += "rotateSourceIdentity" } override fun snapshotCapable() = false.also { calls += "snapshotCapable" } override suspend fun vaultSnapshot() = null.also { calls += "vaultSnapshot" } override suspend fun outgoingDeviceManifest() = null.also { calls += "outgoingDeviceManifest" } override suspend fun markSynced(deviceId: String) { calls += "markSynced" } override suspend fun duressSlotJson() = null.also { calls += "duressSlotJson" } } @Test fun `the session binder forwards every member, and covers the whole interface`() = runBlocking { val probe = RecordingSourceDeps() val sessionPairState = AdoptionPairState("33".repeat(32), "wss://session.test") var confirmedSas: String? = null var confirmedRequest: AdoptionFrame.Request? = null val bound = bindSourceSessionDeps( sourceDeps = probe, sourceIdentity = presented, pairState = sessionPairState, sas = "12345678", snapshotCapable = true, confirm = { sas, request -> confirmedSas = sas confirmedRequest = request true }, ) // Session facts are bound, not forwarded. assertEquals("the presented identity is the session's", presented, bound.sourceIdentity) assertEquals(sessionPairState, bound.pairState) assertTrue("snapshot capability is a session fact", bound.snapshotCapable()) bound.rootKeyHex() bound.vaultId() bound.license() bound.applyLicense("PPK-x") bound.onConsent(request) bound.persistPendingHolder(request, 1L) bound.completePendingHolder(request, device) bound.persistPendingIdentity(device, 1L) bound.completePendingIdentity(device) bound.upsertPairedDevice(device) bound.hasDeviceIdentityCollision(device) bound.rotateSourceIdentity() bound.vaultSnapshot() bound.outgoingDeviceManifest() bound.markSynced(device.deviceId) bound.duressSlotJson() assertEquals(true, bound.onConsent(requestV2)) // The v2 consent must reach the prompt. This is the defect that motivated // §10.5: a `true` here without calling through loses the user's SAS. assertEquals("the v2 consent must carry the session SAS", "12345678", confirmedSas) assertEquals( "the v2 consent must hand the human the requesting device's label", "receiver-id", confirmedRequest?.sessionRequestId, ) // v1 consent is answered by the caller, which has already run its own SAS // prompt before the grant; the binder must not prompt a second time. assertFalse("the v1 consent is answered locally", probe.calls.contains("onConsentV1")) // Exhaustive: a member added to the interface and not exercised here // fails this, rather than surfacing once a peer triggers it. val declared = AdoptionSourceV2Deps::class.java.methods .map { it.name } .filterNot { it in OBJECT_METHODS } .toSet() val covered = probe.calls .filterNot { it == "onConsentV1" || it == "onConsentV2" } .toSet() + setOf("onConsent", "getSourceIdentity", "getPairState", "snapshotCapable") assertEquals( "every AdoptionSourceV2Deps member must be covered by this test", declared, covered, ) } private companion object { val OBJECT_METHODS = setOf("equals", "hashCode", "toString") } }