Implement Android TOTP cached discovery and search (#110)
This commit is contained in:
1 parent
0c8366d34f
commit
f4a6edbc68
18 files changed
+1070
-165
No files matched your search
@@ -13,6 +13,7 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.SemanticsActions
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.hasContentDescription
|
||||
import androidx.compose.ui.test.hasSetTextAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.accessibility.enableAccessibilityChecks
|
||||
@@ -70,19 +71,27 @@ class EntryViewerTest {
|
||||
}
|
||||
compose.onNodeWithText("654321").assertDoesNotExist()
|
||||
compose.onNodeWithText("Unlock").performClick()
|
||||
compose.onNodeWithText("alice 日本語").performScrollTo().assertIsDisplayed()
|
||||
compose.onNodeWithText("123 456").performScrollTo().assertIsDisplayed()
|
||||
compose.onNodeWithContentDescription("Reveal PIN").performScrollTo()
|
||||
compose.onNodeWithTag("entry-detail").performScrollToNode(hasText("alice 日本語"))
|
||||
compose.onNodeWithText("alice 日本語").assertIsDisplayed()
|
||||
compose.onNodeWithTag("entry-detail").performScrollToNode(hasText("123 456"))
|
||||
compose.onNodeWithText("123 456").assertIsDisplayed()
|
||||
compose.onNodeWithTag(
|
||||
"entry-detail"
|
||||
).performScrollToNode(hasContentDescription("Reveal PIN"))
|
||||
compose.onNodeWithContentDescription("Reveal PIN")
|
||||
.performSemanticsAction(SemanticsActions.OnClick) { org.junit.Assert.assertTrue(it()) }
|
||||
assertEquals(2uL, repository.lastId)
|
||||
compose.onNodeWithTag("entry-detail").performScrollToNode(hasText("654321"))
|
||||
compose.onNodeWithText("654321").assertIsDisplayed()
|
||||
compose.onNodeWithContentDescription("Hide PIN").performScrollTo().performClick()
|
||||
compose.onNodeWithTag("entry-detail").performScrollToNode(hasContentDescription("Hide PIN"))
|
||||
compose.onNodeWithContentDescription("Hide PIN").performClick()
|
||||
compose.onNodeWithText("654321").assertDoesNotExist()
|
||||
compose.onNodeWithContentDescription("Copy PIN").performScrollTo()
|
||||
compose.onNodeWithTag("entry-detail").performScrollToNode(hasContentDescription("Copy PIN"))
|
||||
compose.onNodeWithContentDescription("Copy PIN")
|
||||
.performSemanticsAction(SemanticsActions.OnClick) { org.junit.Assert.assertTrue(it()) }
|
||||
assertEquals(2uL, repository.lastId)
|
||||
compose.onNodeWithContentDescription("Edit PIN").performScrollTo().performClick()
|
||||
compose.onNodeWithTag("entry-detail").performScrollToNode(hasContentDescription("Edit PIN"))
|
||||
compose.onNodeWithContentDescription("Edit PIN").performClick()
|
||||
compose.onNode(hasSetTextAction()).performTextReplacement("987654")
|
||||
compose.onNodeWithText("Save").performClick()
|
||||
assertEquals("987654", repository.saved)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package de.rfc1437.ironstorage
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import dagger.hilt.EntryPoints
|
||||
import de.rfc1437.ironstorage.core.EditorFieldKind
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.junit.Assume.assumeTrue
|
||||
import org.junit.Test
|
||||
|
||||
class TotpDiscoveryFixtureTest {
|
||||
/** Public test entry only, guarded by the retained fixture's exact key identity. */
|
||||
@Test fun addPublicTotpForComputerUse(): Unit = runBlocking {
|
||||
assumeTrue(InstrumentationRegistry.getArguments().getString("totpFixtureSetup") == "true")
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val access = EntryPoints.get(context.applicationContext, ProviderFixtureAccess::class.java)
|
||||
val setup = access.onboarding()
|
||||
withContext(Dispatchers.Main.immediate) { setup.refresh() }
|
||||
val before = withTimeout(30000) { setup.setup.first { !it.busy } }
|
||||
check(
|
||||
before.configured &&
|
||||
before.keys.single().fingerprint == "0C1ABFD707B2103E495B42ACF34D59DF60521FE3"
|
||||
)
|
||||
val catalog = access.totp()
|
||||
withContext(Dispatchers.Main.immediate) { catalog.enter(null, null) }
|
||||
val cached = withTimeout(30000) { catalog.state.first { !it.loading } }
|
||||
check(cached.error == null)
|
||||
if (cached.rows.any { it.path == "discovery-fixture" }) {
|
||||
withContext(Dispatchers.Main.immediate) { access.authentication().lock() }
|
||||
return@runBlocking
|
||||
}
|
||||
val editor = access.editor()
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
editor.create("", "discovery-fixture", "public-provider-fixture-passphrase")
|
||||
}
|
||||
val created = withTimeout(30000) { editor.state.first { !it.busy } }
|
||||
check(created.open && !created.locked && created.error == null) { created.error.orEmpty() }
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
editor.add(
|
||||
EditorFieldKind.OtpUri,
|
||||
null,
|
||||
"otpauth://totp/Public:discovery?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=Public"
|
||||
)
|
||||
}
|
||||
withTimeout(30000) {
|
||||
editor.state.first {
|
||||
!it.busy &&
|
||||
it.fields.any { field -> field.kind == EditorFieldKind.OtpUri }
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main.immediate) { editor.save() }
|
||||
val saved = withTimeout(30000) { editor.state.first { !it.busy && !it.open } }
|
||||
check(saved.error == null)
|
||||
withContext(Dispatchers.Main.immediate) { access.authentication().lock() }
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,15 @@ import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performSemanticsAction
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import de.rfc1437.ironstorage.core.PasswordOtp
|
||||
import de.rfc1437.ironstorage.core.PasswordOtpKind
|
||||
import de.rfc1437.ironstorage.core.PasswordRepository
|
||||
import de.rfc1437.ironstorage.core.TotpDiscoveryPhase
|
||||
import de.rfc1437.ironstorage.core.TotpDiscoveryProgress
|
||||
import de.rfc1437.ironstorage.core.TotpRepository
|
||||
import de.rfc1437.ironstorage.core.TotpState
|
||||
import de.rfc1437.ironstorage.features.TotpScreen
|
||||
@@ -118,6 +121,27 @@ class TotpScreenTest {
|
||||
compose.onNodeWithTag("otp-code").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test fun discoveryShowsTypedProgressCancellationAndCachedQuery() {
|
||||
val fixture = Fixture()
|
||||
fixture.state.value = fixture.state.value.copy(
|
||||
refreshing = true,
|
||||
progress = TotpDiscoveryProgress(TotpDiscoveryPhase.Inspecting, 10, 4, 3, 2, 1)
|
||||
)
|
||||
display(fixture)
|
||||
compose.onNodeWithText("Inspecting entries").assertIsDisplayed()
|
||||
compose.onNodeWithText(
|
||||
"Inspected: 4 / 10 · Cache hits: 3 · Matches: 2 · Unavailable: 1"
|
||||
).assertIsDisplayed()
|
||||
compose.onNodeWithTag("totp-progress").assertIsDisplayed()
|
||||
compose.onNodeWithTag("totp-query").performTextInput("cached")
|
||||
assertEquals("cached", fixture.state.value.query)
|
||||
compose.onNodeWithText("Cancel").performClick()
|
||||
compose.onNodeWithText(
|
||||
"Cancelled. Completed checkpoints are retained; refresh to resume."
|
||||
).assertIsDisplayed()
|
||||
compose.onNodeWithText("Apple Watch").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test fun inspectDisposableTotpWithComputerUse() {
|
||||
val arguments = InstrumentationRegistry.getArguments()
|
||||
assumeTrue(arguments.getString("otpInspect") == "true")
|
||||
@@ -194,6 +218,9 @@ class TotpScreenTest {
|
||||
}
|
||||
override fun enter(selected: String?, revision: String?) = Unit
|
||||
override fun reload() = Unit
|
||||
override fun query(value: String) {
|
||||
state.value = state.value.copy(query = value)
|
||||
}
|
||||
override fun open(id: String, revision: String) {
|
||||
state.value =
|
||||
state.value.copy(detail = true)
|
||||
@@ -203,7 +230,12 @@ class TotpScreenTest {
|
||||
state.value = state.value.copy(detail = false)
|
||||
}
|
||||
override fun discover(passphrase: String) = Unit
|
||||
override fun cancel() = Unit
|
||||
override fun cancel() {
|
||||
state.value = state.value.copy(
|
||||
refreshing = false,
|
||||
progress = state.value.progress?.copy(phase = TotpDiscoveryPhase.Cancelled)
|
||||
)
|
||||
}
|
||||
override fun leave() = viewer.leave()
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@ package de.rfc1437.ironstorage
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import de.rfc1437.ironstorage.core.EditorRepository
|
||||
import de.rfc1437.ironstorage.core.NativeAuthenticationRepository
|
||||
import de.rfc1437.ironstorage.core.NativeCredentialRepository
|
||||
import de.rfc1437.ironstorage.core.OnboardingRepository
|
||||
import de.rfc1437.ironstorage.core.TotpRepository
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@@ -13,4 +15,6 @@ interface ProviderFixtureAccess {
|
||||
fun onboarding(): OnboardingRepository
|
||||
fun authentication(): NativeAuthenticationRepository
|
||||
fun credentials(): NativeCredentialRepository
|
||||
fun editor(): EditorRepository
|
||||
fun totp(): TotpRepository
|
||||
}
|
||||
Reference in new issue
Block a user