Implement Android Home activity and HTTPS synchronization (#97)
Android ARM verification / rust (push) Failing after 46m50s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Successful in 23s

This commit is contained in:
hugo committed 2026-10-09 14:45:57 +02:00
1 parent 8c9341f214
commit 5c933a99a3
29 files changed
+2008 -110

No files matched your search

@@ -0,0 +1,188 @@
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.assertIsNotEnabled
import androidx.compose.ui.test.hasScrollAction
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performScrollToNode
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeDown
import de.rfc1437.ironstorage.core.HomeAction
import de.rfc1437.ironstorage.core.HomeActionKind
import de.rfc1437.ironstorage.core.HomeChange
import de.rfc1437.ironstorage.core.HomeCommit
import de.rfc1437.ironstorage.core.HomeFailure
import de.rfc1437.ironstorage.core.HomeFailureKind
import de.rfc1437.ironstorage.core.HomeFreshness
import de.rfc1437.ironstorage.core.HomeNotice
import de.rfc1437.ironstorage.core.HomePage
import de.rfc1437.ironstorage.core.HomeRepository
import de.rfc1437.ironstorage.core.HomeRow
import de.rfc1437.ironstorage.core.HomeState
import de.rfc1437.ironstorage.core.ShellIcon
import de.rfc1437.ironstorage.features.HomeScreen
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
class HomeScreenTest {
@get:Rule val compose = createAndroidComposeRule<MainActivity>()
private fun show(repository: Fixture) {
compose.activity.runOnUiThread {
compose.activity.setContent { MaterialTheme { HomeScreen(repository) } }
}
}
@Test fun typedActionsUseTheConfiguredIdentityAndNeverReplayAfterReload() {
val repository = Fixture()
show(repository)
compose.onNodeWithText("Cached activity").assertIsDisplayed()
compose.onAllNodesWithText("Commit")[0].performScrollTo().performClick()
compose.onNodeWithText("Android fixture").assertIsDisplayed()
compose.onNodeWithText("fixture@example.test").assertIsDisplayed()
compose.onNodeWithText("Commit message").performTextInput("Commit encrypted objects")
compose.onNodeWithText("Create commit").performClick()
assertEquals(
HomeActionKind.Commit to "Commit encrypted objects",
repository.requests.single()
)
compose.onNodeWithText("Cancel").performClick()
for (action in listOf(HomeActionKind.Fetch, HomeActionKind.Pull, HomeActionKind.Push)) {
compose.onNodeWithText(action.name).performScrollTo().performClick()
assertEquals(action, repository.requests.last().first)
compose.onNodeWithText("Cancel").performClick()
compose.onNodeWithText("Reload status").performClick()
}
val before = repository.requests.size
show(repository)
compose.waitForIdle()
assertEquals(before, repository.requests.size)
assertTrue(repository.entries > 0)
}
@Test fun slowOperationsLeaveScrollingAndCancellationResponsiveWithLargeHistory() {
val repository = Fixture(1000)
show(repository)
compose.onNodeWithText("Fetch").performScrollTo().performClick()
compose.onNodeWithText("Receiving remote objects").assertIsDisplayed()
compose.onNodeWithText("Fetch").performScrollTo().assertIsNotEnabled()
compose.onNode(hasScrollAction()).performScrollToNode(hasText("object-999"))
compose.onNodeWithText("object-999").assertIsDisplayed()
compose.onNodeWithText("Cancel").performClick()
compose.onNode(hasScrollAction()).performScrollToNode(hasText("Refresh Interrupted"))
compose.onNodeWithText("Refresh Interrupted").assertIsDisplayed()
assertEquals(1, repository.cancellations)
}
@Test fun swipeRefreshRequestsTheRustRefreshWorkflow() {
val repository = Fixture()
show(repository)
compose.onRoot().performTouchInput { swipeDown() }
compose.waitUntil(5000) { repository.refreshes == 1 }
assertTrue(repository.requests.isEmpty())
compose.onNodeWithText("Cancel").performClick()
}
private class Fixture(changes: Int = 1) : HomeRepository {
private val actions = HomeActionKind.entries.map {
HomeAction(
it,
it.name,
when (it) {
HomeActionKind.Commit -> ShellIcon.Success
HomeActionKind.Fetch -> ShellIcon.Refresh
HomeActionKind.Pull -> ShellIcon.Incoming
HomeActionKind.Push -> ShellIcon.Outgoing
}
)
}
override val state = MutableStateFlow(
HomeState(
page = HomePage(
HomeFreshness.Cached,
1234567890,
listOf(
HomeRow(
"tracked",
"origin/main",
"Tracked HTTPS branch",
ShellIcon.Branch,
actions
)
),
listOf(
HomeCommit(
"incoming-id",
"Incoming encrypted objects",
"Fixture author",
1234567890,
(0 until changes).map {
HomeChange(
"change-$it",
"object-$it",
"Added password entry",
ShellIcon.Key
)
},
emptyList()
)
),
emptyList(),
1u,
0u,
null
),
name = "Android fixture",
email = "fixture@example.test"
)
)
val requests = mutableListOf<Pair<HomeActionKind, String>>()
var cancellations = 0
var entries = 0
var refreshes = 0
override fun enter() {
entries++
}
override fun refresh() {
refreshes++
busy()
}
override fun perform(action: HomeActionKind, message: String) {
requests += action to message
busy()
}
private fun busy() {
state.value = state.value.copy(
busy = true,
failure = null,
progress = HomeNotice("Receiving remote objects", "Slow fixture network")
)
}
override fun reload() {
state.value = state.value.copy(failure = null)
}
override fun cancel() {
cancellations++
state.value = state.value.copy(
busy = false,
progress = null,
failure = HomeFailure(
HomeFailureKind.Interrupted,
"Refresh Interrupted",
"Reload cached activity before retrying."
)
)
}
override fun leave() = Unit
}
}
@@ -6,7 +6,9 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import de.rfc1437.ironstorage.core.HomeRepository
import de.rfc1437.ironstorage.core.NativeFixtureShellRepository
import de.rfc1437.ironstorage.core.NativeHomeRepository
import de.rfc1437.ironstorage.core.NativeOnboardingRepository
import de.rfc1437.ironstorage.core.NativePasswordRepository
import de.rfc1437.ironstorage.core.NativePrivateSession
@@ -23,6 +25,18 @@ import kotlinx.coroutines.SupervisorJob
@Module
@InstallIn(SingletonComponent::class)
object ShellModule {
@Provides
@Singleton
fun homeRepository(
session: NativePrivateSession,
passwords: PasswordRepository
): HomeRepository = NativeHomeRepository(
session,
passwords,
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
Dispatchers.IO.limitedParallelism(2)
)
@Provides
@Singleton
fun passwordRepository(session: NativePrivateSession): PasswordRepository =