Implement Rust-owned Android local and HTTPS onboarding (#95)
Android ARM verification / rust (push) Failing after 46m9s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Successful in 21s

This commit is contained in:
hugo committed 2026-10-09 11:31:50 +02:00
1 parent 66b1d4a4c4
commit 36e5f55283
46 files changed
+3002 -79

No files matched your search

+1
View File
@@ -311,4 +311,5 @@ org.ow2.asm:asm-tree:9.9.1=composeMappingProducerClasspath
org.ow2.asm:asm:9.9=androidLintTool
org.ow2.asm:asm:9.9.1=composeMappingProducerClasspath
org.slf4j:slf4j-api:2.0.7=ktlint
rustls:rustls-platform-verifier:0.1.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
empty=_agp_internal_debugAndroidTest_kspClasspath,_agp_internal_debugUnitTest_kspClasspath,androidApis,androidJdkImage,androidTestUtil,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAnnotationProcessorClasspath,debugImplementationDependenciesMetadata,debugReverseMetadataValues,debugUnitTestAnnotationProcessorClasspath,debugUnitTestImplementationDependenciesMetadata,hiltCompileOnlyDebug,hiltCompileOnlyDebugAndroidTest,hiltCompileOnlyDebugUnitTest,hiltCompileOnlyRelease,kotlinCompilerPluginClasspath,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,lintChecks,lintPublish,releaseAnnotationProcessorClasspath,releaseImplementationDependenciesMetadata,releaseReverseMetadataValues
@@ -0,0 +1,99 @@
package de.rfc1437.ironstorage
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextInput
import de.rfc1437.ironstorage.core.OnboardingRepository
import de.rfc1437.ironstorage.core.SetupKey
import de.rfc1437.ironstorage.core.SetupStatus
import de.rfc1437.ironstorage.features.OnboardingScreen
import de.rfc1437.ironstorage.features.SetupFormViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
class OnboardingScreenTest {
@get:Rule val compose = createAndroidComposeRule<MainActivity>()
@Test fun localAndRemoteFormsUseTypedFacadeAndClearSubmittedSecrets() {
val repository = FixtureRepository()
val secrets = SetupFormViewModel()
compose.activity.runOnUiThread {
compose.activity.setContent { MaterialTheme { OnboardingScreen(repository, secrets) } }
}
compose.waitForIdle()
compose.onNodeWithText("Create local store").performClick()
compose.onNodeWithText("Generate GPG key").performScrollTo().performClick()
compose.onNodeWithText(
"Key name and email"
).performTextInput("Fixture <fixture@example.test>")
compose.onNodeWithText("GPG passphrase").performTextInput("public-test-passphrase")
compose.onNodeWithText("Generate GPG key").performScrollTo().performClick()
compose.onNodeWithText("Create encrypted local store").performScrollTo().performClick()
compose.onNodeWithText("Add HTTPS synchronization").assertIsDisplayed().performClick()
compose.onNodeWithText("HTTPS server URL").performTextInput("https://example.test")
compose.onNodeWithText("Account").performTextInput("alice")
compose.onNodeWithText(
"Repository path (owner/repository)"
).performTextInput("team/passwords")
compose.onNodeWithText(
"Application token"
).performScrollTo().performTextInput("public-test-token")
compose.onNodeWithText("Connect local store").performScrollTo().performClick()
assertEquals(listOf("generate", "local", "connect"), repository.calls)
assertTrue(
secrets.passphrase.isEmpty() && secrets.token.isEmpty() && secrets.armor.isEmpty()
)
}
private class FixtureRepository : OnboardingRepository {
override val setup = MutableStateFlow(SetupStatus())
val calls = mutableListOf<String>()
override fun refresh() {
setup.value = setup.value.copy(branches = emptyList())
}
override fun generate(userId: String, passphrase: String) {
assertEquals("Fixture <fixture@example.test>", userId)
assertEquals("public-test-passphrase", passphrase)
calls.add("generate")
setup.value =
setup.value.copy(
keys = listOf(SetupKey("fixture-key", listOf(userId))),
selectedKey = "fixture-key"
)
}
override fun createLocal(fingerprint: String, passphrase: String) {
assertEquals("fixture-key", fingerprint)
assertEquals("public-test-passphrase", passphrase)
calls.add("local")
setup.value = setup.value.copy(configured = true)
}
override fun connect(server: String, account: String, repository: String, token: String) {
assertEquals("https://example.test", server)
assertEquals("alice", account)
assertEquals("team/passwords", repository)
assertEquals("public-test-token", token)
calls.add("connect")
setup.value = setup.value.copy(connected = true)
}
override fun importKey(armor: String, passphrase: String) = error("Unexpected import")
override fun discover(server: String, account: String, repository: String, token: String) =
error("Unexpected discovery")
override fun cloneStore(
branch: String,
fingerprint: String,
passphrase: String,
useExisting: Boolean
) = error("Unexpected clone")
override fun replaceToken(account: String, token: String) =
error("Unexpected token replacement")
override fun cancel() = Unit
}
}
@@ -12,11 +12,16 @@ import androidx.compose.ui.Modifier
import de.rfc1437.ironstorage.core.NativeFixtureShellRepository
import de.rfc1437.ironstorage.core.ShellRepository
import de.rfc1437.ironstorage.core.ShellState
import de.rfc1437.ironstorage.features.SetupFormViewModel
import de.rfc1437.ironstorage.features.ShellScreen
import de.rfc1437.ironstorage.features.ShellViewModel
@Composable
fun VariantContent(viewModel: ShellViewModel, repository: ShellRepository) {
fun VariantContent(
viewModel: ShellViewModel,
repository: ShellRepository,
setupForm: SetupFormViewModel
) {
Column(Modifier.statusBarsPadding()) {
Row(Modifier.horizontalScroll(rememberScrollState())) {
ShellState.entries.forEach { state ->
@@ -27,6 +32,6 @@ fun VariantContent(viewModel: ShellViewModel, repository: ShellRepository) {
}
}
}
ShellScreen(viewModel)
ShellScreen(viewModel, setupForm)
}
}
+1
View File
@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<application
android:name=".IronStorageApplication"
@@ -8,6 +8,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.fragment.app.FragmentActivity
import dagger.hilt.android.AndroidEntryPoint
import de.rfc1437.ironstorage.core.ShellRepository
import de.rfc1437.ironstorage.features.SetupFormViewModel
import de.rfc1437.ironstorage.features.ShellViewModel
import de.rfc1437.ironstorage.platform.AndroidKeystoreAdapter
import javax.inject.Inject
@@ -18,6 +19,8 @@ class MainActivity : FragmentActivity() {
@Inject lateinit var repository: ShellRepository
private val setupForm: SetupFormViewModel by viewModels()
private val viewModel: ShellViewModel by viewModels()
override fun onDestroy() {
@@ -31,7 +34,7 @@ class MainActivity : FragmentActivity() {
enableEdgeToEdge()
setContent {
MaterialTheme {
VariantContent(viewModel, repository)
VariantContent(viewModel, repository, setupForm)
}
}
}
@@ -7,6 +7,9 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import de.rfc1437.ironstorage.core.NativeFixtureShellRepository
import de.rfc1437.ironstorage.core.NativeOnboardingRepository
import de.rfc1437.ironstorage.core.NativePrivateSession
import de.rfc1437.ironstorage.core.OnboardingRepository
import de.rfc1437.ironstorage.core.ShellRepository
import de.rfc1437.ironstorage.platform.AndroidKeystoreAdapter
import de.rfc1437.ironstorage.platform.privateDirectories
@@ -18,6 +21,25 @@ import kotlinx.coroutines.SupervisorJob
@Module
@InstallIn(SingletonComponent::class)
object ShellModule {
@Provides
@Singleton
fun privateSession(
@ApplicationContext context: Context,
keystore: AndroidKeystoreAdapter
): NativePrivateSession =
NativePrivateSession(context, { privateDirectories(context) }, keystore)
@Provides
@Singleton
fun onboardingRepository(
session: NativePrivateSession,
shell: ShellRepository
): OnboardingRepository = NativeOnboardingRepository(
session,
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
shell
)
@Provides
@Singleton
fun keystore(@ApplicationContext context: Context): AndroidKeystoreAdapter =
@@ -25,13 +47,9 @@ object ShellModule {
@Provides
@Singleton
fun shellRepository(
@ApplicationContext context: Context,
keystore: AndroidKeystoreAdapter
): ShellRepository = NativeFixtureShellRepository(
context,
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
{ privateDirectories(context) },
keystore
)
fun shellRepository(session: NativePrivateSession): ShellRepository =
NativeFixtureShellRepository(
session,
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
}
@@ -2,11 +2,16 @@ package de.rfc1437.ironstorage
import androidx.compose.runtime.Composable
import de.rfc1437.ironstorage.core.ShellRepository
import de.rfc1437.ironstorage.features.SetupFormViewModel
import de.rfc1437.ironstorage.features.ShellScreen
import de.rfc1437.ironstorage.features.ShellViewModel
@Composable
@Suppress("UNUSED_PARAMETER")
fun VariantContent(viewModel: ShellViewModel, repository: ShellRepository) {
ShellScreen(viewModel)
fun VariantContent(
viewModel: ShellViewModel,
repository: ShellRepository,
setupForm: SetupFormViewModel
) {
ShellScreen(viewModel, setupForm)
}