diff --git a/stripecardscan/build.gradle b/stripecardscan/build.gradle index 5966167a05d..f123145a4b4 100644 --- a/stripecardscan/build.gradle +++ b/stripecardscan/build.gradle @@ -37,7 +37,7 @@ dependencies { testImplementation "junit:junit:$junitVersion" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlinVersion" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutinesVersion" - testImplementation "org.mockito:mockito-core:$mockitoCoreVersion" + testImplementation "org.mockito:mockito-inline:$mockitoCoreVersion" testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" testImplementation "com.google.truth:truth:$truthVersion" testImplementation "org.robolectric:robolectric:$robolectricVersion" diff --git a/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/MachineState.kt b/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/MachineState.kt index 8ce73065588..e2b1d9d6fdc 100644 --- a/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/MachineState.kt +++ b/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/MachineState.kt @@ -1,6 +1,5 @@ package com.stripe.android.stripecardscan.framework -import android.util.Log import com.stripe.android.camera.framework.time.Clock import com.stripe.android.camera.framework.time.ClockMark @@ -10,11 +9,4 @@ internal abstract class MachineState { * Keep track of when this state was reached */ protected open val reachedStateAt: ClockMark = Clock.markNow() - - override fun toString(): String = - "${this::class.java.simpleName}(reachedStateAt=$reachedStateAt)" - - init { - Log.d(LOG_TAG, "${this::class.java.simpleName} machine state reached") - } } diff --git a/stripecardscan/src/test/java/com/stripe/android/stripecardscan/cardimageverification/result/MainLoopStateMachineTest.kt b/stripecardscan/src/test/java/com/stripe/android/stripecardscan/cardimageverification/result/MainLoopStateMachineTest.kt index 8ca59b54bab..60dc801d5a7 100644 --- a/stripecardscan/src/test/java/com/stripe/android/stripecardscan/cardimageverification/result/MainLoopStateMachineTest.kt +++ b/stripecardscan/src/test/java/com/stripe/android/stripecardscan/cardimageverification/result/MainLoopStateMachineTest.kt @@ -1,6 +1,8 @@ package com.stripe.android.stripecardscan.cardimageverification.result import androidx.test.filters.LargeTest +import com.stripe.android.camera.framework.time.Clock +import com.stripe.android.camera.framework.time.ClockMark import com.stripe.android.camera.framework.time.milliseconds import com.stripe.android.stripecardscan.cardimageverification.analyzer.MainLoopAnalyzer import com.stripe.android.stripecardscan.cardimageverification.result.MainLoopState.Companion.DESIRED_CARD_COUNT @@ -9,7 +11,6 @@ import com.stripe.android.stripecardscan.cardimageverification.result.MainLoopSt import com.stripe.android.stripecardscan.cardimageverification.result.MainLoopState.Companion.OCR_AND_CARD_SEARCH_DURATION import com.stripe.android.stripecardscan.cardimageverification.result.MainLoopState.Companion.OCR_ONLY_SEARCH_DURATION import com.stripe.android.stripecardscan.cardimageverification.result.MainLoopState.Companion.WRONG_CARD_DURATION -import com.stripe.android.stripecardscan.framework.time.delay import com.stripe.android.stripecardscan.framework.util.ItemCounter import com.stripe.android.stripecardscan.payment.card.CardIssuer import com.stripe.android.stripecardscan.payment.ml.CardDetect @@ -18,6 +19,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.Test +import org.mockito.Mockito +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertSame @@ -26,22 +30,24 @@ import kotlin.test.assertTrue class MainLoopStateMachineTest { @Test - fun initial_runsOcrOnly() { + fun `initial state runs OCR and CardDetector ML models`() { val state = MainLoopState.Initial( requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 0, ) assertTrue(state.runOcr) - assertFalse(state.runCardDetect) + assertTrue(state.runCardDetect) } @Test @ExperimentalCoroutinesApi - fun initial_noCard_noOcr() = runTest { + fun `initial state does not transition when no card and no OCR are found`() = runTest { val state = MainLoopState.Initial( requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 0, ) val prediction = MainLoopAnalyzer.Prediction( @@ -57,10 +63,11 @@ class MainLoopStateMachineTest { @Test @ExperimentalCoroutinesApi - fun initial_wrongCard() = runTest { + fun `initial state transitions to wrong card when a wrong card is detected`() = runTest { val state = MainLoopState.Initial( requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 0, ) val prediction = MainLoopAnalyzer.Prediction( @@ -75,10 +82,11 @@ class MainLoopStateMachineTest { @Test @ExperimentalCoroutinesApi - fun initial_noCard_foundOcr() = runTest { + fun `initial state does not transition when OCR is found but no card is visible`() = runTest { val state = MainLoopState.Initial( requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 1, ) val prediction = MainLoopAnalyzer.Prediction( @@ -86,17 +94,64 @@ class MainLoopStateMachineTest { card = null, ) + val newState = state.consumeTransition(prediction) + assertTrue(newState is MainLoopState.Initial) + } + + @Test + @ExperimentalCoroutinesApi + fun `initial state does not transition when OCR is not found but a card is visible`() = runTest { + val state = MainLoopState.Initial( + requiredCardIssuer = CardIssuer.Visa, + requiredLastFour = "8770", + strictModeFrames = 1, + ) + + val prediction = MainLoopAnalyzer.Prediction( + ocr = null, + card = CardDetect.Prediction( + side = CardDetect.Prediction.Side.PAN, + noCardProbability = 0F, + noPanProbability = 0F, + panProbability = 1F, + ), + ) + + val newState = state.consumeTransition(prediction) + assertTrue(newState is MainLoopState.Initial) + } + + @Test + @ExperimentalCoroutinesApi + fun `initial state transitions when OCR is found and a card is visible`() = runTest { + val state = MainLoopState.Initial( + requiredCardIssuer = CardIssuer.Visa, + requiredLastFour = "8770", + strictModeFrames = 1, + ) + + val prediction = MainLoopAnalyzer.Prediction( + ocr = SSDOcr.Prediction("4847186095118770"), + card = CardDetect.Prediction( + side = CardDetect.Prediction.Side.PAN, + noCardProbability = 0F, + noPanProbability = 0F, + panProbability = 1F, + ), + ) + val newState = state.consumeTransition(prediction) assertTrue(newState is MainLoopState.OcrFound) } @Test - fun panFound_runsCardDetectAndOcrOnly() { + fun `ocrFound runs CardDetect and OCR ML models`() { val state = MainLoopState.OcrFound( - pan = "4847186095118770", - isCardVisible = true, + panCounter = ItemCounter("4847186095118770"), + visibleCardCount = 1, requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 0, ) assertTrue(state.runOcr) @@ -105,12 +160,13 @@ class MainLoopStateMachineTest { @Test @ExperimentalCoroutinesApi - fun panFound_noCard_noTimeout() = runTest { + fun `ocrFound doesn't transition if it hasn't timed out and hasn't found more OCR`() = runTest { val state = MainLoopState.OcrFound( - pan = "4847186095118770", - isCardVisible = true, + panCounter = ItemCounter("4847186095118770"), + visibleCardCount = 1, requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 0, ) val prediction = MainLoopAnalyzer.Prediction( @@ -124,12 +180,13 @@ class MainLoopStateMachineTest { @Test @ExperimentalCoroutinesApi - fun panFound_cardSatisfied_noTimeout() = runTest { + fun `ocrFound transitions to CardSatisfied when enough matching cards are found`() = runTest { var state: MainLoopState = MainLoopState.OcrFound( - pan = "4847186095118770", - isCardVisible = true, + panCounter = ItemCounter("4847186095118770"), + visibleCardCount = 1, requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 0, ) val prediction = MainLoopAnalyzer.Prediction( @@ -142,6 +199,10 @@ class MainLoopStateMachineTest { ), ) + // this is -2 because it must be: + // 1 for initial count + // + 1 to stay below desired before last transition + // the last transition will meet the desired count repeat(DESIRED_CARD_COUNT - 2) { state = state.consumeTransition(prediction) assertTrue(state is MainLoopState.OcrFound) @@ -153,12 +214,13 @@ class MainLoopStateMachineTest { @Test @ExperimentalCoroutinesApi - fun panFound_panSatisfied_noTimeout() = runTest { + fun `ocrFound transitions to OcrSatisfied when enough pans are found`() = runTest { var state: MainLoopState = MainLoopState.OcrFound( - pan = "4847186095118770", - isCardVisible = true, + panCounter = ItemCounter("4847186095118770"), + visibleCardCount = 1, requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 0, ) val prediction = MainLoopAnalyzer.Prediction( @@ -166,6 +228,10 @@ class MainLoopStateMachineTest { card = null, ) + // this is -2 because it must be: + // 1 for initial count + // + 1 to stay below desired before last transition + // the last transition will meet the desired count repeat(DESIRED_OCR_AGREEMENT - 2) { state = state.consumeTransition(prediction) assertTrue(state is MainLoopState.OcrFound) @@ -182,28 +248,39 @@ class MainLoopStateMachineTest { */ @Test @ExperimentalCoroutinesApi - fun panFound_finished_timeout() = runBlocking { - var state: MainLoopState = MainLoopState.OcrFound( - pan = "4847186095118770", - isCardVisible = true, - requiredCardIssuer = CardIssuer.Visa, - requiredLastFour = "8770", - ) - - val prediction = MainLoopAnalyzer.Prediction( - ocr = SSDOcr.Prediction("4847186095118770"), - card = null, - ) - - repeat(DESIRED_OCR_AGREEMENT - 3) { - state = state.consumeTransition(prediction) - assertTrue(state is MainLoopState.OcrFound) + fun `OcrFound transitions to Finished when it times out`() = runBlocking { + val mockClockMark = mock() + Mockito.mockStatic(Clock::class.java).use { mockClock -> + mockClock.`when` { Clock.markNow() }.thenReturn(mockClockMark) + + var state: MainLoopState = MainLoopState.OcrFound( + panCounter = ItemCounter("4847186095118770"), + visibleCardCount = 1, + requiredCardIssuer = CardIssuer.Visa, + requiredLastFour = "8770", + strictModeFrames = 0, + ) + + val prediction = MainLoopAnalyzer.Prediction( + ocr = SSDOcr.Prediction("4847186095118770"), + card = null, + ) + + // this is -3 because it must be: + // 1 for initial count + // + 1 to stay below desired before last transition + // + 1 to stay below desired with the last transition + repeat(DESIRED_OCR_AGREEMENT - 3) { + state = state.consumeTransition(prediction) + assertTrue(state is MainLoopState.OcrFound) + } + + whenever(mockClockMark.elapsedSince()) + .thenReturn(OCR_AND_CARD_SEARCH_DURATION + 1.milliseconds) + + val newState = state.consumeTransition(prediction) + assertTrue(newState is MainLoopState.Finished) } - - delay(OCR_AND_CARD_SEARCH_DURATION + 1.milliseconds) - - val newState = state.consumeTransition(prediction) - assertTrue(newState is MainLoopState.Finished) } /** @@ -213,33 +290,43 @@ class MainLoopStateMachineTest { */ @Test @ExperimentalCoroutinesApi - fun panFound_noCardVisible_timeout() = runBlocking { - var state: MainLoopState = MainLoopState.OcrFound( - pan = "4847186095118770", - isCardVisible = true, - requiredCardIssuer = CardIssuer.Visa, - requiredLastFour = "8770", - ) - - val predictionWithCard = MainLoopAnalyzer.Prediction( - ocr = SSDOcr.Prediction("4847186095118770"), - card = null, - ) - - repeat(DESIRED_OCR_AGREEMENT - 2) { - state = state.consumeTransition(predictionWithCard) - assertTrue(state is MainLoopState.OcrFound) + fun `OcrFound transitions to Initial if no card is visible after timeout`() = runBlocking { + val mockClockMark = mock() + Mockito.mockStatic(Clock::class.java).use { mockClock -> + mockClock.`when` { Clock.markNow() }.thenReturn(mockClockMark) + + var state: MainLoopState = MainLoopState.OcrFound( + panCounter = ItemCounter("4847186095118770"), + visibleCardCount = 1, + requiredCardIssuer = CardIssuer.Visa, + requiredLastFour = "8770", + strictModeFrames = 0, + ) + + val predictionWithCard = MainLoopAnalyzer.Prediction( + ocr = SSDOcr.Prediction("4847186095118770"), + card = null, + ) + + whenever(mockClockMark.elapsedSince()) + .thenReturn(NO_CARD_VISIBLE_DURATION - 1.milliseconds) + + repeat(DESIRED_OCR_AGREEMENT - 2) { + state = state.consumeTransition(predictionWithCard) + assertTrue(state is MainLoopState.OcrFound) + } + + whenever(mockClockMark.elapsedSince()) + .thenReturn(NO_CARD_VISIBLE_DURATION + 1.milliseconds) + + val predictionWithoutCard = MainLoopAnalyzer.Prediction( + ocr = null, + card = CardDetect.Prediction(CardDetect.Prediction.Side.NO_CARD, 1.0F, 0.0F, 0.0F) + ) + + val newState = state.consumeTransition(predictionWithoutCard) + assertTrue(newState is MainLoopState.Initial) } - - delay(NO_CARD_VISIBLE_DURATION + 1.milliseconds) - - val predictionWithoutCard = MainLoopAnalyzer.Prediction( - ocr = null, - card = CardDetect.Prediction(CardDetect.Prediction.Side.NO_CARD, 1.0F, 0.0F, 0.0F) - ) - - val newState = state.consumeTransition(predictionWithoutCard) - assertTrue(newState is MainLoopState.Initial) } /** @@ -249,33 +336,41 @@ class MainLoopStateMachineTest { */ @Test @LargeTest - fun panFound_timeout() = runBlocking { - val state = MainLoopState.OcrFound( - pan = "4847186095118770", - isCardVisible = true, - requiredCardIssuer = CardIssuer.Visa, - requiredLastFour = "8770", - ) - - delay(OCR_AND_CARD_SEARCH_DURATION + 1.milliseconds) - - val prediction = MainLoopAnalyzer.Prediction( - ocr = null, - card = null, - ) - - val newState = state.consumeTransition(prediction) - assertTrue(newState is MainLoopState.Finished, "$newState is not Finished") + fun `OcrFound transitions to Finished after a timeout if cards are visible`() = runBlocking { + val mockClockMark = mock() + Mockito.mockStatic(Clock::class.java).use { mockClock -> + mockClock.`when` { Clock.markNow() }.thenReturn(mockClockMark) + + val state = MainLoopState.OcrFound( + panCounter = ItemCounter("4847186095118770"), + visibleCardCount = 1, + requiredCardIssuer = CardIssuer.Visa, + requiredLastFour = "8770", + strictModeFrames = 0, + ) + + whenever(mockClockMark.elapsedSince()) + .thenReturn(OCR_AND_CARD_SEARCH_DURATION + 1.milliseconds) + + val prediction = MainLoopAnalyzer.Prediction( + ocr = null, + card = null, + ) + + val newState = state.consumeTransition(prediction) + assertTrue(newState is MainLoopState.Finished, "$newState is not Finished") + } } @Test @LargeTest - fun panFound_wrongCardIgnored() = runBlocking { + fun `OcrFound state ignores wrong card pans`() = runBlocking { val state = MainLoopState.OcrFound( - pan = "4847186095118770", - isCardVisible = true, + panCounter = ItemCounter("4847186095118770"), + visibleCardCount = 1, requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 0, ) val prediction = MainLoopAnalyzer.Prediction( @@ -288,7 +383,7 @@ class MainLoopStateMachineTest { } @Test - fun panSatisfied_runsCardDetectOnly() { + fun `OcrSatisfied only runs the CardDetect ML model`() { val state = MainLoopState.OcrSatisfied( pan = "4847186095118770", visibleCardCount = 0, @@ -300,7 +395,7 @@ class MainLoopStateMachineTest { @Test @ExperimentalCoroutinesApi - fun panSatisfied_noCard_noTimeout() = runTest { + fun `OcrSatisfied doesn't transition if no card detected and it hasn't timed out`() = runTest { val state = MainLoopState.OcrSatisfied( pan = "4847186095118770", visibleCardCount = 0, @@ -322,7 +417,7 @@ class MainLoopStateMachineTest { @Test @ExperimentalCoroutinesApi - fun panSatisfied_enoughSides_noTimeout() = runTest { + fun `OcrSatisfied transitions to Finished when enough cards are seen`() = runTest { val state = MainLoopState.OcrSatisfied( pan = "4847186095118770", visibleCardCount = DESIRED_CARD_COUNT - 1, @@ -349,29 +444,36 @@ class MainLoopStateMachineTest { */ @Test @LargeTest - fun panSatisfied_timeout() = runBlocking { - val state = MainLoopState.OcrSatisfied( - pan = "4847186095118770", - visibleCardCount = DESIRED_CARD_COUNT - 1, - ) - - val prediction = MainLoopAnalyzer.Prediction( - ocr = null, - card = null, - ) - - delay(NO_CARD_VISIBLE_DURATION + 1.milliseconds) - - val newState = state.consumeTransition(prediction) - assertTrue(newState is MainLoopState.Finished) + fun `OcrSatisfied transitions to Finished when it times out`() = runBlocking { + val mockClockMark = mock() + Mockito.mockStatic(Clock::class.java).use { mockClock -> + mockClock.`when` { Clock.markNow() }.thenReturn(mockClockMark) + + val state = MainLoopState.OcrSatisfied( + pan = "4847186095118770", + visibleCardCount = DESIRED_CARD_COUNT - 1, + ) + + val prediction = MainLoopAnalyzer.Prediction( + ocr = null, + card = null, + ) + + whenever(mockClockMark.elapsedSince()) + .thenReturn(NO_CARD_VISIBLE_DURATION + 1.milliseconds) + + val newState = state.consumeTransition(prediction) + assertTrue(newState is MainLoopState.Finished) + } } @Test - fun cardSatisfied_runsOcrOnly() { + fun `CardSatisfied only runs the OCR ML model`() { val state = MainLoopState.CardSatisfied( panCounter = ItemCounter("4847186095118770"), requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 0, ) assertTrue(state.runOcr) @@ -380,11 +482,12 @@ class MainLoopStateMachineTest { @Test @ExperimentalCoroutinesApi - fun cardSatisfied_noPan_noTimeout() = runTest { + fun `CardSatisfied doesn't transition if no card and not timed out`() = runTest { val state = MainLoopState.CardSatisfied( panCounter = ItemCounter("4847186095118770"), requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 0, ) val prediction = MainLoopAnalyzer.Prediction( @@ -398,11 +501,12 @@ class MainLoopStateMachineTest { @Test @ExperimentalCoroutinesApi - fun cardSatisfied_pan_noTimeout() = runTest { + fun `CardSatisfied transitions to Finished if enough pans are found`() = runTest { var state: MainLoopState = MainLoopState.CardSatisfied( panCounter = ItemCounter("4847186095118770"), requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 0, ) val prediction = MainLoopAnalyzer.Prediction( @@ -410,6 +514,10 @@ class MainLoopStateMachineTest { card = null, ) + // this is -2 because it must be: + // 1 for initial count + // + 1 to stay below desired before last transition + // the last transition will meet the desired count repeat(DESIRED_OCR_AGREEMENT - 2) { state = state.consumeTransition(prediction) assertTrue(state is MainLoopState.CardSatisfied) @@ -426,31 +534,39 @@ class MainLoopStateMachineTest { */ @Test @LargeTest - fun cardSatisfied_noPan_timeout() = runBlocking { - val state = MainLoopState.CardSatisfied( - panCounter = ItemCounter("4847186095118770"), - requiredCardIssuer = CardIssuer.Visa, - requiredLastFour = "8770", - ) - - val prediction = MainLoopAnalyzer.Prediction( - ocr = null, - card = null, - ) - - delay(OCR_ONLY_SEARCH_DURATION + 1.milliseconds) - - val newState = state.consumeTransition(prediction) - assertTrue(newState is MainLoopState.Finished) + fun `CardSatisfied transitions to Finished after timeout`() = runBlocking { + val mockClockMark = mock() + Mockito.mockStatic(Clock::class.java).use { mockClock -> + mockClock.`when` { Clock.markNow() }.thenReturn(mockClockMark) + + val state = MainLoopState.CardSatisfied( + panCounter = ItemCounter("4847186095118770"), + requiredCardIssuer = CardIssuer.Visa, + requiredLastFour = "8770", + strictModeFrames = 0, + ) + + val prediction = MainLoopAnalyzer.Prediction( + ocr = null, + card = null, + ) + + whenever(mockClockMark.elapsedSince()) + .thenReturn(OCR_ONLY_SEARCH_DURATION + 1.milliseconds) + + val newState = state.consumeTransition(prediction) + assertTrue(newState is MainLoopState.Finished) + } } @Test @LargeTest - fun cardSatisfied_wrongCardIgnored() = runBlocking { + fun `CardSatisfied ignores wrong pans`() = runBlocking { val state = MainLoopState.CardSatisfied( panCounter = ItemCounter("4847186095118770"), requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 0, ) val prediction = MainLoopAnalyzer.Prediction( @@ -463,11 +579,12 @@ class MainLoopStateMachineTest { } @Test - fun wrongPanFound_runsOcrOnly() { + fun `WrongCard only runs the OCR ML model`() { val state = MainLoopState.WrongCard( pan = "5445435282861343", requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 0, ) assertTrue(state.runOcr) @@ -476,11 +593,12 @@ class MainLoopStateMachineTest { @Test @ExperimentalCoroutinesApi - fun wrongPanFound_noPan_noTimeout() = runTest { + fun `WrongCard does not transition if it hasn't timed out and no card seen`() = runTest { val state = MainLoopState.WrongCard( pan = "5445435282861343", requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 0, ) val prediction = MainLoopAnalyzer.Prediction( @@ -494,11 +612,12 @@ class MainLoopStateMachineTest { @Test @ExperimentalCoroutinesApi - fun wrongPanFound_wrongPan_noTimeout() = runTest { + fun `WrongCard resets if a new frame with the wrong card is visible`() = runTest { val state = MainLoopState.WrongCard( pan = "5445435282861343", requiredCardIssuer = CardIssuer.MasterCard, requiredLastFour = "8770", + strictModeFrames = 0, ) val prediction = MainLoopAnalyzer.Prediction( @@ -512,11 +631,12 @@ class MainLoopStateMachineTest { @Test @ExperimentalCoroutinesApi - fun wrongPanFound_rightPan_noTimeout() = runTest { + fun `WrongCard transitions to OcrFound if the right card is seen`() = runTest { val state = MainLoopState.WrongCard( pan = "5445435282861343", requiredCardIssuer = CardIssuer.Visa, requiredLastFour = "8770", + strictModeFrames = 0, ) val prediction = MainLoopAnalyzer.Prediction( @@ -535,26 +655,33 @@ class MainLoopStateMachineTest { */ @Test @LargeTest - fun wrongPanFound_noPan_timeout() = runBlocking { - val state = MainLoopState.WrongCard( - pan = "5445435282861343", - requiredCardIssuer = CardIssuer.Visa, - requiredLastFour = "8770", - ) - - val prediction = MainLoopAnalyzer.Prediction( - ocr = null, - card = null, - ) - - delay(WRONG_CARD_DURATION + 1.milliseconds) - - val newState = state.consumeTransition(prediction) - assertTrue(newState is MainLoopState.Initial) + fun `WrongCard transitions to initial when it times out`() = runBlocking { + val mockClockMark = mock() + Mockito.mockStatic(Clock::class.java).use { mockClock -> + mockClock.`when` { Clock.markNow() }.thenReturn(mockClockMark) + + val state = MainLoopState.WrongCard( + pan = "5445435282861343", + requiredCardIssuer = CardIssuer.Visa, + requiredLastFour = "8770", + strictModeFrames = 0, + ) + + val prediction = MainLoopAnalyzer.Prediction( + ocr = null, + card = null, + ) + + whenever(mockClockMark.elapsedSince()) + .thenReturn(WRONG_CARD_DURATION + 1.milliseconds) + + val newState = state.consumeTransition(prediction) + assertTrue(newState is MainLoopState.Initial) + } } @Test - fun finished_runsNothing() { + fun `Finished does not run any ML models`() { val state = MainLoopState.Finished("4847186095118770") assertFalse(state.runOcr) @@ -563,7 +690,7 @@ class MainLoopStateMachineTest { @Test @ExperimentalCoroutinesApi - fun finished_goesNowhere() = runTest { + fun `Finished does not transition`() = runTest { val state = MainLoopState.Finished("4847186095118770") val prediction = MainLoopAnalyzer.Prediction( diff --git a/stripecardscan/src/test/java/com/stripe/android/stripecardscan/framework/util/RetryTest.kt b/stripecardscan/src/test/java/com/stripe/android/stripecardscan/framework/util/RetryTest.kt index 99ce877a0ab..41f0f6625ca 100644 --- a/stripecardscan/src/test/java/com/stripe/android/stripecardscan/framework/util/RetryTest.kt +++ b/stripecardscan/src/test/java/com/stripe/android/stripecardscan/framework/util/RetryTest.kt @@ -13,12 +13,12 @@ class RetryTest { @Test @SmallTest @ExperimentalCoroutinesApi - fun retry_succeedsFirst() = runTest { + fun `Retry succeeds if the first attempt succeeds`() = runTest { var executions = 0 assertEquals( 1, - retry(1.milliseconds) { + retry({ _, _-> 1.milliseconds }) { executions++ 1 } @@ -29,12 +29,12 @@ class RetryTest { @Test @SmallTest @ExperimentalCoroutinesApi - fun retry_succeedsSecond() = runTest { + fun `Retry succeeds if the second execution succeeds`() = runTest { var executions = 0 assertEquals( 1, - retry(1.milliseconds) { + retry({ _, _-> 1.milliseconds }) { executions++ if (executions == 2) { 1 @@ -49,11 +49,11 @@ class RetryTest { @Test @SmallTest @ExperimentalCoroutinesApi - fun retry_fails() = runTest { + fun `Retry fails if all attempts fail`() = runTest { var executions = 0 assertFailsWith { - retry(1.milliseconds) { + retry({ _, _-> 1.milliseconds }) { executions++ throw RuntimeException() } @@ -64,11 +64,11 @@ class RetryTest { @Test @SmallTest @ExperimentalCoroutinesApi - fun retry_excluding() = runTest { + fun `Retry does not retry excluded exception types`() = runTest { var executions = 0 assertFailsWith { - retry(1.milliseconds, excluding = listOf(RuntimeException::class.java)) { + retry({ _, _-> 1.milliseconds }, excluding = listOf(RuntimeException::class.java)) { executions++ throw RuntimeException() }