libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 2cce8ae13da4e1592bf83ffca37afc64f26fcf8f
parent 7153ffba0a060cb6131421933fd8f978d4c9487a
Author: Antoine A <>
Date:   Wed, 26 Jun 2024 23:19:09 +0200

common: implemente a more lenient wire transfer subject parser

Diffstat:
Mcommon/src/main/kotlin/TxMedatada.kt | 50+++++++++++++++++++++++++++++++++++++++++++++++---
Acommon/src/test/kotlin/TxMedataTest.kt | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dnexus/src/test/kotlin/Parsing.kt | 69---------------------------------------------------------------------
3 files changed, 148 insertions(+), 72 deletions(-)

diff --git a/common/src/main/kotlin/TxMedatada.kt b/common/src/main/kotlin/TxMedatada.kt @@ -18,12 +18,56 @@ */ package tech.libeufin.common +private val BASE32_32B_UPPER_PATTERN = Regex("[0-9A-Z]{52}") private val BASE32_32B_PATTERN = Regex("[a-z0-9A-Z]{52}") +private val CLEAN_PATTERN = Regex(" ?[\\n\\-\\+] ?") -/** Extract the reserve public key from an incoming Taler transaction subject */ +/** + * Extract the reserve public key from an incoming Taler transaction subject + * + * We first try to match an uppercase key then a lowercase key. If none are + * found we clean the subject and retry. + **/ fun parseIncomingTxMetadata(subject: String): EddsaPublicKey { - val match = BASE32_32B_PATTERN.find(subject)?.value ?: throw Exception("Missing reserve public key") - return EddsaPublicKey(match) + /** + * Extract the reserve public key from [subject] using [pattern] + * + * Return null if found none and throw if find too many + **/ + fun run(subject: String, pattern: Regex): EddsaPublicKey? { + val matches = pattern.findAll(subject).iterator() + if (!matches.hasNext()) return null + val match = matches.next() + if (matches.hasNext()) { + //val count = matches.count() + throw Exception("Found multiple reserve public key") + } + return EddsaPublicKey(match.value) + } + + // Wire transfer subjects are generally small in size, and not + // being able to find the encoded reserve public key poses a huge + // usability problem. So we're ready to work hard to find it. + + // Bank interfaces doesn't always allow a complete encoded key to + // be entered on a single line, so users have to split them, which + // may lead them to add an erroneous space or + or - separator. + // If we can't find a key, we try to eliminate these common errors + // before trying again. + + // Since any sequence of 52 upper and lowercase characters can be a + // valid encoded key, deleting spaces and separators can create false + // positives, so we always start by searching for a key coded in + // uppercase and then mixed once. + + val matchWithoutCleaning = run(subject, BASE32_32B_UPPER_PATTERN) + ?: run(subject, BASE32_32B_PATTERN) + if (matchWithoutCleaning != null) return matchWithoutCleaning + + val cleaned = subject.replace(CLEAN_PATTERN, "") + return run(cleaned, BASE32_32B_UPPER_PATTERN) + ?: run(cleaned, BASE32_32B_PATTERN) + ?: throw Exception("Missing reserve public key") } /** Extract the reserve public key from an incoming Taler transaction subject */ diff --git a/common/src/test/kotlin/TxMedataTest.kt b/common/src/test/kotlin/TxMedataTest.kt @@ -0,0 +1,100 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +import tech.libeufin.common.* +import kotlin.test.* + +class TxMetadataTest{ + fun assertFailsMsg(msg: String, lambda: () -> Unit) { + val failure = assertFails(lambda) + assertEquals(msg, failure.message) + } + + @Test + fun parse() { + val upper = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0" + val upperL = "4MZT6RS3RVB3B0E2RDMYW0YRA3" + val upperR = "Y0VPHYV0CYDE6XBB0YMPFXCEG0" + val mixed = "4mzt6RS3rvb3b0e2rdmyw0yra3y0vphyv0cyde6xbb0ympfxceg0" + val mixedL = "4mzt6rs3rvb3b0e2rdmyw0yra3" + val mixedR = "y0vphyv0cyde6xbb0ympfxceg0" + val otherUpper = "TEST6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0" + val otherMixed = "test6rRSrvb3b0e2rdmyw0yra3y0vphyv0cyde6xbb0ympfxceg0" + val key = EddsaPublicKey(upper) + + // Check succeed if upper or mixed + for (case in sequenceOf(upper, mixed)) { + for (test in sequenceOf( + "noise $case noise", + "$case noise to the right", + "noise to the left $case", + " $case ", + "noise\n$case\nnoise", + "Test+$case" + )) { + assertEquals(key, parseIncomingTxMetadata(test)) + } + } + + // Check succeed if upper or mixed and split + for ((L, R) in sequenceOf(upperL to upperR, mixedL to mixedR)) { + for (case in sequenceOf( + "left $L$R right", + "left $L-$R right", + "left $L+$R right", + "left $L\n$R right", + "left $L-+\n$R right", + "left $L - $R right", + "left $L + $R right", + "left $L \n $R right", + "left $L - + \n $R right", + )) { + assertEquals(key, parseIncomingTxMetadata(case)) + } + } + + // Check prefer upper + for (case in sequenceOf( + "$mixed $upper", + "$mixedL-$mixedR $upperL-$upperR" + )) { + assertEquals(key, parseIncomingTxMetadata(case)) + } + + // Check failure when multiple keys match + for (case in sequenceOf( + "$upper $upper", // Both upper + "$mixed $mixed" // Both mixed + )) { + assertFailsMsg("Found multiple reserve public key") { + parseIncomingTxMetadata(case) + } + } + + // Check failure if malformed or missing + for (case in sequenceOf( + "does not contain any reserve", // Check fail if none + upper.substring(0, upper.length-1) // Check fail if missing char + )) { + assertFailsMsg("Missing reserve public key") { + parseIncomingTxMetadata(case) + } + } + } +} +\ No newline at end of file diff --git a/nexus/src/test/kotlin/Parsing.kt b/nexus/src/test/kotlin/Parsing.kt @@ -1,68 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * LibEuFin is free software; you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation; either version 3, or - * (at your option) any later version. - - * LibEuFin is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General - * Public License for more details. - - * You should have received a copy of the GNU Affero General Public - * License along with LibEuFin; see the file COPYING. If not, see - * <http://www.gnu.org/licenses/> - */ - -import org.junit.Test -import tech.libeufin.common.EddsaPublicKey -import tech.libeufin.common.TalerAmount -import tech.libeufin.common.parseIncomingTxMetadata -import tech.libeufin.nexus.getAmountNoCurrency -import kotlin.test.assertEquals -import kotlin.test.assertFails - -class Parsing { - - @Test - fun reservePublicKey() { - assertFails { parseIncomingTxMetadata("does not contain any reserve") } - val encoded = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0" - val key = EddsaPublicKey(encoded) - assertEquals(key, parseIncomingTxMetadata("noise $encoded noise")) - assertEquals(key, parseIncomingTxMetadata("$encoded noise to the right")) - assertEquals(key, parseIncomingTxMetadata("noise to the left $encoded")) - assertEquals(key, parseIncomingTxMetadata(" $encoded ")) - assertEquals(key, parseIncomingTxMetadata("noise\n$encoded\nnoise")) - assertEquals(key, parseIncomingTxMetadata("Test+$encoded")) - // Got the first char removed. - assertFails { parseIncomingTxMetadata("MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0") } - } - - @Test // Could be moved in a dedicated Amounts.kt test module. - fun generateCurrencyAgnosticAmount() { - assertFails { - // Too many fractional digits. - getAmountNoCurrency(TalerAmount(1, 123456789, "KUDOS")) - } - assertFails { - // Nexus doesn't support sub-cents. - getAmountNoCurrency(TalerAmount(1, 12345678, "KUDOS")) - } - assertFails { - // Nexus doesn't support sub-cents. - getAmountNoCurrency(TalerAmount(0, 1, "KUDOS")) - } - assertEquals( - "0.01", - getAmountNoCurrency(TalerAmount(0, 1000000, "KUDOS")) - ) - assertEquals( - "0.1", - getAmountNoCurrency(TalerAmount(0, 10000000, "KUDOS")) - ) - } -} -\ No newline at end of file