Implement Rust-owned Android local and HTTPS onboarding (#95)
This commit is contained in:
1 parent
66b1d4a4c4
commit
36e5f55283
46 files changed
+3002
-79
No files matched your search
@@ -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,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)
|
||||
}
|
||||
Reference in new issue
Block a user