Refactor coroutine testing setup (#1583)
* coroutine-utils: init * coroutine-utils-testing: init * format-common: switch over to using DispatcherProvider * Convert Binds method to an extension function * Add Dispatcher module
This commit is contained in:
parent
933558caf8
commit
8db0b67ce9
14 changed files with 173 additions and 13 deletions
|
@ -74,6 +74,7 @@ dependencies {
|
||||||
implementation(libs.androidx.annotation)
|
implementation(libs.androidx.annotation)
|
||||||
coreLibraryDesugaring(libs.android.desugarJdkLibs)
|
coreLibraryDesugaring(libs.android.desugarJdkLibs)
|
||||||
implementation(projects.autofillParser)
|
implementation(projects.autofillParser)
|
||||||
|
implementation(projects.coroutineUtils)
|
||||||
implementation(projects.cryptoPgpainless)
|
implementation(projects.cryptoPgpainless)
|
||||||
implementation(projects.formatCommon)
|
implementation(projects.formatCommon)
|
||||||
implementation(projects.openpgpKtx)
|
implementation(projects.openpgpKtx)
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.msfjarvis.aps.injection.coroutines
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import dev.msfjarvis.aps.util.coroutines.DefaultDispatcherProvider
|
||||||
|
import dev.msfjarvis.aps.util.coroutines.DispatcherProvider
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface DispatcherModule {
|
||||||
|
@Binds fun DefaultDispatcherProvider.bind(): DispatcherProvider
|
||||||
|
}
|
|
@ -15,5 +15,5 @@ import dev.msfjarvis.aps.util.totp.UriTotpFinder
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(ActivityComponent::class)
|
@InstallIn(ActivityComponent::class)
|
||||||
interface TotpModule {
|
interface TotpModule {
|
||||||
@Binds abstract fun bindTotpFinder(totpFinder: UriTotpFinder): TotpFinder
|
@Binds fun UriTotpFinder.bind(): TotpFinder
|
||||||
}
|
}
|
||||||
|
|
8
coroutine-utils-testing/api/coroutine-utils-testing.api
Normal file
8
coroutine-utils-testing/api/coroutine-utils-testing.api
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
public final class dev/msfjarvis/aps/test/CoroutineTestRule : org/junit/rules/TestWatcher {
|
||||||
|
public fun <init> ()V
|
||||||
|
public fun <init> (Lkotlinx/coroutines/test/TestDispatcher;)V
|
||||||
|
public synthetic fun <init> (Lkotlinx/coroutines/test/TestDispatcher;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
|
public final fun getTestDispatcher ()Lkotlinx/coroutines/test/TestDispatcher;
|
||||||
|
public final fun getTestDispatcherProvider ()Ldev/msfjarvis/aps/util/coroutines/DispatcherProvider;
|
||||||
|
}
|
||||||
|
|
14
coroutine-utils-testing/build.gradle.kts
Normal file
14
coroutine-utils-testing/build.gradle.kts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
kotlin("jvm")
|
||||||
|
id("com.github.android-password-store.kotlin-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.coroutineUtils)
|
||||||
|
implementation(libs.testing.junit)
|
||||||
|
implementation(libs.kotlin.coroutines.test)
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.msfjarvis.aps.test
|
||||||
|
|
||||||
|
import dev.msfjarvis.aps.util.coroutines.DispatcherProvider
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestCoroutineScheduler
|
||||||
|
import kotlinx.coroutines.test.TestDispatcher
|
||||||
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import org.junit.rules.TestWatcher
|
||||||
|
import org.junit.runner.Description
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JUnit [TestWatcher] to correctly handle setting and resetting a given [testDispatcher] for tests.
|
||||||
|
*/
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
public class CoroutineTestRule(
|
||||||
|
public val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(TestCoroutineScheduler()),
|
||||||
|
) : TestWatcher() {
|
||||||
|
|
||||||
|
public val testDispatcherProvider: DispatcherProvider =
|
||||||
|
object : DispatcherProvider {
|
||||||
|
override fun default(): CoroutineDispatcher = testDispatcher
|
||||||
|
override fun io(): CoroutineDispatcher = testDispatcher
|
||||||
|
override fun main(): CoroutineDispatcher = testDispatcher
|
||||||
|
override fun unconfined(): CoroutineDispatcher = testDispatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun starting(description: Description?) {
|
||||||
|
super.starting(description)
|
||||||
|
Dispatchers.setMain(testDispatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finished(description: Description?) {
|
||||||
|
super.finished(description)
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
}
|
||||||
|
}
|
22
coroutine-utils/api/coroutine-utils.api
Normal file
22
coroutine-utils/api/coroutine-utils.api
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
public final class dev/msfjarvis/aps/util/coroutines/DefaultDispatcherProvider : dev/msfjarvis/aps/util/coroutines/DispatcherProvider {
|
||||||
|
public fun <init> ()V
|
||||||
|
public fun default ()Lkotlinx/coroutines/CoroutineDispatcher;
|
||||||
|
public fun io ()Lkotlinx/coroutines/CoroutineDispatcher;
|
||||||
|
public fun main ()Lkotlinx/coroutines/CoroutineDispatcher;
|
||||||
|
public fun unconfined ()Lkotlinx/coroutines/CoroutineDispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract interface class dev/msfjarvis/aps/util/coroutines/DispatcherProvider {
|
||||||
|
public abstract fun default ()Lkotlinx/coroutines/CoroutineDispatcher;
|
||||||
|
public abstract fun io ()Lkotlinx/coroutines/CoroutineDispatcher;
|
||||||
|
public abstract fun main ()Lkotlinx/coroutines/CoroutineDispatcher;
|
||||||
|
public abstract fun unconfined ()Lkotlinx/coroutines/CoroutineDispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class dev/msfjarvis/aps/util/coroutines/DispatcherProvider$DefaultImpls {
|
||||||
|
public static fun default (Ldev/msfjarvis/aps/util/coroutines/DispatcherProvider;)Lkotlinx/coroutines/CoroutineDispatcher;
|
||||||
|
public static fun io (Ldev/msfjarvis/aps/util/coroutines/DispatcherProvider;)Lkotlinx/coroutines/CoroutineDispatcher;
|
||||||
|
public static fun main (Ldev/msfjarvis/aps/util/coroutines/DispatcherProvider;)Lkotlinx/coroutines/CoroutineDispatcher;
|
||||||
|
public static fun unconfined (Ldev/msfjarvis/aps/util/coroutines/DispatcherProvider;)Lkotlinx/coroutines/CoroutineDispatcher;
|
||||||
|
}
|
||||||
|
|
13
coroutine-utils/build.gradle.kts
Normal file
13
coroutine-utils/build.gradle.kts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
kotlin("jvm")
|
||||||
|
id("com.github.android-password-store.kotlin-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.kotlin.coroutines.core)
|
||||||
|
implementation(libs.dagger.hilt.core)
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.msfjarvis.aps.util.coroutines
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
|
/** Interface to allow abstracting individual [CoroutineDispatcher]s out of class dependencies. */
|
||||||
|
public interface DispatcherProvider {
|
||||||
|
|
||||||
|
public fun main(): CoroutineDispatcher = Dispatchers.Main
|
||||||
|
public fun default(): CoroutineDispatcher = Dispatchers.Default
|
||||||
|
public fun io(): CoroutineDispatcher = Dispatchers.IO
|
||||||
|
public fun unconfined(): CoroutineDispatcher = Dispatchers.Unconfined
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Concrete type for [DispatcherProvider] with all the defaults from the class. */
|
||||||
|
public class DefaultDispatcherProvider @Inject constructor() : DispatcherProvider
|
|
@ -1,5 +1,5 @@
|
||||||
public final class dev/msfjarvis/aps/data/passfile/PasswordEntry {
|
public final class dev/msfjarvis/aps/data/passfile/PasswordEntry {
|
||||||
public fun <init> (Ldev/msfjarvis/aps/util/time/UserClock;Ldev/msfjarvis/aps/util/totp/TotpFinder;Lkotlinx/coroutines/CoroutineScope;[B)V
|
public fun <init> (Ldev/msfjarvis/aps/util/time/UserClock;Ldev/msfjarvis/aps/util/totp/TotpFinder;Ldev/msfjarvis/aps/util/coroutines/DispatcherProvider;Lkotlinx/coroutines/CoroutineScope;[B)V
|
||||||
public final fun getExtraContent ()Ljava/util/Map;
|
public final fun getExtraContent ()Ljava/util/Map;
|
||||||
public final fun getExtraContentString ()Ljava/lang/String;
|
public final fun getExtraContentString ()Ljava/lang/String;
|
||||||
public final fun getExtraContentWithoutAuthData ()Ljava/lang/String;
|
public final fun getExtraContentWithoutAuthData ()Ljava/lang/String;
|
||||||
|
|
|
@ -9,11 +9,13 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(projects.coroutineUtils)
|
||||||
implementation(libs.androidx.annotation)
|
implementation(libs.androidx.annotation)
|
||||||
implementation(libs.dagger.hilt.core)
|
implementation(libs.dagger.hilt.core)
|
||||||
implementation(libs.thirdparty.commons.codec)
|
implementation(libs.thirdparty.commons.codec)
|
||||||
implementation(libs.thirdparty.kotlinResult)
|
implementation(libs.thirdparty.kotlinResult)
|
||||||
implementation(libs.kotlin.coroutines.core)
|
implementation(libs.kotlin.coroutines.core)
|
||||||
|
testImplementation(projects.coroutineUtilsTesting)
|
||||||
testImplementation(libs.bundles.testDependencies)
|
testImplementation(libs.bundles.testDependencies)
|
||||||
testImplementation(libs.kotlin.coroutines.test)
|
testImplementation(libs.kotlin.coroutines.test)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import com.github.michaelbull.result.mapBoth
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import dev.msfjarvis.aps.util.coroutines.DispatcherProvider
|
||||||
import dev.msfjarvis.aps.util.time.UserClock
|
import dev.msfjarvis.aps.util.time.UserClock
|
||||||
import dev.msfjarvis.aps.util.totp.Otp
|
import dev.msfjarvis.aps.util.totp.Otp
|
||||||
import dev.msfjarvis.aps.util.totp.TotpFinder
|
import dev.msfjarvis.aps.util.totp.TotpFinder
|
||||||
|
@ -32,6 +33,8 @@ constructor(
|
||||||
clock: UserClock,
|
clock: UserClock,
|
||||||
/** [TotpFinder] implementation to extract data from a TOTP URI */
|
/** [TotpFinder] implementation to extract data from a TOTP URI */
|
||||||
totpFinder: TotpFinder,
|
totpFinder: TotpFinder,
|
||||||
|
/** Instance of [DispatcherProvider] to select an IO dispatcher for emitting TOTP values. */
|
||||||
|
dispatcherProvider: DispatcherProvider,
|
||||||
/**
|
/**
|
||||||
* A cancellable [CoroutineScope] inside which we constantly emit new TOTP values as time elapses
|
* A cancellable [CoroutineScope] inside which we constantly emit new TOTP values as time elapses
|
||||||
*/
|
*/
|
||||||
|
@ -81,7 +84,7 @@ constructor(
|
||||||
username = findUsername()
|
username = findUsername()
|
||||||
totpSecret = totpFinder.findSecret(content)
|
totpSecret = totpFinder.findSecret(content)
|
||||||
if (totpSecret != null) {
|
if (totpSecret != null) {
|
||||||
scope.launch {
|
scope.launch(dispatcherProvider.io()) {
|
||||||
val digits = totpFinder.findDigits(content)
|
val digits = totpFinder.findDigits(content)
|
||||||
val totpPeriod = totpFinder.findPeriod(content)
|
val totpPeriod = totpFinder.findPeriod(content)
|
||||||
val totpAlgorithm = totpFinder.findAlgorithm(content)
|
val totpAlgorithm = totpFinder.findAlgorithm(content)
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
package dev.msfjarvis.aps.data.passfile
|
package dev.msfjarvis.aps.data.passfile
|
||||||
|
|
||||||
|
import dev.msfjarvis.aps.test.CoroutineTestRule
|
||||||
import dev.msfjarvis.aps.util.time.TestUserClock
|
import dev.msfjarvis.aps.util.time.TestUserClock
|
||||||
import dev.msfjarvis.aps.util.totp.TotpFinder
|
import dev.msfjarvis.aps.util.totp.TotpFinder
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
@ -16,15 +17,22 @@ import kotlin.test.assertNull
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
|
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
|
||||||
class PasswordEntryTest {
|
class PasswordEntryTest {
|
||||||
|
|
||||||
|
@get:Rule val coroutineTestRule: CoroutineTestRule = CoroutineTestRule()
|
||||||
private fun makeEntry(content: String) =
|
private fun makeEntry(content: String) =
|
||||||
PasswordEntry(fakeClock, testFinder, scope, content.encodeToByteArray())
|
PasswordEntry(
|
||||||
|
fakeClock,
|
||||||
|
testFinder,
|
||||||
|
coroutineTestRule.testDispatcherProvider,
|
||||||
|
TestScope(coroutineTestRule.testDispatcher),
|
||||||
|
content.encodeToByteArray(),
|
||||||
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testGetPassword() {
|
fun testGetPassword() {
|
||||||
|
@ -125,19 +133,20 @@ class PasswordEntryTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("Timing with runTest seems hard to implement right now")
|
@Ignore("Timing with runTest seems hard to implement right now")
|
||||||
fun testGeneratesOtpFromTotpUri() =
|
fun testGeneratesOtpFromTotpUri() {
|
||||||
scope.runTest {
|
runTest {
|
||||||
val entry = makeEntry("secret\nextra\n$TOTP_URI")
|
val entry = makeEntry("secret\nextra\n$TOTP_URI")
|
||||||
assertTrue(entry.hasTotp())
|
assertTrue(entry.hasTotp())
|
||||||
val code = entry.totp.value
|
val code = entry.totp.value
|
||||||
assertNotNull(code) { "Generated OTP cannot be null" }
|
assertNotNull(code) { "Generated OTP cannot be null" }
|
||||||
assertEquals("818800", code)
|
assertEquals("818800", code)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("Timing with runTest seems hard to implement right now")
|
@Ignore("Timing with runTest seems hard to implement right now")
|
||||||
fun testGeneratesOtpWithOnlyUriInFile() =
|
fun testGeneratesOtpWithOnlyUriInFile() {
|
||||||
scope.runTest {
|
runTest {
|
||||||
val entry = makeEntry(TOTP_URI)
|
val entry = makeEntry(TOTP_URI)
|
||||||
assertNull(entry.password)
|
assertNull(entry.password)
|
||||||
assertTrue(entry.hasTotp())
|
assertTrue(entry.hasTotp())
|
||||||
|
@ -145,6 +154,7 @@ class PasswordEntryTest {
|
||||||
assertNotNull(code) { "Generated OTP cannot be null" }
|
assertNotNull(code) { "Generated OTP cannot be null" }
|
||||||
assertEquals("818800", code)
|
assertEquals("818800", code)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testOnlyLooksForUriInFirstLine() {
|
fun testOnlyLooksForUriInFirstLine() {
|
||||||
|
@ -171,9 +181,6 @@ class PasswordEntryTest {
|
||||||
const val TOTP_URI =
|
const val TOTP_URI =
|
||||||
"otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30"
|
"otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30"
|
||||||
|
|
||||||
val dispatcher = StandardTestDispatcher()
|
|
||||||
val scope = TestScope(dispatcher)
|
|
||||||
|
|
||||||
val fakeClock = TestUserClock()
|
val fakeClock = TestUserClock()
|
||||||
|
|
||||||
// This implementation is hardcoded for the URI above.
|
// This implementation is hardcoded for the URI above.
|
||||||
|
|
|
@ -42,6 +42,10 @@ include("app")
|
||||||
|
|
||||||
include("autofill-parser")
|
include("autofill-parser")
|
||||||
|
|
||||||
|
include("coroutine-utils")
|
||||||
|
|
||||||
|
include("coroutine-utils-testing")
|
||||||
|
|
||||||
include("crypto-common")
|
include("crypto-common")
|
||||||
|
|
||||||
include("crypto-pgpainless")
|
include("crypto-pgpainless")
|
||||||
|
@ -50,4 +54,4 @@ include("format-common")
|
||||||
|
|
||||||
include("openpgp-ktx")
|
include("openpgp-ktx")
|
||||||
|
|
||||||
include(":dependency-sync")
|
include("dependency-sync")
|
||||||
|
|
Loading…
Reference in a new issue