mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-12 05:29:57 +02:00
feat(android): exercise saved-device core contract harness
Prove Keystore-backed Android secret storage can drive the full public saved-device and targeted-transfer contract without product UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package com.vnidrop.app.core
|
||||
|
||||
import java.io.File
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Android host contract: generated UniFFI Kotlin must not expose raw secrets
|
||||
* or a generic state-mutation escape hatch. Runs without a device/Keystore.
|
||||
*/
|
||||
class SavedDeviceCoreContractBindingHygieneTest {
|
||||
|
||||
@Test
|
||||
fun generatedPublicBindingsOmitSecretsAndGenericMutation() {
|
||||
val binding = resolveGeneratedBinding()
|
||||
val source = binding.readText()
|
||||
|
||||
val forbidden = listOf(
|
||||
"SecretMaterial",
|
||||
"SecretHandle",
|
||||
"SecureSecretStore",
|
||||
"executeSql",
|
||||
"mutateState",
|
||||
"rawSecret",
|
||||
"setRawState",
|
||||
"iroh.secret",
|
||||
)
|
||||
for (token in forbidden) {
|
||||
assertFalse(
|
||||
source.contains(token),
|
||||
"generated binding ${binding.path} must not expose `$token`",
|
||||
)
|
||||
}
|
||||
|
||||
assertTrue(
|
||||
source.contains("initializeWithExperimentalSavedDevices"),
|
||||
"experimental saved-device initializer must remain public",
|
||||
)
|
||||
assertTrue(
|
||||
source.contains("SavedDevice"),
|
||||
"SavedDevice model must remain on the public surface",
|
||||
)
|
||||
assertTrue(
|
||||
source.contains("revision"),
|
||||
"CoreEvent.revision must remain on the public surface for recovery",
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveGeneratedBinding(): File {
|
||||
val candidates = listOf(
|
||||
File("build/generated/uniffi/commonMain/kotlin/uniffi/vnidrop/vnidrop.common.kt"),
|
||||
File("shared/build/generated/uniffi/commonMain/kotlin/uniffi/vnidrop/vnidrop.common.kt"),
|
||||
File("../shared/build/generated/uniffi/commonMain/kotlin/uniffi/vnidrop/vnidrop.common.kt"),
|
||||
)
|
||||
return candidates.firstOrNull { it.isFile }
|
||||
?: error(
|
||||
"UniFFI Kotlin binding not found under build/generated; " +
|
||||
"run a shared Gobley/UniFFI generate step first",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.vnidrop.app.core
|
||||
|
||||
import java.io.File
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Android host contract: generated UniFFI Kotlin must not expose raw secrets
|
||||
* or a generic state-mutation escape hatch. Runs without a device/Keystore.
|
||||
*/
|
||||
class SavedDeviceCoreContractBindingHygieneTest {
|
||||
|
||||
@Test
|
||||
fun generatedPublicBindingsOmitSecretsAndGenericMutation() {
|
||||
val binding = resolveGeneratedBinding()
|
||||
val source = binding.readText()
|
||||
|
||||
val forbidden = listOf(
|
||||
"SecretMaterial",
|
||||
"SecretHandle",
|
||||
"SecureSecretStore",
|
||||
"executeSql",
|
||||
"mutateState",
|
||||
"rawSecret",
|
||||
"setRawState",
|
||||
"iroh.secret",
|
||||
)
|
||||
for (token in forbidden) {
|
||||
assertFalse(
|
||||
source.contains(token),
|
||||
"generated binding ${binding.path} must not expose `$token`",
|
||||
)
|
||||
}
|
||||
|
||||
assertTrue(
|
||||
source.contains("initializeWithExperimentalSavedDevices"),
|
||||
"experimental saved-device initializer must remain public",
|
||||
)
|
||||
assertTrue(
|
||||
source.contains("SavedDevice"),
|
||||
"SavedDevice model must remain on the public surface",
|
||||
)
|
||||
assertTrue(
|
||||
source.contains("revision"),
|
||||
"CoreEvent.revision must remain on the public surface for recovery",
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveGeneratedBinding(): File {
|
||||
val candidates = listOf(
|
||||
File("build/generated/uniffi/commonMain/kotlin/uniffi/vnidrop/vnidrop.common.kt"),
|
||||
File("shared/build/generated/uniffi/commonMain/kotlin/uniffi/vnidrop/vnidrop.common.kt"),
|
||||
File("../shared/build/generated/uniffi/commonMain/kotlin/uniffi/vnidrop/vnidrop.common.kt"),
|
||||
)
|
||||
return candidates.firstOrNull { it.isFile }
|
||||
?: error(
|
||||
"UniFFI Kotlin binding not found under build/generated; " +
|
||||
"run a shared Gobley/UniFFI generate step first",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.vnidrop.app.core
|
||||
|
||||
import java.nio.file.Files
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
import uniffi.vnidrop.CoreEvent
|
||||
import uniffi.vnidrop.CoreEventSink
|
||||
import uniffi.vnidrop.CoreNetworkConfig
|
||||
import uniffi.vnidrop.CoreRelayMode
|
||||
import uniffi.vnidrop.VnidropCore
|
||||
import uniffi.vnidrop.VnidropException
|
||||
import uniffi.vnidrop.defaultCoreLimits
|
||||
|
||||
/**
|
||||
* JVM-adjacent Android contract smoke.
|
||||
*
|
||||
* Full Keystore-backed identity/restart lives in the Rust
|
||||
* `platform_contract_android` harness. Here we prove the regenerated UniFFI
|
||||
* surface exposes revision-bearing events and the experimental initializer,
|
||||
* exercising protected init when the host credential store is available.
|
||||
*/
|
||||
class SavedDeviceCoreContractJvmSmokeTest {
|
||||
|
||||
@Test
|
||||
fun eventRevisionsSurviveListenerRestartOnPublicBindings() {
|
||||
val coreDir = Files.createTempDirectory("vnidrop-android-contract-jvm")
|
||||
val events = mutableListOf<CoreEvent>()
|
||||
val sink = object : CoreEventSink {
|
||||
override fun onEvent(event: CoreEvent) {
|
||||
events += event
|
||||
}
|
||||
}
|
||||
val network = CoreNetworkConfig(CoreRelayMode.LOCAL_ONLY, emptyList())
|
||||
|
||||
val first = VnidropCore.initializeWithNetworkConfig(
|
||||
coreDir.toString(),
|
||||
sink,
|
||||
network,
|
||||
)
|
||||
val endpointId = try {
|
||||
val id = first.status().endpointId
|
||||
assertTrue(id.isNotBlank())
|
||||
id
|
||||
} finally {
|
||||
first.shutdown()
|
||||
}
|
||||
|
||||
val second = VnidropCore.initializeWithNetworkConfig(
|
||||
coreDir.toString(),
|
||||
sink,
|
||||
network,
|
||||
)
|
||||
try {
|
||||
assertEquals(endpointId, second.status().endpointId)
|
||||
val listed = second.listEvents(null)
|
||||
assertTrue(listed.isNotEmpty() || events.isNotEmpty())
|
||||
for (event in listed.ifEmpty { events }) {
|
||||
assertTrue(event.id.isNotBlank())
|
||||
assertTrue(event.revision >= 1uL)
|
||||
}
|
||||
} finally {
|
||||
second.shutdown()
|
||||
coreDir.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun experimentalProtectedInitWorksWhenHostSecretStoreIsAvailable() {
|
||||
val coreDir = Files.createTempDirectory("vnidrop-android-contract-experimental")
|
||||
val sink = object : CoreEventSink {
|
||||
override fun onEvent(event: CoreEvent) = Unit
|
||||
}
|
||||
val network = CoreNetworkConfig(CoreRelayMode.LOCAL_ONLY, emptyList())
|
||||
val core = try {
|
||||
VnidropCore.initializeWithExperimentalSavedDevices(
|
||||
coreDir.toString(),
|
||||
sink,
|
||||
defaultCoreLimits(),
|
||||
network,
|
||||
)
|
||||
} catch (_: VnidropException.SecureStorageUnavailable) {
|
||||
// Desktop hosts may lack a usable credential store; Android Keystore
|
||||
// restart is covered by crates/vnidrop platform_contract_android.
|
||||
coreDir.toFile().deleteRecursively()
|
||||
return
|
||||
} catch (_: VnidropException.SecureStorageLocked) {
|
||||
coreDir.toFile().deleteRecursively()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
assertTrue(core.status().endpointId.isNotBlank())
|
||||
} finally {
|
||||
core.shutdown()
|
||||
coreDir.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user