summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntoine A <>2024-01-09 01:26:57 +0000
committerAntoine A <>2024-01-09 01:26:57 +0000
commit6bac5cf1c5d642d6e0a0a9afbc6c90cb0291c78b (patch)
tree12f1de2713b85ced196ef1771a26a1e871c897b2
parentf24be2dae4b9d081825d9409f9dd00afca7181bc (diff)
downloadlibeufin-6bac5cf1c5d642d6e0a0a9afbc6c90cb0291c78b.tar.gz
libeufin-6bac5cf1c5d642d6e0a0a9afbc6c90cb0291c78b.tar.bz2
libeufin-6bac5cf1c5d642d6e0a0a9afbc6c90cb0291c78b.zip
Improve ebics cli error handling and improve logic
-rw-r--r--.gitignore1
-rw-r--r--integration/src/main/kotlin/Main.kt24
-rw-r--r--nexus/conf/test.conf13
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt127
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt183
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt43
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt59
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt8
-rw-r--r--nexus/src/test/kotlin/CliTest.kt98
-rw-r--r--nexus/src/test/kotlin/Keys.kt16
-rw-r--r--nexus/src/test/kotlin/PostFinance.kt219
-rw-r--r--util/src/main/kotlin/Cli.kt13
-rw-r--r--util/src/main/kotlin/DB.kt2
13 files changed, 327 insertions, 479 deletions
diff --git a/.gitignore b/.gitignore
index a680a8b8..8d1b734f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
/sandbox/bin/
/util/bin/
nexus/libeufin-nexus-dev
+nexus/test
integration/test
integration/config.json
sandbox/libeufin-sandbox-dev
diff --git a/integration/src/main/kotlin/Main.kt b/integration/src/main/kotlin/Main.kt
index 074803a5..a18bf461 100644
--- a/integration/src/main/kotlin/Main.kt
+++ b/integration/src/main/kotlin/Main.kt
@@ -35,6 +35,7 @@ import java.time.Instant
import kotlinx.coroutines.runBlocking
import io.ktor.client.request.*
import net.taler.wallet.crypto.Base32Crockford
+import kotlin.io.path.*
fun randBytes(lenght: Int): ByteArray {
val bytes = ByteArray(lenght)
@@ -56,30 +57,29 @@ fun ask(question: String): String? {
}
fun CliktCommandTestResult.assertOk(msg: String? = null) {
- assertEquals(0, statusCode, "msg\n$output")
- println(output)
+ assertEquals(0, statusCode, msg)
}
fun CliktCommandTestResult.assertErr(msg: String? = null) {
- assertEquals(1, statusCode, "msg\n$output")
- println(output)
+ assertEquals(1, statusCode, msg)
}
class PostFinanceCli : CliktCommand("Run tests on postfinance", name="postfinance") {
override fun run() {
runBlocking {
- Files.createDirectories(Paths.get("test/postfinance"))
+ Path("test/postfinance").createDirectories()
val conf = "conf/postfinance.conf"
- val clientKeysPath = Paths.get("test/postfinance/client-keys.json")
- val bankKeysPath = Paths.get("test/postfinance/bank-keys.json")
-
- var hasClientKeys = Files.exists(clientKeysPath)
- var hasBankKeys = Files.exists(bankKeysPath)
+ val cfg = loadConfig(conf)
+ val clientKeysPath = Path(cfg.requireString("nexus-ebics", "client_private_keys_file"))
+ val bankKeysPath = Path(cfg.requireString("nexus-ebics", "bank_public_keys_file"))
+
+ var hasClientKeys = clientKeysPath.exists()
+ var hasBankKeys = bankKeysPath.exists()
if (hasClientKeys || hasBankKeys) {
if (ask("Reset keys ? y/n>") == "y") {
- if (hasClientKeys) Files.deleteIfExists(clientKeysPath)
- if (hasBankKeys) Files.deleteIfExists(bankKeysPath)
+ if (hasClientKeys) clientKeysPath.deleteIfExists()
+ if (hasBankKeys) bankKeysPath.deleteIfExists()
hasClientKeys = false
hasBankKeys = false
}
diff --git a/nexus/conf/test.conf b/nexus/conf/test.conf
new file mode 100644
index 00000000..0235dde1
--- /dev/null
+++ b/nexus/conf/test.conf
@@ -0,0 +1,13 @@
+[nexus-ebics]
+currency = CHF
+BANK_DIALECT = postfinance
+HOST_BASE_URL = https://isotest.postfinance.ch/ebicsweb/ebicsweb
+BANK_PUBLIC_KEYS_FILE = test/tmp/bank-keys.json
+CLIENT_PRIVATE_KEYS_FILE = test/tmp/client-keys.json
+IBAN = CH7789144474425692816
+HOST_ID = PFEBICS
+USER_ID = PFC00563
+PARTNER_ID = PFC00563
+
+[nexus-postgres]
+CONFIG = postgres:///libeufincheck \ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
index 72975e38..ba7956e3 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
@@ -528,9 +528,7 @@ class EbicsFetch: CliktCommand("Fetches bank records. Defaults to camt.054 noti
*/
override fun run() = cliCmd(logger) {
val cfg: EbicsSetupConfig = extractEbicsConfig(common.config)
-
val dbCfg = cfg.config.dbConfig()
- val db = Database(dbCfg.dbConnStr)
// Deciding what to download.
var whichDoc = SupportedDocument.CAMT_054
@@ -538,78 +536,81 @@ class EbicsFetch: CliktCommand("Fetches bank records. Defaults to camt.054 noti
if (onlyReports) whichDoc = SupportedDocument.CAMT_052
if (onlyStatements) whichDoc = SupportedDocument.CAMT_053
if (onlyLogs) whichDoc = SupportedDocument.PAIN_002_LOGS
- if (parse || import) {
- logger.debug("Reading from STDIN, running in debug mode. Not involving the database.")
- val maybeStdin = generateSequence(::readLine).joinToString("\n")
- when(whichDoc) {
- SupportedDocument.CAMT_054 -> {
- val incomingTxs = mutableListOf<IncomingPayment>()
- val outgoingTxs = mutableListOf<OutgoingPayment>()
- parseTxNotif(maybeStdin, cfg.currency, incomingTxs, outgoingTxs)
- println(incomingTxs)
- println(outgoingTxs)
- if (import) {
- runBlocking {
- incomingTxs.forEach {
- ingestIncomingPayment(db, it)
- }
- outgoingTxs.forEach {
- ingestOutgoingPayment(db, it)
+ Database(dbCfg.dbConnStr).use { db ->
+ if (parse || import) {
+ logger.debug("Reading from STDIN, running in debug mode. Not involving the database.")
+ val maybeStdin = generateSequence(::readLine).joinToString("\n")
+ when(whichDoc) {
+ SupportedDocument.CAMT_054 -> {
+ val incomingTxs = mutableListOf<IncomingPayment>()
+ val outgoingTxs = mutableListOf<OutgoingPayment>()
+
+ parseTxNotif(maybeStdin, cfg.currency, incomingTxs, outgoingTxs)
+ println(incomingTxs)
+ println(outgoingTxs)
+ if (import) {
+ runBlocking {
+ incomingTxs.forEach {
+ ingestIncomingPayment(db, it)
+ }
+ outgoingTxs.forEach {
+ ingestOutgoingPayment(db, it)
+ }
}
}
}
+ else -> throw Exception("Parsing $whichDoc not supported")
}
- else -> throw Error("Parsing $whichDoc not supported")
+ return@cliCmd
}
- return@cliCmd
- }
- val (clientKeys, bankKeys) = expectFullKeys(cfg)
- val ctx = FetchContext(
- cfg,
- HttpClient(),
- clientKeys,
- bankKeys,
- whichDoc,
- EbicsVersion.three,
- ebicsExtraLog
- )
- if (transient) {
- logger.info("Transient mode: fetching once and returning.")
- val pinnedStartVal = pinnedStart
- val pinnedStartArg = if (pinnedStartVal != null) {
- logger.debug("Pinning start date to: $pinnedStartVal")
- // Converting YYYY-MM-DD to Instant.
- LocalDate.parse(pinnedStartVal).atStartOfDay(ZoneId.of("UTC")).toInstant()
- } else null
- ctx.pinnedStart = pinnedStartArg
- if (whichDoc == SupportedDocument.PAIN_002_LOGS)
- ctx.ebicsVersion = EbicsVersion.two
- runBlocking {
- fetchDocuments(db, ctx)
- }
- return@cliCmd
- }
- val configValue = cfg.config.requireString("nexus-fetch", "frequency")
- val frequencySeconds = checkFrequency(configValue)
- val frequency: NexusFrequency = NexusFrequency(frequencySeconds, configValue)
- logger.debug("Running with a frequency of ${frequency.fromConfig}")
- if (frequency.inSeconds == 0) {
- logger.warn("Long-polling not implemented, running therefore in transient mode")
- runBlocking {
- fetchDocuments(db, ctx)
+ val (clientKeys, bankKeys) = expectFullKeys(cfg)
+ val ctx = FetchContext(
+ cfg,
+ HttpClient(),
+ clientKeys,
+ bankKeys,
+ whichDoc,
+ EbicsVersion.three,
+ ebicsExtraLog
+ )
+ if (transient) {
+ logger.info("Transient mode: fetching once and returning.")
+ val pinnedStartVal = pinnedStart
+ val pinnedStartArg = if (pinnedStartVal != null) {
+ logger.debug("Pinning start date to: $pinnedStartVal")
+ // Converting YYYY-MM-DD to Instant.
+ LocalDate.parse(pinnedStartVal).atStartOfDay(ZoneId.of("UTC")).toInstant()
+ } else null
+ ctx.pinnedStart = pinnedStartArg
+ if (whichDoc == SupportedDocument.PAIN_002_LOGS)
+ ctx.ebicsVersion = EbicsVersion.two
+ runBlocking {
+ fetchDocuments(db, ctx)
+ }
+ return@cliCmd
}
- return@cliCmd
- }
- fixedRateTimer(
- name = "ebics submit period",
- period = (frequency.inSeconds * 1000).toLong(),
- action = {
+ val configValue = cfg.config.requireString("nexus-fetch", "frequency")
+ val frequencySeconds = checkFrequency(configValue)
+ val frequency: NexusFrequency = NexusFrequency(frequencySeconds, configValue)
+ logger.debug("Running with a frequency of ${frequency.fromConfig}")
+ if (frequency.inSeconds == 0) {
+ logger.warn("Long-polling not implemented, running therefore in transient mode")
runBlocking {
fetchDocuments(db, ctx)
}
+ return@cliCmd
}
- )
+ fixedRateTimer(
+ name = "ebics submit period",
+ period = (frequency.inSeconds * 1000).toLong(),
+ action = {
+ runBlocking {
+ fetchDocuments(db, ctx)
+ }
+ }
+ )
+ }
}
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
index 5fd0c985..40a09d7f 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
@@ -33,6 +33,9 @@ import tech.libeufin.util.*
import tech.libeufin.util.ebics_h004.HTDResponseOrderData
import java.time.Instant
import kotlin.reflect.typeOf
+import java.nio.file.Files
+import java.nio.file.StandardCopyOption
+import kotlin.io.path.*
/**
* Writes the JSON content to disk. Used when we create or update
@@ -40,23 +43,23 @@ import kotlin.reflect.typeOf
* silently what's found under the given location!
*
* @param obj the class representing the JSON content to store to disk.
- * @param location where to store `obj`
- * @return true in case of success, false otherwise.
+ * @param path where to store `obj`
*/
-inline fun <reified T> syncJsonToDisk(obj: T, location: String): Boolean {
- val fileContent = try {
+inline fun <reified T> syncJsonToDisk(obj: T, path: String) {
+ val content = try {
myJson.encodeToString(obj)
} catch (e: Exception) {
- logger.error("Could not encode the input '${typeOf<T>()}' to JSON, detail: ${e.message}")
- return false
+ throw Exception("Could not encode the input '${typeOf<T>()}' to JSON", e)
}
try {
- File(location).writeText(fileContent)
+ // Write to temp file then rename to enable atomicity when possible
+ val path = Path(path).absolute()
+ val tmp = Files.createTempFile(path.parent, "tmp_", "_${path.fileName}")
+ tmp.writeText(content)
+ tmp.moveTo(path, StandardCopyOption.REPLACE_EXISTING);
} catch (e: Exception) {
- logger.error("Could not write JSON content at $location, detail: ${e.message}")
- return false
+ throw Exception("Could not write JSON content at $path", e)
}
- return true
}
/**
@@ -72,42 +75,28 @@ fun generateNewKeys(): ClientPrivateKeysFile =
submitted_hia = false,
submitted_ini = false
)
-/**
- * Conditionally generates the client private keys and stores them
- * to disk, if the file does not exist already. Does nothing if the
- * file exists.
- *
- * @param filename keys file location
- * @return true if the keys file existed already or its creation
- * went through, false for any error.
- */
-fun maybeCreatePrivateKeysFile(filename: String): Boolean {
- val f = File(filename)
- // NOT overriding any file at the wanted location.
- if (f.exists()) {
- logger.debug("Private key file found at: $filename.")
- return true
- }
- val newKeys = generateNewKeys()
- if (!syncJsonToDisk(newKeys, filename))
- return false
- logger.info("New client keys created at: $filename")
- return true
-}
/**
* Obtains the client private keys, regardless of them being
* created for the first time, or read from an existing file
* on disk.
*
- * @param location path to the file that contains the keys.
- * @return true if the operation succeeds, false otherwise.
+ * @param path path to the file that contains the keys.
+ * @return current or new client keys
*/
-private fun preparePrivateKeys(location: String): ClientPrivateKeysFile? {
- if (!maybeCreatePrivateKeysFile(location)) {
- throw Error("Could not create client keys at $location")
+private fun preparePrivateKeys(path: String): ClientPrivateKeysFile {
+ // If exists load from disk
+ val current = loadPrivateKeysFromDisk(path)
+ if (current != null) return current
+ // Else create new keys
+ try {
+ val newKeys = generateNewKeys()
+ syncJsonToDisk(newKeys, path)
+ logger.info("New client keys created at: $path")
+ return newKeys
+ } catch (e: Exception) {
+ throw Exception("Could not create client keys at $path", e)
}
- return loadPrivateKeysFromDisk(location) // loads what found at location.
}
/**
@@ -157,47 +146,40 @@ private fun askUserToAcceptKeys(bankKeys: BankPublicKeysFile): Boolean {
*
* @param cfg used to get the location of the bank keys file.
* @param bankKeys bank response to the HPB message.
- * @return true if the keys were stored to disk (as "not accepted"),
- * false if the storage failed or the content was invalid.
*/
private fun handleHpbResponse(
cfg: EbicsSetupConfig,
bankKeys: EbicsKeyManagementResponseContent
-): Boolean {
+) {
val hpbBytes = bankKeys.orderData // silences compiler.
if (hpbBytes == null) {
- logger.error("HPB content not found in a EBICS response with successful return codes.")
- return false
+ throw Exception("HPB content not found in a EBICS response with successful return codes.")
}
val hpbObj = try {
parseEbicsHpbOrder(hpbBytes)
- }
- catch (e: Exception) {
- logger.error("HPB response content seems invalid.")
- return false
+ } catch (e: Exception) {
+ throw Exception("HPB response content seems invalid: e")
}
val encPub = try {
CryptoUtil.loadRsaPublicKey(hpbObj.encryptionPubKey.encoded)
} catch (e: Exception) {
- logger.error("Could not import bank encryption key from HPB response, detail: ${e.message}")
- return false
+ throw Exception("Could not import bank encryption key from HPB response", e)
}
val authPub = try {
CryptoUtil.loadRsaPublicKey(hpbObj.authenticationPubKey.encoded)
} catch (e: Exception) {
- logger.error("Could not import bank authentication key from HPB response, detail: ${e.message}")
- return false
+ throw Exception("Could not import bank authentication key from HPB response", e)
}
val json = BankPublicKeysFile(
bank_authentication_public_key = authPub,
bank_encryption_public_key = encPub,
accepted = false
)
- if (!syncJsonToDisk(json, cfg.bankPublicKeysFilename)) {
- logger.error("Failed to persist the bank keys to disk at: ${cfg.bankPublicKeysFilename}")
- return false
+ try {
+ syncJsonToDisk(json, cfg.bankPublicKeysFilename)
+ } catch (e: Exception) {
+ throw Exception("Failed to persist the bank keys to disk", e)
}
- return true
}
/**
@@ -211,15 +193,13 @@ private fun handleHpbResponse(
* @param orderType INI or HIA.
* @param autoAcceptBankKeys only given in case of HPB. Expresses
* the --auto-accept-key CLI flag.
- * @return true if the message fulfilled its purpose AND the state
- * on disk was accordingly updated, or false otherwise.
*/
suspend fun doKeysRequestAndUpdateState(
cfg: EbicsSetupConfig,
privs: ClientPrivateKeysFile,
client: HttpClient,
orderType: KeysOrderType
-): Boolean {
+) {
logger.debug("Doing key request ${orderType.name}")
val req = when(orderType) {
KeysOrderType.INI -> generateIniMessage(cfg, privs)
@@ -228,33 +208,29 @@ suspend fun doKeysRequestAndUpdateState(
}
val xml = client.postToBank(cfg.hostBaseUrl, req)
if (xml == null) {
- logger.error("Could not POST the ${orderType.name} message to the bank")
- return false
+ throw Exception("Could not POST the ${orderType.name} message to the bank")
}
val ebics = parseKeysMgmtResponse(privs.encryption_private_key, xml)
if (ebics == null) {
- logger.error("Could not get any EBICS from the bank ${orderType.name} response ($xml).")
- return false
+ throw Exception("Could not get any EBICS from the bank ${orderType.name} response ($xml).")
}
if (ebics.technicalReturnCode != EbicsReturnCode.EBICS_OK) {
- logger.error("EBICS ${orderType.name} failed with code: ${ebics.technicalReturnCode}")
- return false
+ throw Exception("EBICS ${orderType.name} failed with code: ${ebics.technicalReturnCode}")
}
if (ebics.bankReturnCode != EbicsReturnCode.EBICS_OK) {
- logger.error("EBICS ${orderType.name} reached the bank, but could not be fulfilled, error code: ${ebics.bankReturnCode}")
- return false
+ throw Exception("EBICS ${orderType.name} reached the bank, but could not be fulfilled, error code: ${ebics.bankReturnCode}")
}
- when(orderType) {
+ when (orderType) {
KeysOrderType.INI -> privs.submitted_ini = true
KeysOrderType.HIA -> privs.submitted_hia = true
KeysOrderType.HPB -> return handleHpbResponse(cfg, ebics)
}
- if (!syncJsonToDisk(privs, cfg.clientPrivateKeysFilename)) {
- logger.error("Could not update the ${orderType.name} state on disk")
- return false
+ try {
+ syncJsonToDisk(privs, cfg.clientPrivateKeysFilename)
+ } catch (e: Exception) {
+ throw Exception("Could not update the ${orderType.name} state on disk", e)
}
- return true
}
/**
@@ -279,12 +255,12 @@ private fun makePdf(privs: ClientPrivateKeysFile, cfg: EbicsSetupConfig) {
val pdf = generateKeysPdf(privs, cfg)
val pdfFile = File("/tmp/libeufin-nexus-keys-${Instant.now().epochSecond}.pdf")
if (pdfFile.exists()) {
- throw Error("PDF file exists already at: ${pdfFile.path}, not overriding it")
+ throw Exception("PDF file exists already at: ${pdfFile.path}, not overriding it")
}
try {
pdfFile.writeBytes(pdf)
} catch (e: Exception) {
- throw Error("Could not write PDF to ${pdfFile}, detail: ${e.message}")
+ throw Exception("Could not write PDF to ${pdfFile}, detail: ${e.message}")
}
println("PDF file with keys hex encoding created at: $pdfFile")
}
@@ -333,54 +309,39 @@ class EbicsSetup: CliktCommand("Set up the EBICS subscriber") {
return@cliCmd
}
// Config is sane. Go (maybe) making the private keys.
- val privsMaybe = preparePrivateKeys(cfg.clientPrivateKeysFilename)
- if (privsMaybe == null) {
- throw Error("Private keys preparation failed.")
- }
+ val clientKeys = preparePrivateKeys(cfg.clientPrivateKeysFilename)
val httpClient = HttpClient()
// Privs exist. Upload their pubs
- val keysNotSub = !privsMaybe.submitted_ini || !privsMaybe.submitted_hia
+ val keysNotSub = !clientKeys.submitted_ini || !clientKeys.submitted_hia
runBlocking {
- if ((!privsMaybe.submitted_ini) || forceKeysResubmission)
- doKeysRequestAndUpdateState(cfg, privsMaybe, httpClient, KeysOrderType.INI).apply { if (!this) throw Error() }
- if ((!privsMaybe.submitted_hia) || forceKeysResubmission)
- doKeysRequestAndUpdateState(cfg, privsMaybe, httpClient, KeysOrderType.HIA).apply { if (!this) throw Error() }
- }
- // Reloading new state from disk if any upload (and therefore a disk write) actually took place
- val haveSubmitted = forceKeysResubmission || keysNotSub
- val privs = if (haveSubmitted) {
- logger.info("Keys submitted to the bank, at ${cfg.hostBaseUrl}")
- loadPrivateKeysFromDisk(cfg.clientPrivateKeysFilename)
- } else privsMaybe
- if (privs == null) {
- throw Error("Could not reload private keys from disk after submission")
- }
- // Really both must be submitted here.
- if ((!privs.submitted_hia) || (!privs.submitted_ini)) {
- throw Error("Cannot continue with non-submitted client keys.")
+ if ((!clientKeys.submitted_ini) || forceKeysResubmission)
+ doKeysRequestAndUpdateState(cfg, clientKeys, httpClient, KeysOrderType.INI)
+ if ((!clientKeys.submitted_hia) || forceKeysResubmission)
+ doKeysRequestAndUpdateState(cfg, clientKeys, httpClient, KeysOrderType.HIA)
}
// Eject PDF if the keys were submitted for the first time, or the user asked.
- if (keysNotSub || generateRegistrationPdf) makePdf(privs, cfg)
+ if (keysNotSub || generateRegistrationPdf) makePdf(clientKeys, cfg)
// Checking if the bank keys exist on disk.
val bankKeysFile = File(cfg.bankPublicKeysFilename)
if (!bankKeysFile.exists()) {
- val areKeysOnDisk = runBlocking {
- doKeysRequestAndUpdateState(
- cfg,
- privs,
- httpClient,
- KeysOrderType.HPB
- )
- }
- if (!areKeysOnDisk) {
- throw Error("Could not download bank keys. Send client keys (and/or related PDF document with --generate-registration-pdf) to the bank.")
+ runBlocking {
+ try {
+ doKeysRequestAndUpdateState(
+ cfg,
+ clientKeys,
+ httpClient,
+ KeysOrderType.HPB
+ )
+ } catch (e: Exception) {
+ throw Exception("Could not download bank keys. Send client keys (and/or related PDF document with --generate-registration-pdf) to the bank", e)
+ }
}
logger.info("Bank keys stored at ${cfg.bankPublicKeysFilename}")
}
// bank keys made it to the disk, check if they're accepted.
val bankKeysMaybe = loadBankKeys(cfg.bankPublicKeysFilename)
if (bankKeysMaybe == null) {
- throw Error("Although previous checks, could not load the bank keys file from: ${cfg.bankPublicKeysFilename}")
+ throw Exception("Although previous checks, could not load the bank keys file from: ${cfg.bankPublicKeysFilename}")
}
if (!bankKeysMaybe.accepted) {
@@ -389,10 +350,12 @@ class EbicsSetup: CliktCommand("Set up the EBICS subscriber") {
else bankKeysMaybe.accepted = askUserToAcceptKeys(bankKeysMaybe)
if (!bankKeysMaybe.accepted) {
- throw Error("Cannot successfully finish the setup without accepting the bank keys.")
+ throw Exception("Cannot successfully finish the setup without accepting the bank keys.")
}
- if (!syncJsonToDisk(bankKeysMaybe, cfg.bankPublicKeysFilename)) {
- throw Error("Could not set bank keys as accepted on disk.")
+ try {
+ syncJsonToDisk(bankKeysMaybe, cfg.bankPublicKeysFilename)
+ } catch (e: Exception) {
+ throw Exception("Could not set bank keys as accepted on disk.", e)
}
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt
index b9bc2c38..061e64f7 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt
@@ -123,7 +123,7 @@ private fun maybeLog(
)
// Very rare: same pain.001 should not be submitted twice in the same microsecond.
if (f.exists()) {
- throw Error("pain.001 log file exists already at: $f")
+ throw Exception("pain.001 log file exists already at: $f")
}
f.writeText(xml)
}
@@ -271,7 +271,6 @@ class EbicsSubmit : CliktCommand("Submits any initiated payment found in the dat
override fun run() = cliCmd(logger) {
val cfg: EbicsSetupConfig = extractEbicsConfig(common.config)
val dbCfg = cfg.config.dbConfig()
- val db = Database(dbCfg.dbConnStr)
val (clientKeys, bankKeys) = expectFullKeys(cfg)
val ctx = SubmissionContext(
cfg = cfg,
@@ -295,26 +294,28 @@ class EbicsSubmit : CliktCommand("Submits any initiated payment found in the dat
}
return@cliCmd
}
- if (transient) {
- logger.info("Transient mode: submitting what found and returning.")
- submitBatch(ctx, db)
- return@cliCmd
- }
- val configValue = cfg.config.requireString("nexus-submit", "frequency")
- val frequencySeconds = checkFrequency(configValue)
- val frequency: NexusFrequency = NexusFrequency(frequencySeconds, configValue)
- logger.debug("Running with a frequency of ${frequency.fromConfig}")
- if (frequency.inSeconds == 0) {
- logger.warn("Long-polling not implemented, running therefore in transient mode")
- submitBatch(ctx, db)
- return@cliCmd
- }
- fixedRateTimer(
- name = "ebics submit period",
- period = (frequency.inSeconds * 1000).toLong(),
- action = {
+ Database(dbCfg.dbConnStr).use { db ->
+ if (transient) {
+ logger.info("Transient mode: submitting what found and returning.")
submitBatch(ctx, db)
+ return@cliCmd
}
- )
+ val configValue = cfg.config.requireString("nexus-submit", "frequency")
+ val frequencySeconds = checkFrequency(configValue)
+ val frequency: NexusFrequency = NexusFrequency(frequencySeconds, configValue)
+ logger.debug("Running with a frequency of ${frequency.fromConfig}")
+ if (frequency.inSeconds == 0) {
+ logger.warn("Long-polling not implemented, running therefore in transient mode")
+ submitBatch(ctx, db)
+ return@cliCmd
+ }
+ fixedRateTimer(
+ name = "ebics submit period",
+ period = (frequency.inSeconds * 1000).toLong(),
+ action = {
+ submitBatch(ctx, db)
+ }
+ )
+ }
}
} \ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
index ab9fe2c6..3a535aff 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -48,6 +48,7 @@ import tech.libeufin.nexus.ebics.*
import tech.libeufin.util.*
import java.security.interfaces.RSAPrivateCrtKey
import java.security.interfaces.RSAPublicKey
+import java.io.FileNotFoundException
val NEXUS_CONFIG_SOURCE = ConfigSource("libeufin", "libeufin-nexus", "libeufin-nexus")
val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus")
@@ -280,16 +281,16 @@ fun expectFullKeys(
cfg: EbicsSetupConfig
): Pair<ClientPrivateKeysFile, BankPublicKeysFile> {
val clientKeys = loadPrivateKeysFromDisk(cfg.clientPrivateKeysFilename)
- if (clientKeys == null ||
- !clientKeys.submitted_ini ||
- !clientKeys.submitted_hia) {
- throw Error("Cannot operate without or with unsubmitted subscriber keys." +
- " Run 'libeufin-nexus ebics-setup' first.")
+ if (clientKeys == null) {
+ throw Exception("Cannot operate without client keys. Missing '${cfg.clientPrivateKeysFilename}' file. Run 'libeufin-nexus ebics-setup' first")
+ } else if (!clientKeys.submitted_ini || !clientKeys.submitted_hia) {
+ throw Exception("Cannot operate with unsubmitted client keys, run 'libeufin-nexus ebics-setup' first")
}
val bankKeys = loadBankKeys(cfg.bankPublicKeysFilename)
- if (bankKeys == null || !bankKeys.accepted) {
- throw Error("Cannot operate without or with unaccepted bank keys." +
- " Run 'libeufin-nexus ebics-setup' until accepting the bank keys.")
+ if (bankKeys == null) {
+ throw Exception("Cannot operate without bank keys. Missing '${cfg.bankPublicKeysFilename}' file. run 'libeufin-nexus ebics-setup' first")
+ } else if (!bankKeys.accepted) {
+ throw Exception("Cannot operate with unaccepted bank keys, run 'libeufin-nexus ebics-setup' until accepting the bank keys")
}
return Pair(clientKeys, bankKeys)
}
@@ -299,27 +300,20 @@ fun expectFullKeys(
*
* @param location the keys file location.
* @return the internal JSON representation of the keys file,
- * or null on failures.
+ * or null if the file does not exist
*/
fun loadBankKeys(location: String): BankPublicKeysFile? {
- val f = File(location)
- if (!f.exists()) {
- logger.error("Could not find the bank keys file at: $location")
+ val content = try {
+ File(location).readText()
+ } catch (e: FileNotFoundException) {
return null
- }
- val fileContent = try {
- f.readText() // read from disk.
} catch (e: Exception) {
- logger.error("Could not read the bank keys file from disk, detail: ${e.message}")
- return null
+ throw Exception("Could not read the bank keys file from disk", e)
}
return try {
- myJson.decodeFromString(fileContent) // Parse into JSON.
+ myJson.decodeFromString(content)
} catch (e: Exception) {
- logger.error(e.message)
- @OptIn(InternalAPI::class) // enables message below.
- logger.error(e.rootCause?.message) // actual useful message mentioning failing fields
- return null
+ throw Exception("Could not decode bank keys", e)
}
}
@@ -328,27 +322,20 @@ fun loadBankKeys(location: String): BankPublicKeysFile? {
*
* @param location the keys file location.
* @return the internal JSON representation of the keys file,
- * or null on failures.
+ * or null if the file does not exist
*/
fun loadPrivateKeysFromDisk(location: String): ClientPrivateKeysFile? {
- val f = File(location)
- if (!f.exists()) {
- logger.error("Could not find the private keys file at: $location")
+ val content = try {
+ File(location).readText()
+ } catch (e: FileNotFoundException) {
return null
- }
- val fileContent = try {
- f.readText() // read from disk.
} catch (e: Exception) {
- logger.error("Could not read private keys from disk, detail: ${e.message}")
- return null
+ throw Exception("Could not read private keys from disk", e)
}
return try {
- myJson.decodeFromString(fileContent) // Parse into JSON.
+ myJson.decodeFromString(content)
} catch (e: Exception) {
- logger.error(e.message)
- @OptIn(InternalAPI::class) // enables message below.
- logger.error(e.rootCause?.message) // actual useful message mentioning failing fields
- return null
+ throw Exception("Could not decode private keys", e)
}
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
index 615e5a2b..b700abb2 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
@@ -336,14 +336,14 @@ suspend fun doEbicsDownload(
val initResp = postEbics(client, cfg, bankKeys, reqXml, isEbics3)
logger.debug("Download init phase done. EBICS- and bank-technical codes are: ${initResp.technicalReturnCode}, ${initResp.bankReturnCode}")
if (initResp.technicalReturnCode != EbicsReturnCode.EBICS_OK) {
- throw Error("Download init phase has EBICS-technical error: ${initResp.technicalReturnCode}")
+ throw Exception("Download init phase has EBICS-technical error: ${initResp.technicalReturnCode}")
}
if (initResp.bankReturnCode == EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE && tolerateEmptyResult) {
logger.info("Download content is empty")
return ByteArray(0)
}
if (initResp.bankReturnCode != EbicsReturnCode.EBICS_OK) {
- throw Error("Download init phase has bank-technical error: ${initResp.bankReturnCode}")
+ throw Exception("Download init phase has bank-technical error: ${initResp.bankReturnCode}")
}
val tId = initResp.transactionID
?: throw EbicsSideException(
@@ -353,7 +353,7 @@ suspend fun doEbicsDownload(
logger.debug("EBICS download transaction passed the init phase, got ID: $tId")
val howManySegments = initResp.numSegments
if (howManySegments == null) {
- throw Error("Init response lacks the quantity of segments, failing.")
+ throw Exception("Init response lacks the quantity of segments, failing.")
}
val ebicsChunks = mutableListOf<String>()
// Getting the chunk(s)
@@ -385,7 +385,7 @@ suspend fun doEbicsDownload(
}
val chunk = transResp.orderDataEncChunk
if (chunk == null) {
- throw Error("EBICS transfer phase lacks chunk #$x, failing.")
+ throw Exception("EBICS transfer phase lacks chunk #$x, failing.")
}
ebicsChunks.add(chunk)
}
diff --git a/nexus/src/test/kotlin/CliTest.kt b/nexus/src/test/kotlin/CliTest.kt
new file mode 100644
index 00000000..e6386e21
--- /dev/null
+++ b/nexus/src/test/kotlin/CliTest.kt
@@ -0,0 +1,98 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2023 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/>
+ */
+
+import tech.libeufin.nexus.*
+import com.github.ajalt.clikt.core.*
+import com.github.ajalt.clikt.testing.*
+import kotlin.test.*
+import java.io.*
+import java.nio.file.*
+import kotlin.io.path.*
+import tech.libeufin.util.*
+
+val nexusCmd = LibeufinNexusCommand()
+
+fun CliktCommand.testErr(cmd: String, msg: String) {
+ val prevOut = System.err
+ val tmpOut = ByteArrayOutputStream()
+ System.setErr(PrintStream(tmpOut))
+ val result = test(cmd)
+ System.setErr(prevOut)
+ val tmpStr = tmpOut.toString(Charsets.UTF_8)
+ println(tmpStr)
+ assertEquals(1, result.statusCode, "'$cmd' should have failed")
+ val line = tmpStr.substringAfterLast(" - ").trimEnd('\n')
+ println(line)
+ assertEquals(msg, line)
+}
+
+class CliTest {
+ /** Test error format related to the keying process */
+ @Test
+ fun keys() {
+ val cmds = listOf("ebics-submit", "ebics-fetch")
+ val allCmds = listOf("ebics-submit", "ebics-fetch", "ebics-setup")
+ val conf = "conf/test.conf"
+ val cfg = loadConfig(conf)
+ val clientKeysPath = Path(cfg.requireString("nexus-ebics", "client_private_keys_file"))
+ val bankKeysPath = Path(cfg.requireString("nexus-ebics", "bank_public_keys_file"))
+ clientKeysPath.parent?.createDirectories()
+ bankKeysPath.parent?.createDirectories()
+
+ // Missing client keys
+ clientKeysPath.deleteIfExists()
+ for (cmd in cmds) {
+ nexusCmd.testErr("$cmd -c $conf", "Cannot operate without client keys. Missing '$clientKeysPath' file. Run 'libeufin-nexus ebics-setup' first")
+ }
+ // Bad client json
+ clientKeysPath.writeText("CORRUPTION", Charsets.UTF_8)
+ for (cmd in allCmds) {
+ nexusCmd.testErr("$cmd -c $conf", "Could not decode private keys: Expected start of the object '{', but had 'EOF' instead at path: $\nJSON input: CORRUPTION")
+ }
+ // Unfinished client
+ syncJsonToDisk(generateNewKeys(), clientKeysPath.toString())
+ for (cmd in cmds) {
+ nexusCmd.testErr("$cmd -c $conf", "Cannot operate with unsubmitted client keys, run 'libeufin-nexus ebics-setup' first")
+ }
+
+ // Missing bank keys
+ syncJsonToDisk(generateNewKeys().apply {
+ submitted_hia = true
+ submitted_ini = true
+ }, clientKeysPath.toString())
+ bankKeysPath.deleteIfExists()
+ for (cmd in cmds) {
+ nexusCmd.testErr("$cmd -c $conf", "Cannot operate without bank keys. Missing '$bankKeysPath' file. run 'libeufin-nexus ebics-setup' first")
+ }
+ // Bad bank json
+ bankKeysPath.writeText("CORRUPTION", Charsets.UTF_8)
+ for (cmd in allCmds) {
+ nexusCmd.testErr("$cmd -c $conf", "Could not decode bank keys: Expected start of the object '{', but had 'EOF' instead at path: $\nJSON input: CORRUPTION")
+ }
+ // Unfinished bank
+ syncJsonToDisk(BankPublicKeysFile(
+ bank_authentication_public_key = CryptoUtil.generateRsaKeyPair(2048).public,
+ bank_encryption_public_key = CryptoUtil.generateRsaKeyPair(2048).public,
+ accepted = false
+ ), bankKeysPath.toString())
+ for (cmd in cmds) {
+ nexusCmd.testErr("$cmd -c $conf", "Cannot operate with unaccepted bank keys, run 'libeufin-nexus ebics-setup' until accepting the bank keys")
+ }
+ }
+} \ No newline at end of file
diff --git a/nexus/src/test/kotlin/Keys.kt b/nexus/src/test/kotlin/Keys.kt
index d2894c04..37c095fd 100644
--- a/nexus/src/test/kotlin/Keys.kt
+++ b/nexus/src/test/kotlin/Keys.kt
@@ -23,7 +23,7 @@ class PublicKeys {
bank_encryption_public_key = CryptoUtil.generateRsaKeyPair(2028).public
)
// storing them on disk.
- assertTrue(syncJsonToDisk(fileContent, "/tmp/nexus-tests-bank-keys.json"))
+ syncJsonToDisk(fileContent, "/tmp/nexus-tests-bank-keys.json")
// loading them and check that values are the same.
val fromDisk = loadBankKeys("/tmp/nexus-tests-bank-keys.json")
assertNotNull(fromDisk)
@@ -50,16 +50,12 @@ class PrivateKeys {
fun createWrongPermissions() {
f.writeText("won't be overridden")
f.setReadOnly()
- assertFalse(syncJsonToDisk(clientKeys, f.path))
+ try {
+ syncJsonToDisk(clientKeys, f.path)
+ throw Exception("Should have failed")
+ } catch (e: Exception) { }
}
- // Testing keys file creation.
- @Test
- fun creation() {
- assertFalse(f.exists())
- maybeCreatePrivateKeysFile(f.path) // file doesn't exist, this must create.
- j.decodeFromString<ClientPrivateKeysFile>(f.readText()) // reading and validating disk content.
- }
/**
* Tests whether loading keys from disk yields the same
* values that were stored to the file.
@@ -67,7 +63,7 @@ class PrivateKeys {
@Test
fun load() {
assertFalse(f.exists())
- assertTrue(syncJsonToDisk(clientKeys, f.path)) // Artificially storing this to the file.
+ syncJsonToDisk(clientKeys, f.path) // Artificially storing this to the file.
val fromDisk = loadPrivateKeysFromDisk(f.path) // loading it via the tested routine.
assertNotNull(fromDisk)
// Checking the values from disk match the initial object.
diff --git a/nexus/src/test/kotlin/PostFinance.kt b/nexus/src/test/kotlin/PostFinance.kt
deleted file mode 100644
index 860e8ef0..00000000
--- a/nexus/src/test/kotlin/PostFinance.kt
+++ /dev/null
@@ -1,219 +0,0 @@
-import io.ktor.client.*
-import kotlinx.coroutines.runBlocking
-import org.junit.Ignore
-import org.junit.Test
-import tech.libeufin.nexus.*
-import tech.libeufin.nexus.ebics.*
-import tech.libeufin.util.ebics_h005.Ebics3Request
-import tech.libeufin.util.parsePayto
-import java.io.File
-import java.time.Instant
-import java.time.temporal.ChronoUnit
-import kotlin.test.assertNotNull
-import kotlin.test.assertTrue
-
-// Tests only manual, that's why they are @Ignore
-
-private fun prep(): EbicsSetupConfig {
- val handle = TalerConfig(NEXUS_CONFIG_SOURCE)
- val ebicsUserId = File("/tmp/pofi-ebics-user-id.txt").readText()
- val ebicsPartnerId = File("/tmp/pofi-ebics-partner-id.txt").readText()
- handle.loadFromString(getPofiConfig(ebicsUserId, ebicsPartnerId))
- return EbicsSetupConfig(handle)
-}
-
-@Ignore
-class Iso20022 {
-
- private val yesterday: Instant = Instant.now().minus(1, ChronoUnit.DAYS)
-
- @Test // asks a pain.002, links with pain.001's MsgId
- fun getAck() {
- download(prepAckRequest3(startDate = yesterday)
- ).unzipForEach { name, content ->
- println(name)
- println(content)
- }
- }
-
- /**
- * With the "mit Detailavisierung" option, each entry has an
- * AcctSvcrRef & wire transfer subject.
- */
- @Test
- fun getStatement() {
- val inflatedBytes = download(prepStatementRequest3())
- inflatedBytes.unzipForEach { name, content ->
- println(name)
- println(content)
- }
- }
-
- @Test
- fun getNotification() {
- val inflatedBytes = download(
- prepNotificationRequest3(
- // startDate = yesterday,
- isAppendix = true
- )
- )
- inflatedBytes.unzipForEach { name, content ->
- println(name)
- println(content)
- }
- }
-
- /**
- * Never shows the subject.
- */
- @Test
- fun getReport() {
- download(prepReportRequest3(yesterday)).unzipForEach { name, content ->
- println(name)
- println(content)
- }
- }
-
- @Test
- fun simulateIncoming() {
- val cfg = prep()
- val orderService: Ebics3Request.OrderDetails.Service = Ebics3Request.OrderDetails.Service().apply {
- serviceName = "OTH"
- scope = "BIL"
- messageName = Ebics3Request.OrderDetails.Service.MessageName().apply {
- value = "csv"
- }
- serviceOption = "CH002LMF"
- }
- val instruction = """
- Product;Channel;Account;Currency;Amount;Reference;Name;Street;Number;Postcode;City;Country;DebtorAddressLine;DebtorAddressLine;DebtorAccount;ReferenceType;UltimateDebtorName;UltimateDebtorStreet;UltimateDebtorNumber;UltimateDebtorPostcode;UltimateDebtorTownName;UltimateDebtorCountry;UltimateDebtorAddressLine;UltimateDebtorAddressLine;RemittanceInformationText
- QRR;PO;CH9789144829733648596;CHF;1;;D009;Musterstrasse;1;1111;Musterstadt;CH;;;;NON;D009;Musterstrasse;1;1111;Musterstadt;CH;;;Taler-Demo
- """.trimIndent()
-
- runBlocking {
- try {
- doEbicsUpload(
- HttpClient(),
- cfg,
- loadPrivateKeysFromDisk(cfg.clientPrivateKeysFilename)!!,
- loadBankKeys(cfg.bankPublicKeysFilename)!!,
- orderService,
- instruction.toByteArray(Charsets.UTF_8)
- )
- }
- catch (e: EbicsUploadException) {
- logger.error(e.message)
- logger.error("bank EC: ${e.bankErrorCode}, EBICS EC: ${e.ebicsErrorCode}")
- }
- }
- }
-
- fun download(req: Ebics3Request.OrderDetails.BTOrderParams): ByteArray {
- val cfg = prep()
- val bankKeys = loadBankKeys(cfg.bankPublicKeysFilename)!!
- val myKeys = loadPrivateKeysFromDisk(cfg.clientPrivateKeysFilename)!!
- val initXml = createEbics3DownloadInitialization(
- cfg,
- bankKeys,
- myKeys,
- orderParams = req
- )
- return runBlocking {
- doEbicsDownload(
- HttpClient(),
- cfg,
- myKeys,
- bankKeys,
- initXml,
- isEbics3 = true,
- tolerateEmptyResult = true
- )
- }
- }
-
- @Test
- fun sendPayment() {
- val cfg = prep()
- val xml = createPain001(
- "random",
- Instant.now(),
- cfg.myIbanAccount,
- TalerAmount(4, 0, "CHF"),
- "Test reimbursement, part 2",
- parsePayto("payto://iban/CH9300762011623852957?receiver-name=NotGiven")!!
- )
- runBlocking {
- // Not asserting, as it throws in case of errors.
- submitPain001(
- xml,
- cfg,
- loadPrivateKeysFromDisk(cfg.clientPrivateKeysFilename)!!,
- loadBankKeys(cfg.bankPublicKeysFilename)!!,
- HttpClient()
- )
- }
- }
-}
-
-@Ignore
-class PostFinance {
- // Tests sending client keys to the PostFinance test platform.
- @Test
- fun postClientKeys() {
- val cfg = prep()
- runBlocking {
- val httpClient = HttpClient()
- assertTrue(doKeysRequestAndUpdateState(cfg, clientKeys, httpClient, KeysOrderType.INI))
- assertTrue(doKeysRequestAndUpdateState(cfg, clientKeys, httpClient, KeysOrderType.HIA))
- }
- }
-
- // Tests getting the PostFinance keys from their test platform.
- @Test
- fun getBankKeys() {
- val cfg = prep()
- val keys = loadPrivateKeysFromDisk(cfg.clientPrivateKeysFilename)
- assertNotNull(keys)
- assertTrue(keys.submitted_ini)
- assertTrue(keys.submitted_hia)
- runBlocking {
- assertTrue(
- doKeysRequestAndUpdateState(
- cfg,
- keys,
- HttpClient(),
- KeysOrderType.HPB
- ))
- }
- }
-
- // Arbitrary download request for manual tests.
- @Test
- fun customDownload() {
- val cfg = prep()
- val clientKeys = loadPrivateKeysFromDisk(cfg.clientPrivateKeysFilename)
- val bankKeys = loadBankKeys(cfg.bankPublicKeysFilename)
- runBlocking {
- val bytes = doEbicsCustomDownload(
- messageType = "HTD",
- cfg = cfg,
- bankKeys = bankKeys!!,
- clientKeys = clientKeys!!,
- client = HttpClient()
- )
- println(bytes.toString())
- }
- }
-
- // Tests the HTD message type.
- @Test
- fun fetchAccounts() {
- val cfg = prep()
- val clientKeys = loadPrivateKeysFromDisk(cfg.clientPrivateKeysFilename)
- assertNotNull(clientKeys)
- val bankKeys = loadBankKeys(cfg.bankPublicKeysFilename)
- assertNotNull(bankKeys)
- val htd = runBlocking { fetchBankAccounts(cfg, clientKeys, bankKeys, HttpClient()) }
- println(htd)
- }
-} \ No newline at end of file
diff --git a/util/src/main/kotlin/Cli.kt b/util/src/main/kotlin/Cli.kt
index 25f99340..93a15a21 100644
--- a/util/src/main/kotlin/Cli.kt
+++ b/util/src/main/kotlin/Cli.kt
@@ -36,7 +36,14 @@ fun cliCmd(logger: Logger, lambda: () -> Unit) {
try {
lambda()
} catch (e: Throwable) {
- logger.error(e.message)
+ var msg = StringBuilder(e.message)
+ var cause = e.cause;
+ while (cause != null) {
+ msg.append(": ")
+ msg.append(cause.message)
+ cause = cause.cause
+ }
+ logger.error(msg.toString())
throw ProgramResult(1)
}
}
@@ -81,13 +88,13 @@ private class CliConfigGet(private val configSource: ConfigSource) : CliktComman
if (isPath) {
val res = config.lookupPath(sectionName, optionName)
if (res == null) {
- throw Error("value not found in config")
+ throw Exception("value not found in config")
}
println(res)
} else {
val res = config.lookupString(sectionName, optionName)
if (res == null) {
- throw Error("value not found in config")
+ throw Exception("value not found in config")
}
println(res)
}
diff --git a/util/src/main/kotlin/DB.kt b/util/src/main/kotlin/DB.kt
index 13fe1b2b..0fa26c9a 100644
--- a/util/src/main/kotlin/DB.kt
+++ b/util/src/main/kotlin/DB.kt
@@ -235,7 +235,7 @@ fun initializeDatabaseTables(conn: PgConnection, cfg: DatabaseConfig, sqlFilePre
val patchName = "$sqlFilePrefix-$numStr"
checkStmt.setString(1, patchName)
- val patchCount = checkStmt.oneOrNull { it.getInt(1) } ?: throw Error("unable to query patches");
+ val patchCount = checkStmt.oneOrNull { it.getInt(1) } ?: throw Exception("unable to query patches");
if (patchCount >= 1) {
logger.info("patch $patchName already applied")
continue