libeufin

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

commit 4f8b94faf1822d24ae1a4e9ece93bda2f797ab59
parent 0856649d9d96c97f466533a86210101b5c81d235
Author: MS <ms@taler.net>
Date:   Wed, 13 Sep 2023 16:26:33 +0200

Bank: error handling and amount parser.

Ktor's StatusPages catches any Exception thrown in the application
and try to build a Taler ErrorDetail to respond to the client.

Diffstat:
M.idea/gradle.xml | 1+
M.idea/workspace.xml | 162+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mbank/src/main/kotlin/tech/libeufin/bank/Database.kt | 8++++----
Abank/src/main/kotlin/tech/libeufin/bank/Helpers.kt | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 128++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Abank/src/test/kotlin/AmountTest.kt | 21+++++++++++++++++++++
Mbank/src/test/kotlin/DatabaseTest.kt | 4++++
Mbank/src/test/kotlin/LibeuFinApiTest.kt | 18+++++++++++++-----
Dutil/src/test/kotlin/AmountTest.kt | 46----------------------------------------------
9 files changed, 316 insertions(+), 159 deletions(-)

diff --git a/.idea/gradle.xml b/.idea/gradle.xml @@ -10,6 +10,7 @@ <set> <option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$/bank" /> + <option value="$PROJECT_DIR$/nexus" /> <option value="$PROJECT_DIR$/util" /> </set> </option> diff --git a/.idea/workspace.xml b/.idea/workspace.xml @@ -5,74 +5,16 @@ </component> <component name="ChangeListManager"> <list default="true" id="9436eb1e-de48-4f11-8ff7-f359340cb458" name="Changes" comment=""> - <change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> - <change afterPath="$PROJECT_DIR$/bank/src/test/kotlin/LibeuFinApiTest.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/.idea/$PRODUCT_WORKSPACE_FILE$" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/.idea/.gitignore" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/.idea/codeStyles/Project.xml" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/.idea/codeStyles/codeStyleConfig.xml" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/.idea/dictionaries/dold.xml" beforeDir="false" /> + <change afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/Helpers.kt" afterDir="false" /> + <change afterPath="$PROJECT_DIR$/bank/src/test/kotlin/AmountTest.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/gradle.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/gradle.xml" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/.idea/inspectionProfiles/Project_Default.xml" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/.idea/kotlinc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/kotlinc.xml" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/.idea/libraries-with-intellij-classes.xml" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/.idea/runConfigurations/SchedulingTest.xml" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/.idea/runConfigurations/run_sandbox.xml" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/.idea/runConfigurations/test_nexus.xml" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/.idea/runConfigurations/test_sandbox.xml" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/.idea/uiDesigner.xml" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/bank/README" beforeDir="false" afterPath="$PROJECT_DIR$/bank/README" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/bank/build.gradle" beforeDir="false" afterPath="$PROJECT_DIR$/bank/build.gradle" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/CircuitApi.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/CircuitApi.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/ConversionService.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/ConversionService.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/DB.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/DB.kt" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/Database.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/Database.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/EbicsProtocolBackend.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/EbicsProtocolBackend.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/Helpers.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/Helpers.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/JSON.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/JSON.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/Main.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/Main.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/XMLEbicsConverter.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/XMLEbicsConverter.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/bankAccount.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/main/kotlin/tech/libeufin/bank/bankAccount.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/bank/src/main/resources/logback.xml" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/main/resources/logback.xml" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/bank/src/test/kotlin/BalanceTest.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/test/kotlin/BalanceTest.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/bank/src/test/kotlin/DBTest.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/test/kotlin/DBTest.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/bank/src/test/kotlin/DatabaseTest.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/test/kotlin/DatabaseTest.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/bank/src/test/kotlin/EbicsErrorTest.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/test/kotlin/EbicsErrorTest.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/bank/src/test/kotlin/StringsTest.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/test/kotlin/StringsTest.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/database-versioning/new/libeufin-bank-0001.sql" beforeDir="false" afterPath="$PROJECT_DIR$/database-versioning/new/libeufin-bank-0001.sql" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/database-versioning/new/procedures.sql" beforeDir="false" afterPath="$PROJECT_DIR$/database-versioning/new/procedures.sql" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/build.gradle" beforeDir="false" afterPath="$PROJECT_DIR$/nexus/build.gradle" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/ConversionServiceTest.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/DbEventTest.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/EbicsTest.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/Iso20022Test.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/JsonTest.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/LetterFormatTest.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/MakeEnv.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/NexusApiTest.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/PainTest.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/PostFinance.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/SandboxAccessApiTest.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/SandboxBankAccountTest.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/SandboxCircuitApiTest.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/SandboxLegacyApiTest.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/SchedulingTest.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/SplitString.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/SubjectNormalization.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/TalerTest.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/XLibeufinBankTest.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/kotlin/XPathTest.kt" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/resources/iso20022-samples/camt.053/de.camt.053.001.02.xml" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/nexus/src/test/resources/logback-test.xml" beforeDir="false" /> - <change beforePath="$PROJECT_DIR$/util/build.gradle" beforeDir="false" afterPath="$PROJECT_DIR$/util/build.gradle" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/util/src/main/kotlin/Config.kt" beforeDir="false" afterPath="$PROJECT_DIR$/util/src/main/kotlin/Config.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/util/src/main/kotlin/DB.kt" beforeDir="false" afterPath="$PROJECT_DIR$/util/src/main/kotlin/DB.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/util/src/main/kotlin/HTTP.kt" beforeDir="false" afterPath="$PROJECT_DIR$/util/src/main/kotlin/HTTP.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/util/src/main/kotlin/iban.kt" beforeDir="false" afterPath="$PROJECT_DIR$/util/src/main/kotlin/iban.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/util/src/main/kotlin/startServer.kt" beforeDir="false" afterPath="$PROJECT_DIR$/util/src/main/kotlin/startServer.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/util/src/main/kotlin/time.kt" beforeDir="false" afterPath="$PROJECT_DIR$/util/src/main/kotlin/time.kt" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/util/src/test/kotlin/StartServerTest.kt" beforeDir="false" /> + <change beforePath="$PROJECT_DIR$/bank/src/test/kotlin/LibeuFinApiTest.kt" beforeDir="false" afterPath="$PROJECT_DIR$/bank/src/test/kotlin/LibeuFinApiTest.kt" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/contrib/wallet-core" beforeDir="false" afterPath="$PROJECT_DIR$/contrib/wallet-core" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/util/src/test/kotlin/AmountTest.kt" beforeDir="false" /> </list> <option name="SHOW_DIALOG" value="false" /> <option name="HIGHLIGHT_CONFLICTS" value="true" /> @@ -90,10 +32,28 @@ <task path="$PROJECT_DIR$"> <activation /> </task> - <projects_view /> + <projects_view> + <tree_state> + <expand> + <path> + <item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" /> + <item name="libeufin" type="f1a62948:ProjectNode" /> + </path> + </expand> + <select /> + </tree_state> + </projects_view> </state> </system> </component> + <component name="FileTemplateManagerImpl"> + <option name="RECENT_TEMPLATES"> + <list> + <option value="Kotlin Class" /> + <option value="Kotlin File" /> + </list> + </option> + </component> <component name="Git.Settings"> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> </component> @@ -108,11 +68,16 @@ <component name="PropertiesComponent">{ &quot;keyToString&quot;: { &quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;, - &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot; + &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;, + &quot;project.structure.last.edited&quot;: &quot;Project&quot;, + &quot;project.structure.proportion&quot;: &quot;0.0&quot;, + &quot;project.structure.side.proportion&quot;: &quot;0.0&quot;, + &quot;settings.editor.selected.configurable&quot;: &quot;project.kotlinCompiler&quot;, + &quot;settings.editor.splitter.proportion&quot;: &quot;0.31419808&quot; } }</component> - <component name="RunManager" selected="Gradle.LibeuFinApiTest.createAccountTest"> - <configuration name="DatabaseTest" type="GradleRunConfiguration" factoryName="Gradle" temporary="true"> + <component name="RunManager" selected="Gradle.DatabaseTest.bankAccountTest"> + <configuration name="AmountTest.parseAmountTest" type="GradleRunConfiguration" factoryName="Gradle" temporary="true"> <ExternalSystemSettings> <option name="executionName" /> <option name="externalProjectPath" value="$PROJECT_DIR$" /> @@ -125,7 +90,7 @@ <list> <option value=":bank:test" /> <option value="--tests" /> - <option value="&quot;DatabaseTest&quot;" /> + <option value="&quot;AmountTest.parseAmountTest&quot;" /> </list> </option> <option name="vmOptions" /> @@ -135,7 +100,7 @@ <DebugAllEnabled>false</DebugAllEnabled> <method v="2" /> </configuration> - <configuration name="DatabaseTest.bearerTokenTest" type="GradleRunConfiguration" factoryName="Gradle" temporary="true"> + <configuration name="AmountTest.parseTalerAmountTest" type="GradleRunConfiguration" factoryName="Gradle" temporary="true"> <ExternalSystemSettings> <option name="executionName" /> <option name="externalProjectPath" value="$PROJECT_DIR$" /> @@ -148,7 +113,7 @@ <list> <option value=":bank:test" /> <option value="--tests" /> - <option value="&quot;DatabaseTest.bearerTokenTest&quot;" /> + <option value="&quot;AmountTest.parseTalerAmountTest&quot;" /> </list> </option> <option name="vmOptions" /> @@ -158,7 +123,30 @@ <DebugAllEnabled>false</DebugAllEnabled> <method v="2" /> </configuration> - <configuration name="LibeuFinApiTest.createAccountTest" type="GradleRunConfiguration" factoryName="Gradle" temporary="true"> + <configuration name="DatabaseTest.bankAccountTest" type="GradleRunConfiguration" factoryName="Gradle" temporary="true"> + <ExternalSystemSettings> + <option name="executionName" /> + <option name="externalProjectPath" value="$PROJECT_DIR$" /> + <option name="externalSystemIdString" value="GRADLE" /> + <option name="scriptParameters" value="" /> + <option name="taskDescriptions"> + <list /> + </option> + <option name="taskNames"> + <list> + <option value=":bank:test" /> + <option value="--tests" /> + <option value="&quot;DatabaseTest.bankAccountTest&quot;" /> + </list> + </option> + <option name="vmOptions" /> + </ExternalSystemSettings> + <ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess> + <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> + <DebugAllEnabled>false</DebugAllEnabled> + <method v="2" /> + </configuration> + <configuration name="JsonTest.deserializationTest" type="GradleRunConfiguration" factoryName="Gradle" temporary="true"> <ExternalSystemSettings> <option name="executionName" /> <option name="externalProjectPath" value="$PROJECT_DIR$" /> @@ -171,7 +159,7 @@ <list> <option value=":bank:test" /> <option value="--tests" /> - <option value="&quot;LibeuFinApiTest.createAccountTest&quot;" /> + <option value="&quot;JsonTest.deserializationTest&quot;" /> </list> </option> <option name="vmOptions" /> @@ -181,39 +169,43 @@ <DebugAllEnabled>false</DebugAllEnabled> <method v="2" /> </configuration> - <configuration name="libeufin [dependencies]" type="GradleRunConfiguration" factoryName="Gradle" temporary="true"> + <configuration name="LibeuFinApiTest.createAccountTest" type="GradleRunConfiguration" factoryName="Gradle" temporary="true"> <ExternalSystemSettings> <option name="executionName" /> <option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalSystemIdString" value="GRADLE" /> - <option name="scriptParameters" /> + <option name="scriptParameters" value="--quiet" /> <option name="taskDescriptions"> <list /> </option> <option name="taskNames"> <list> - <option value="dependencies" /> + <option value=":bank:test" /> + <option value="--tests" /> + <option value="&quot;LibeuFinApiTest.createAccountTest&quot;" /> </list> </option> <option name="vmOptions" /> </ExternalSystemSettings> - <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> + <ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess> <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> <DebugAllEnabled>false</DebugAllEnabled> <method v="2" /> </configuration> <list> - <item itemvalue="Gradle.libeufin [dependencies]" /> - <item itemvalue="Gradle.DatabaseTest" /> - <item itemvalue="Gradle.DatabaseTest.bearerTokenTest" /> + <item itemvalue="Gradle.DatabaseTest.bankAccountTest" /> + <item itemvalue="Gradle.AmountTest.parseTalerAmountTest" /> + <item itemvalue="Gradle.AmountTest.parseAmountTest" /> + <item itemvalue="Gradle.JsonTest.deserializationTest" /> <item itemvalue="Gradle.LibeuFinApiTest.createAccountTest" /> </list> <recent_temporary> <list> + <item itemvalue="Gradle.DatabaseTest.bankAccountTest" /> <item itemvalue="Gradle.LibeuFinApiTest.createAccountTest" /> - <item itemvalue="Gradle.libeufin [dependencies]" /> - <item itemvalue="Gradle.DatabaseTest" /> - <item itemvalue="Gradle.DatabaseTest.bearerTokenTest" /> + <item itemvalue="Gradle.AmountTest.parseTalerAmountTest" /> + <item itemvalue="Gradle.AmountTest.parseAmountTest" /> + <item itemvalue="Gradle.JsonTest.deserializationTest" /> </list> </recent_temporary> </component> @@ -228,4 +220,8 @@ </task> <servers /> </component> + <component name="XSLT-Support.FileAssociations.UIState"> + <expand /> + <select /> + </component> </project> \ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt @@ -15,10 +15,10 @@ data class Customer( val passwordHash: String, val name: String, val dbRowId: Long? = null, // mostly used when retrieving records. - val email: String?, - val phone: String?, - val cashoutPayto: String?, - val cashoutCurrency: String? + val email: String? = null, + val phone: String? = null, + val cashoutPayto: String? = null, + val cashoutCurrency: String? = null ) fun Customer.expectRowId(): Long = this.dbRowId ?: throw internalServerError("Cutsomer '${this.login}' had no DB row ID") diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/Helpers.kt @@ -0,0 +1,86 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2019 Stanisci and Dold. + + * 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/> + */ + +package tech.libeufin.bank + +import io.ktor.http.* +import tech.libeufin.util.getIban +import java.lang.NumberFormatException + +// HELPERS. FIXME: make unit tests for them. + +fun internalServerError(hint: String): LibeufinBankException = + LibeufinBankException( + httpStatus = HttpStatusCode.InternalServerError, + talerError = TalerError( + code = GENERIC_INTERNAL_INVARIANT_FAILURE, + hint = hint + ) + ) +// Generates a new Payto-URI with IBAN scheme. +fun genIbanPaytoUri(): String = "payto://iban/SANDBOXX/${getIban()}" + +fun parseTalerAmount( + amount: String, + fracDigits: FracDigits = FracDigits.EIGHT +): TalerAmount { + val format = when (fracDigits) { + FracDigits.TWO -> + Pair("^([A-Z]+):([0-9])(\\.[0-9][0-9]?)?$", 100) + FracDigits.EIGHT -> + Pair( + "^([A-Z]+):([0-9])(\\.[0-9][0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?)?\$", + 100000000 + ) + } + val match = Regex(format.first).find(amount) ?: throw LibeufinBankException( + httpStatus = HttpStatusCode.BadRequest, + talerError = TalerError( + code = BANK_BAD_FORMAT_AMOUNT, + hint = "Invalid amount: $amount" + )) + val _value = match.destructured.component2() + // Fraction is at most 8 digits, so it's always < than MAX_INT. + val fraction: Int = match.destructured.component3().run { + var frac = 0 + var power = format.second + if (this.isNotEmpty()) + // Skips the dot and processes the fractional chars. + this.substring(1).forEach { chr -> + power /= 10 + frac += power * chr.digitToInt() + } + return@run frac + } + val value: Long = try { + _value.toLong() + } catch (e: NumberFormatException) { + throw LibeufinBankException( + httpStatus = HttpStatusCode.BadRequest, + talerError = TalerError( + code = BANK_BAD_FORMAT_AMOUNT, + hint = "Invalid amount: ${amount}, could not extract the value part." + ) + ) + } + return TalerAmount( + value = value, + frac = fraction + ) +} +\ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -1,3 +1,22 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2019 Stanisci and Dold. + + * 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/> + */ + package tech.libeufin.bank import io.ktor.http.* @@ -22,11 +41,25 @@ import tech.libeufin.util.* // GLOBALS val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank") val db = Database(System.getProperty("BANK_DB_CONNECTION_STRING")) +// fixme: make enum out of error codes. const val GENERIC_JSON_INVALID = 22 const val GENERIC_PARAMETER_MALFORMED = 26 const val GENERIC_PARAMETER_MISSING = 25 +const val BANK_UNMANAGED_EXCEPTION = 5110 +const val BANK_BAD_FORMAT_AMOUNT = 5108 +const val GENERIC_HTTP_HEADERS_MALFORMED = 23 +const val GENERIC_INTERNAL_INVARIANT_FAILURE = 60 +const val BANK_LOGIN_FAILED = 5105 +const val GENERIC_UNAUTHORIZED = 40 +const val GENERIC_UNDEFINED = -1 // Filler for ECs that don't exist yet. // TYPES + +enum class FracDigits(howMany: Int) { + TWO(2), + EIGHT(8) +} + @Serializable data class TalerError( val code: Int, @@ -50,19 +83,11 @@ data class RegisterAccountRequest( val internal_payto_uri: String? = null ) -// Generates a new Payto-URI with IBAN scheme. -fun genIbanPaytoUri(): String = "payto://iban/SANDBOXX/${getIban()}" -fun parseTalerAmount(amount: String): TalerAmount { - val amountWithCurrencyRe = "^([A-Z]+):([0-9]+(\\.[0-9][0-9]?)?)$" - val match = Regex(amountWithCurrencyRe).find(amount) ?: - throw badRequest("Invalid amount") - val value = match.destructured.component2() - val fraction: Int = match.destructured.component3().run { - if (this.isEmpty()) return@run 0 - return@run this.substring(1).toInt() - } - return TalerAmount(value.toLong(), fraction) -} +class LibeufinBankException( + val httpStatus: HttpStatusCode, + val talerError: TalerError +) : Exception(talerError.hint) + /** * Performs the HTTP basic authentication. Returns the @@ -79,7 +104,14 @@ fun doBasicAuth(encodedCredentials: String): Customer? { */ limit = 2 ) - if (userAndPassSplit.size != 2) throw badRequest("Malformed Basic auth credentials found in the Authorization header.") + if (userAndPassSplit.size != 2) + throw LibeufinBankException( + httpStatus = HttpStatusCode.BadRequest, + talerError = TalerError( + code = GENERIC_HTTP_HEADERS_MALFORMED, // 23 + "Malformed Basic auth credentials found in the Authorization header." + ) + ) val login = userAndPassSplit[0] val plainPassword = userAndPassSplit[1] return db.customerPwAuth(login, CryptoUtil.hashpw(plainPassword)) @@ -96,7 +128,12 @@ fun doTokenAuth( if (isExpired || maybeToken.scope != requiredScope) return null // FIXME: mention the reason? // Getting the related username. return db.customerGetFromRowId(maybeToken.bankCustomer) - ?: throw internalServerError("Customer not found, despite token mentions it.") + ?: throw LibeufinBankException( + httpStatus = HttpStatusCode.InternalServerError, + talerError = TalerError( + code = GENERIC_INTERNAL_INVARIANT_FAILURE, + hint = "Customer not found, despite token mentions it.", + )) } /** @@ -115,7 +152,13 @@ fun ApplicationCall.myAuth(requiredScope: TokenScope): Customer? { return when (authDetails.scheme) { "Basic" -> doBasicAuth(authDetails.content) "Bearer" -> doTokenAuth(authDetails.content, requiredScope) - else -> throw badRequest("Authorization scheme '${authDetails.scheme}' is not supported.") + else -> throw LibeufinBankException( + httpStatus = HttpStatusCode.Unauthorized, + talerError = TalerError( + code = GENERIC_UNAUTHORIZED, + hint = "Authorization method wrong or not supported." + ) + ) } } @@ -145,8 +188,15 @@ val webApp: Application.() -> Unit = { } install(RequestValidation) install(StatusPages) { + /** + * This branch triggers when the Ktor layers detect one + * invalid request. It _might_ be thrown by the bank's + * actual logic, but that should be avoided because this + * (Ktor native) type doesn't easily map to the Taler error + * format. + */ exception<BadRequestException> {call, cause -> - // Discouraged use, but the only helpful message. + // Discouraged use, but the only helpful message: var rootCause: Throwable? = cause.cause while (rootCause?.cause != null) rootCause = rootCause.cause @@ -158,12 +208,36 @@ val webApp: Application.() -> Unit = { else -> GENERIC_JSON_INVALID // 22 } call.respond( - HttpStatusCode.BadRequest, - TalerError( + status = HttpStatusCode.BadRequest, + message = TalerError( code = talerErrorCode, hint = rootCause?.message )) } + /** + * This branch triggers when a bank handler throws it, and namely + * after one logical failure of the request(-handling). This branch + * should be preferred to catch errors, as it allows to include the + * Taler specific error detail. + */ + exception<LibeufinBankException> {call, cause -> + logger.error(cause.talerError.hint) + call.respond( + status = cause.httpStatus, + message = cause.talerError + ) + } + // Catch-all branch to mean that the bank wasn't able to manage one error. + exception<Exception> {call, cause -> + logger.error(cause.message) + call.respond( + status = HttpStatusCode.InternalServerError, + message = TalerError( + code = BANK_UNMANAGED_EXCEPTION,// 5110, bank's fault + hint = cause.message + ) + ) + } } routing { post("/accounts") { @@ -173,13 +247,25 @@ val webApp: Application.() -> Unit = { val customer: Customer? = call.myAuth(TokenScope.readwrite) if (customer == null || customer.login != "admin") // OK to leak the only-admin policy here? - throw unauthorized("Only admin allowed, and it failed to authenticate.") + throw LibeufinBankException( + httpStatus = HttpStatusCode.Unauthorized, + talerError = TalerError( + code = BANK_LOGIN_FAILED, + hint = "Only admin allowed." + ) + ) } // auth passed, proceed with activity. val req = call.receive<RegisterAccountRequest>() // Prohibit reserved usernames: if (req.username == "admin" || req.username == "bank") - throw conflict("Username '${req.username}' is reserved.") + throw LibeufinBankException( + httpStatus = HttpStatusCode.Conflict, + talerError = TalerError( + code = GENERIC_UNDEFINED, // FIXME: this waits GANA. + hint = "Username '${req.username}' is reserved." + ) + ) // Checking imdepotency. val maybeCustomerExists = db.customerGetFromLogin(req.username) // Can be null if previous call crashed before completion. diff --git a/bank/src/test/kotlin/AmountTest.kt b/bank/src/test/kotlin/AmountTest.kt @@ -0,0 +1,20 @@ +import org.junit.Test +import tech.libeufin.bank.parseTalerAmount + +class AmountTest { + @Test + fun parseTalerAmountTest() { + val one = "EUR:1" + var obj = parseTalerAmount(one) + assert(obj.value == 1L && obj.frac == 0) + val onePointZero = "EUR:1.00" + obj = parseTalerAmount(onePointZero) + assert(obj.value == 1L && obj.frac == 0) + val onePointZeroOne = "EUR:1.01" + obj = parseTalerAmount(onePointZeroOne) + assert(obj.value == 1L && obj.frac == 1000000) + // Invalid tries + obj = parseTalerAmount("EUR:0.00000001") + assert(obj.value == 0L && obj.frac == 1) + } +} +\ No newline at end of file diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt @@ -41,6 +41,10 @@ class DatabaseTest { ) fun initDb(): Database { + System.setProperty( + "BANK_DB_CONNECTION_STRING", + "jdbc:postgresql:///libeufincheck" + ) execCommand( listOf( "libeufin-bank-dbinit", diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt b/bank/src/test/kotlin/LibeuFinApiTest.kt @@ -4,6 +4,7 @@ import io.ktor.http.* import io.ktor.server.testing.* import kotlinx.serialization.json.Json import org.junit.Test +import tech.libeufin.bank.Customer import tech.libeufin.bank.Database import tech.libeufin.bank.RegisterAccountRequest import tech.libeufin.bank.webApp @@ -11,6 +12,10 @@ import tech.libeufin.util.execCommand class LibeuFinApiTest { fun initDb(): Database { + System.setProperty( + "BANK_DB_CONNECTION_STRING", + "jdbc:postgresql:///libeufincheck" + ) execCommand( listOf( "libeufin-bank-dbinit", @@ -26,15 +31,17 @@ class LibeuFinApiTest { @Test fun createAccountTest() { testApplication { - System.setProperty( - "BANK_DB_CONNECTION_STRING", - "jdbc:postgresql:///libeufincheck" - ) val db = initDb() db.configSet("max_debt_ordinary_customers", "KUDOS:11") db.configSet("only_admin_registrations", "yes") + db.customerCreate(Customer( + "admin", + "pass", + "CFO" + )) application(webApp) - client.post("/accounts") { + val resp = client.post("/accounts") { + expectSuccess = false contentType(ContentType.Application.Json) basicAuth("admin", "bar") setBody("""{ @@ -43,6 +50,7 @@ class LibeuFinApiTest { "name": "Jane" }""".trimIndent()) } + println("Resp status code: ${resp.status}") } } } \ No newline at end of file diff --git a/util/src/test/kotlin/AmountTest.kt b/util/src/test/kotlin/AmountTest.kt @@ -1,45 +0,0 @@ -import io.ktor.util.reflect.* -import org.junit.Test -import tech.libeufin.util.isAmountZero -import tech.libeufin.util.parseAmount -import tech.libeufin.util.validatePlainAmount -import java.math.BigDecimal -import kotlin.reflect.typeOf - -inline fun <reified ExceptionType> assertException(block: () -> Unit) { - try { - block() - } catch (e: Throwable) { - assert(e.javaClass == ExceptionType::class.java) - return - } - return assert(false) -} -class AmountTest { - @Test - fun equalTest() { - assert(isAmountZero(BigDecimal("-0000000000.0000000000"))) - assert(!isAmountZero(BigDecimal("1"))) - assert(isAmountZero(BigDecimal("0.00"))) - assert(isAmountZero(BigDecimal("0"))) - assert(!isAmountZero(BigDecimal("00000000000001"))) - assert(!isAmountZero(BigDecimal("-1.00000000"))) - } - - @Test - fun parse() { - var res = parseAmount("KUDOS:5.5") - assert(res.amount == "5.5") - assert(res.currency == "KUDOS") - assert(validatePlainAmount("1.0")) - assert(validatePlainAmount("1.00")) - assert(!validatePlainAmount("1.000")) - res = parseAmount("TESTKUDOS:1.11") - assert(res.amount == "1.11") - assert(res.currency == "TESTKUDOS") - assertException<UtilError> { parseAmount("TESTKUDOS:1.") } - assertException<UtilError> { parseAmount("TESTKUDOS:.1") } - assertException<UtilError> { parseAmount("TESTKUDOS:1.000") } - assertException<UtilError> { parseAmount("TESTKUDOS:1..") } - } -} -\ No newline at end of file