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:
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">{
"keyToString": {
"RunOnceActivity.OpenProjectViewOnStart": "true",
- "RunOnceActivity.ShowReadmeOnStart": "true"
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "project.structure.last.edited": "Project",
+ "project.structure.proportion": "0.0",
+ "project.structure.side.proportion": "0.0",
+ "settings.editor.selected.configurable": "project.kotlinCompiler",
+ "settings.editor.splitter.proportion": "0.31419808"
}
}</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=""DatabaseTest"" />
+ <option value=""AmountTest.parseAmountTest"" />
</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=""DatabaseTest.bearerTokenTest"" />
+ <option value=""AmountTest.parseTalerAmountTest"" />
</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=""DatabaseTest.bankAccountTest"" />
+ </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=""LibeuFinApiTest.createAccountTest"" />
+ <option value=""JsonTest.deserializationTest"" />
</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=""LibeuFinApiTest.createAccountTest"" />
</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