From 7a1887e2d036a41d777c3a064fcab5103c4de498 Mon Sep 17 00:00:00 2001 From: vkanellopoulos Date: Thu, 27 Jun 2024 13:49:58 +0300 Subject: [PATCH] marshal txCode for OpenId4VCI pre-authorized flow --- .../-offer/-tx-code-spec/-tx-code-spec.md | 2 +- .../-offer/-tx-code-spec/index.md | 10 +- .../-offer/index.md | 8 +- .../openid4vci/DefaultOpenId4VciManager.kt | 3 +- .../wallet/issue/openid4vci/Exceptions.kt | 4 +- .../issue/openid4vci/IssuerAuthorization.kt | 40 +++++- .../ec/eudi/wallet/issue/openid4vci/Offer.kt | 8 +- .../issue/openid4vci/DeferredStateTest.kt | 97 +------------ .../openid4vci/IssuerAuthorizationTest.kt | 132 +++++++++++++++--- 9 files changed, 168 insertions(+), 136 deletions(-) diff --git a/docs/wallet-core/eu.europa.ec.eudi.wallet.issue.openid4vci/-offer/-tx-code-spec/-tx-code-spec.md b/docs/wallet-core/eu.europa.ec.eudi.wallet.issue.openid4vci/-offer/-tx-code-spec/-tx-code-spec.md index f98d9408..94ab617e 100644 --- a/docs/wallet-core/eu.europa.ec.eudi.wallet.issue.openid4vci/-offer/-tx-code-spec/-tx-code-spec.md +++ b/docs/wallet-core/eu.europa.ec.eudi.wallet.issue.openid4vci/-offer/-tx-code-spec/-tx-code-spec.md @@ -3,6 +3,6 @@ # TxCodeSpec [androidJvm]\ -constructor(inputMode: [Offer.TxCodeSpec.InputMode](-input-mode/index.md) = InputMode.NUMERIC, +constructor(inputMode: [Offer.TxCodeSpec.InputMode](-input-mode/index.md) = NUMERIC, length: [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html)?, description: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? = null) diff --git a/docs/wallet-core/eu.europa.ec.eudi.wallet.issue.openid4vci/-offer/-tx-code-spec/index.md b/docs/wallet-core/eu.europa.ec.eudi.wallet.issue.openid4vci/-offer/-tx-code-spec/index.md index 2c8ff9ca..672474f0 100644 --- a/docs/wallet-core/eu.europa.ec.eudi.wallet.issue.openid4vci/-offer/-tx-code-spec/index.md +++ b/docs/wallet-core/eu.europa.ec.eudi.wallet.issue.openid4vci/-offer/-tx-code-spec/index.md @@ -3,17 +3,17 @@ # TxCodeSpec [androidJvm]\ -data class [TxCodeSpec](index.md)(val inputMode: [Offer.TxCodeSpec.InputMode](-input-mode/index.md) = InputMode.NUMERIC, -val length: [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html)?, val +data class [TxCodeSpec](index.md)(val inputMode: [Offer.TxCodeSpec.InputMode](-input-mode/index.md) = NUMERIC, val +length: [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html)?, val description: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? = null) Specification for a transaction code. ## Constructors -| | | -|--------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [TxCodeSpec](-tx-code-spec.md) | [androidJvm]
constructor(inputMode: [Offer.TxCodeSpec.InputMode](-input-mode/index.md) = InputMode.NUMERIC, length: [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html)?, description: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? = null) | +| | | +|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [TxCodeSpec](-tx-code-spec.md) | [androidJvm]
constructor(inputMode: [Offer.TxCodeSpec.InputMode](-input-mode/index.md) = NUMERIC, length: [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html)?, description: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? = null) | ## Types diff --git a/docs/wallet-core/eu.europa.ec.eudi.wallet.issue.openid4vci/-offer/index.md b/docs/wallet-core/eu.europa.ec.eudi.wallet.issue.openid4vci/-offer/index.md index ef8c770f..ba05a853 100644 --- a/docs/wallet-core/eu.europa.ec.eudi.wallet.issue.openid4vci/-offer/index.md +++ b/docs/wallet-core/eu.europa.ec.eudi.wallet.issue.openid4vci/-offer/index.md @@ -9,10 +9,10 @@ An offer of credentials to be issued. ## Types -| Name | Summary | -|-----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [OfferedDocument](-offered-document/index.md) | [androidJvm]
interface [OfferedDocument](-offered-document/index.md)
An item to be issued. | -| [TxCodeSpec](-tx-code-spec/index.md) | [androidJvm]
data class [TxCodeSpec](-tx-code-spec/index.md)(val inputMode: [Offer.TxCodeSpec.InputMode](-tx-code-spec/-input-mode/index.md) = InputMode.NUMERIC, val length: [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html)?, val description: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? = null)
Specification for a transaction code. | +| Name | Summary | +|-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [OfferedDocument](-offered-document/index.md) | [androidJvm]
interface [OfferedDocument](-offered-document/index.md)
An item to be issued. | +| [TxCodeSpec](-tx-code-spec/index.md) | [androidJvm]
data class [TxCodeSpec](-tx-code-spec/index.md)(val inputMode: [Offer.TxCodeSpec.InputMode](-tx-code-spec/-input-mode/index.md) = NUMERIC, val length: [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html)?, val description: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? = null)
Specification for a transaction code. | ## Properties diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOpenId4VciManager.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOpenId4VciManager.kt index 5bf9057e..24f74b95 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOpenId4VciManager.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOpenId4VciManager.kt @@ -222,8 +222,7 @@ internal class DefaultOpenId4VciManager( ) { offer as DefaultOffer val issuer = issuerCreator.createIssuer(offer) - var authorizedRequest = issuerAuthorization.authorize(issuer, txCode) - + var authorizedRequest = issuerAuthorization.use { it.authorize(issuer, txCode) } listener(IssueEvent.Started(offer.offeredDocuments.size)) val issuedDocumentIds = mutableListOf() val requestMap = offer.offeredDocuments.associateBy { offeredDocument -> diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Exceptions.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Exceptions.kt index de24147c..2ed5cf9d 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Exceptions.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Exceptions.kt @@ -18,14 +18,14 @@ package eu.europa.ec.eudi.wallet.issue.openid4vci import androidx.biometric.BiometricPrompt.CryptoObject -import eu.europa.ec.eudi.openid4vci.SubmittedRequest +import eu.europa.ec.eudi.openid4vci.SubmissionOutcome /** * Exception thrown when user authentication is required. */ internal class UserAuthRequiredException( val cryptoObject: CryptoObject?, - val resume: suspend (Boolean) -> SubmittedRequest + val resume: suspend (Boolean) -> SubmissionOutcome, ) : Throwable() /** diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/IssuerAuthorization.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/IssuerAuthorization.kt index 53eeaf54..a7137420 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/IssuerAuthorization.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/IssuerAuthorization.kt @@ -20,10 +20,7 @@ import android.content.Context import android.content.Intent import android.content.Intent.ACTION_VIEW import android.net.Uri -import eu.europa.ec.eudi.openid4vci.AuthorizationCode -import eu.europa.ec.eudi.openid4vci.AuthorizationRequestPrepared -import eu.europa.ec.eudi.openid4vci.AuthorizedRequest -import eu.europa.ec.eudi.openid4vci.Issuer +import eu.europa.ec.eudi.openid4vci.* import eu.europa.ec.eudi.wallet.issue.openid4vci.OpenId4VciManager.Companion.TAG import eu.europa.ec.eudi.wallet.logging.Logger import kotlinx.coroutines.CancellableContinuation @@ -49,12 +46,17 @@ internal class IssuerAuthorization( * otherwise the browser will be opened for user authorization * @param issuer The issuer to authorize. * @param txCode The pre-authorization code. + * @throws IllegalArgumentException if the txCode is invalid. */ suspend fun authorize(issuer: Issuer, txCode: String?): AuthorizedRequest { close() // close any previous suspensions return with(issuer) { when { - !txCode.isNullOrEmpty() -> authorizeWithPreAuthorizationCode(txCode) + isPreAuthorized() -> { + marshalTxCode(txCode) + authorizeWithPreAuthorizationCode(txCode) + } + else -> { val prepareAuthorizationCodeRequest = prepareAuthorizationRequest().getOrThrow() val authResponse = openBrowserForAuthorization(prepareAuthorizationCodeRequest).getOrThrow() @@ -123,4 +125,32 @@ internal class IssuerAuthorization( } data class Response(val authorizationCode: String, val serverState: String) + + companion object { + /** + * Checks if the issuer's credential offer is pre-authorized. + * @receiver The issuer to check. + */ + fun Issuer.isPreAuthorized(): Boolean = credentialOffer.grants?.preAuthorizedCode() != null + + /** + * Ensures the txCode is valid for the given issuer. + * @param txCode The pre-authorization code. + * @throws IllegalArgumentException if the txCode is invalid. + * @receiver The issuer to authorize. + */ + fun Issuer.marshalTxCode(txCode: String?) { + when (val txCodeSpec = credentialOffer.grants?.preAuthorizedCode()?.txCode) { + // no txCode in credential offer grants, so no txCode is expected for pre-authorization + null -> if (txCode != null) throw IllegalArgumentException("txCode is not required") + // txCode is present in credential offer grants, so it is required for pre-authorization + else -> when { + txCode == null -> throw IllegalArgumentException("txCode is required") + txCode.length != txCodeSpec.length -> throw IllegalArgumentException("txCode length is invalid") + txCodeSpec.inputMode == TxCodeInputMode.NUMERIC && !txCode.all { it.isDigit() } -> + throw IllegalArgumentException("txCode is not numeric") + } + } + } + } } \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Offer.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Offer.kt index 3a27865b..1e50ad5f 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Offer.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Offer.kt @@ -15,10 +15,8 @@ */ package eu.europa.ec.eudi.wallet.issue.openid4vci -import eu.europa.ec.eudi.openid4vci.CredentialConfiguration -import eu.europa.ec.eudi.openid4vci.CredentialConfigurationIdentifier -import eu.europa.ec.eudi.openid4vci.TxCode -import eu.europa.ec.eudi.openid4vci.TxCodeInputMode +import eu.europa.ec.eudi.wallet.issue.openid4vci.Offer.TxCodeSpec.InputMode.NUMERIC +import eu.europa.ec.eudi.wallet.issue.openid4vci.Offer.TxCodeSpec.InputMode.TEXT /** * An offer of credentials to be issued. @@ -55,7 +53,7 @@ interface Offer { * @property description a description of the transaction code */ data class TxCodeSpec( - val inputMode: InputMode = InputMode.NUMERIC, + val inputMode: InputMode = NUMERIC, val length: Int?, val description: String? = null, ) { diff --git a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DeferredStateTest.kt b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DeferredStateTest.kt index c485015d..a462e280 100644 --- a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DeferredStateTest.kt +++ b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DeferredStateTest.kt @@ -12,99 +12,4 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */ - -package eu.europa.ec.eudi.wallet.issue.openid4vci - -import com.nimbusds.jose.JWSAlgorithm -import eu.europa.ec.eudi.openid4vci.* -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import java.net.URI -import java.time.Instant -import java.util.* - -class DeferredStateTest { - - @Test - fun `test encode and decode methods`() { - val deferredCredential = IssuedCredential.Deferred( - transactionId = TransactionId("transactionId"), - ) - val credentialIdentifiers = mapOf( - CredentialConfigurationIdentifier("credentialConfigurationId") to listOf( - CredentialIdentifier("credentialId") - ) - ) - val authorizedRequest = AuthorizedRequest.ProofRequired( - accessToken = AccessToken(accessToken = "accessToken", expiresInSec = 3600, useDPoP = false), - refreshToken = RefreshToken("refreshToken", expiresInSec = 3600), - cNonce = CNonce(value = "cNonce"), - credentialIdentifiers = credentialIdentifiers, - timestamp = Instant.now() - ) - - val credentialIssuerMetadata = CredentialIssuerMetadata( - credentialIssuerIdentifier = CredentialIssuerId("https://localhost:8080").getOrThrow(), - authorizationServers = listOf(HttpsUrl("https://localhost:8080").getOrThrow()), - credentialEndpoint = CredentialIssuerEndpoint("https://localhost:8080").getOrThrow(), - batchCredentialEndpoint = CredentialIssuerEndpoint("https://localhost:8080").getOrThrow(), - deferredCredentialEndpoint = CredentialIssuerEndpoint("https://localhost:8080").getOrThrow(), - notificationEndpoint = CredentialIssuerEndpoint("https://localhost:8080").getOrThrow(), - credentialResponseEncryption = CredentialResponseEncryption.NotSupported, - credentialIdentifiersSupported = true, - credentialConfigurationsSupported = mapOf( - CredentialConfigurationIdentifier("credentialConfigurationId") to MsoMdocCredential( - credentialSigningAlgorithmsSupported = listOf("alg"), - isoCredentialSigningAlgorithmsSupported = listOf(CoseAlgorithm.ES256), - isoCredentialCurvesSupported = listOf(CoseCurve.P_256), - proofTypesSupported = ProofTypesSupported( - setOf( - ProofTypeMeta.Jwt(listOf(JWSAlgorithm.ES256)), - ProofTypeMeta.Cwt(listOf(CoseAlgorithm.ES256), listOf(CoseCurve.P_256)) - ) - ), - display = listOf( - Display( - name = "name", - locale = Locale.US, - logo = Display.Logo(URI("http://localhost:8080"), "image/png"), - textColor = "#444444" - ) - ), - claims = mapOf( - "namespace" to mapOf( - "claim" to Claim( - true, "claim", listOf( - Claim.Display("name", Locale.US) - ) - ) - ) - ), - order = listOf("claim"), - docType = "docType", - isoPolicy = MsoMdocPolicy(true, 10), - scope = "scope", - cryptographicBindingMethodsSupported = listOf(CryptographicBindingMethod.JWK), - ) - ), - display = listOf( - CredentialIssuerMetadata.Display(name = "name", locale = "el_GR") - ), - ) - val state = DeferredState( - clientId = "clientId", - tokenEndpoint = URI("http://localhost:8080"), - credentialIssuerMetadata = credentialIssuerMetadata, - authorizedRequest = authorizedRequest, - deferredCredential = deferredCredential - ) - - val encoded = state.encode() - - val decoded = DeferredState.decode(encoded) - - assertEquals(state, decoded) - - } -} \ No newline at end of file + */ \ No newline at end of file diff --git a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/IssuerAuthorizationTest.kt b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/IssuerAuthorizationTest.kt index 05bb4ad8..2922a69c 100644 --- a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/IssuerAuthorizationTest.kt +++ b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/IssuerAuthorizationTest.kt @@ -19,10 +19,8 @@ package eu.europa.ec.eudi.wallet.issue.openid4vci import android.content.Context import android.content.Intent import android.net.Uri -import eu.europa.ec.eudi.openid4vci.AuthorizationRequestPrepared -import eu.europa.ec.eudi.openid4vci.AuthorizedRequest -import eu.europa.ec.eudi.openid4vci.HttpsUrl -import eu.europa.ec.eudi.openid4vci.Issuer +import eu.europa.ec.eudi.openid4vci.* +import eu.europa.ec.eudi.wallet.issue.openid4vci.IssuerAuthorization.Companion.marshalTxCode import eu.europa.ec.eudi.wallet.logging.Logger import io.mockk.* import kotlinx.coroutines.Dispatchers @@ -33,6 +31,9 @@ import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource import kotlin.time.Duration.Companion.milliseconds @@ -43,7 +44,6 @@ class IssuerAuthorizationTest { lateinit var logger: Logger lateinit var issuer: Issuer - @BeforeAll @JvmStatic fun setup() { @@ -54,12 +54,12 @@ class IssuerAuthorizationTest { mockkConstructor(Intent::class) every { anyConstructed().addFlags(any()) } returns mockk(relaxed = true) - issuer = mockk(relaxed = true) - - context = mockk(relaxed = true) logger = mockk(relaxed = true) } + + @JvmStatic + fun provideTxCode() = txCodeTestSource } lateinit var preparedAuthorizationRequest: AuthorizationRequestPrepared @@ -72,8 +72,8 @@ class IssuerAuthorizationTest { preparedAuthorizationRequest.authorizationCodeURL } returns HttpsUrl("https://test.com").getOrThrow() + issuer = mockk(relaxed = true) authorizedRequest = mockk(relaxed = true) - coEvery { issuer.prepareAuthorizationRequest() } returns Result.success(preparedAuthorizationRequest) @@ -90,8 +90,12 @@ class IssuerAuthorizationTest { } @Test - fun `authorize method when txCode equals to null call openBrowserForAuthorization`() { - + fun `authorize method when no preAuthorizedCode in offer and txCode is null calls openBrowserForAuthorization`() { + every { issuer.credentialOffer } returns mockk(relaxed = true) { + every { grants } returns mockk(relaxed = true) { + every { preAuthorizedCode() } returns null + } + } val issuerAuthorization = spyk(IssuerAuthorization(context, logger)) runTest { launch { @@ -109,12 +113,21 @@ class IssuerAuthorizationTest { } @Test - fun `authorize method when passing txCode does not call openBrowserForAuthorization but calls authorizeWithPreAuthorizationCode`() { - + fun `authorize method when preAuthorizedCode in offer and passing txCode does not call openBrowserForAuthorization but calls authorizeWithPreAuthorizationCode`() { + every { issuer.credentialOffer } returns mockk(relaxed = true) { + every { grants } returns mockk(relaxed = true) { + every { preAuthorizedCode() } returns mockk(relaxed = true) { + every { txCode } returns mockk(relaxed = true) { + every { length } returns 4 + every { inputMode } returns TxCodeInputMode.NUMERIC + } + } + } + } val issuerAuthorization = spyk(IssuerAuthorization(context, logger)) runTest { launch { - issuerAuthorization.authorize(issuer, "testCode") + issuerAuthorization.authorize(issuer, "1234") } launch { delay(500.milliseconds) @@ -126,7 +139,7 @@ class IssuerAuthorizationTest { issuerAuthorization.openBrowserForAuthorization(preparedAuthorizationRequest) } coVerify(exactly = 1) { - issuer.authorizeWithPreAuthorizationCode("testCode") + issuer.authorizeWithPreAuthorizationCode("1234") } } @@ -228,4 +241,91 @@ class IssuerAuthorizationTest { verify(exactly = 1) { issuerAuthorization.close() } assertNull(issuerAuthorization.continuation, "Continuation is removed") } -} \ No newline at end of file + + @ParameterizedTest(name = "marshalTxCode({0}) with issuer {1} throws {2}") + @MethodSource("provideTxCode") + fun `test marshalTxCode method`(txCode: String?, issuer: Issuer, expectedException: Exception?) { + if (expectedException != null) { + assertThrows(expectedException::class.java, { + issuer.marshalTxCode(txCode) + }, expectedException.message) + } else { + assertDoesNotThrow { + issuer.marshalTxCode(txCode) + } + } + } +} + +private val txCodeTestSource = listOf( + listOf( + null, + mockk(relaxed = true) { + every { credentialOffer } returns mockk(relaxed = true) { + every { grants } returns mockk(relaxed = true) { + every { preAuthorizedCode() } returns mockk(relaxed = true) { + every { txCode } returns mockk(relaxed = true) + } + } + } + }, + IllegalArgumentException("txCode is required") + ), + listOf( + null, + mockk(relaxed = true) { + every { credentialOffer } returns mockk(relaxed = true) { + every { grants } returns mockk(relaxed = true) { + every { preAuthorizedCode() } returns mockk(relaxed = true) { + every { txCode } returns null + } + } + } + }, + null + ), + listOf( + "123456", + mockk(relaxed = true) { + every { credentialOffer } returns mockk(relaxed = true) { + every { grants } returns mockk(relaxed = true) { + every { preAuthorizedCode() } returns mockk(relaxed = true) { + every { txCode } returns null + } + } + } + }, + IllegalArgumentException("txCode is not required") + ), + listOf( + "123456", + mockk(relaxed = true) { + every { credentialOffer } returns mockk(relaxed = true) { + every { grants } returns mockk(relaxed = true) { + every { preAuthorizedCode() } returns mockk(relaxed = true) { + every { txCode } returns mockk(relaxed = true) { + every { length } returns 10 + } + } + } + } + }, + IllegalArgumentException("txCode length is invalid") + ), + listOf( + "abcd", + mockk(relaxed = true) { + every { credentialOffer } returns mockk(relaxed = true) { + every { grants } returns mockk(relaxed = true) { + every { preAuthorizedCode() } returns mockk(relaxed = true) { + every { txCode } returns mockk(relaxed = true) { + every { length } returns 4 + every { inputMode } returns TxCodeInputMode.NUMERIC + } + } + } + } + }, + IllegalArgumentException("txCode is not numeric") + ), +).map { Arguments.of(*it.toTypedArray()) } \ No newline at end of file