commit 45785f6b58d3eb93fe04de5c352a274249d08b4c
parent 4e0704b3732a7a139f9ec7eef8a5adfdfa391aaf
Author: Antoine A <>
Date: Fri, 12 Jun 2026 10:31:44 +0200
common: improve config parser and add new cli & repl test utils
Diffstat:
15 files changed, 332 insertions(+), 68 deletions(-)
diff --git a/Makefile b/Makefile
@@ -33,14 +33,14 @@ install-nobuild-files:
install -m 644 -D -t $(man_dir)/man1 doc/prebuilt/man/taler-apns-relay.1
install -m 644 -D -t $(man_dir)/man5 doc/prebuilt/man/taler-apns-relay.conf.5
install -D -t $(bin_dir) contrib/taler-magnet-bank-dbconfig
- install -D -t $(bin_dir) target/release/taler-magnet-bank
install -D -t $(bin_dir) contrib/taler-cyclos-dbconfig
- install -D -t $(bin_dir) target/release/taler-cyclos
install -D -t $(bin_dir) contrib/taler-apns-relay-dbconfig
- install -D -t $(bin_dir) target/release/taler-apns-relay
.PHONY: install
install: build install-nobuild-files
+ install -D -t $(bin_dir) target/release/taler-magnet-bank
+ install -D -t $(bin_dir) target/release/taler-cyclos
+ install -D -t $(bin_dir) target/release/taler-apns-relay
.PHONY: check
check: install-nobuild-files
diff --git a/common/http-client/src/lib.rs b/common/http-client/src/lib.rs
@@ -20,6 +20,7 @@ use http_body_util::Full;
use hyper::{Method, StatusCode, body::Bytes};
use hyper_rustls::ConfigBuilderExt as _;
use hyper_util::rt::TokioExecutor;
+use rustls::crypto::CryptoProvider;
use taler_common::error::FmtSource;
use thiserror::Error;
@@ -58,9 +59,11 @@ pub enum ClientErr {
}
pub fn client() -> Client {
- rustls::crypto::aws_lc_rs::default_provider()
- .install_default()
- .expect("failed to install the default TLS provider");
+ if CryptoProvider::get_default().is_none() {
+ rustls::crypto::aws_lc_rs::default_provider()
+ .install_default()
+ .expect("failed to install the default TLS provider");
+ }
// Prepare the TLS client config
let tls = rustls::ClientConfig::builder()
diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs
@@ -20,6 +20,7 @@ use std::{
os::unix::fs::PermissionsExt,
path::PathBuf,
str::FromStr,
+ sync::Arc,
time::Duration,
};
@@ -41,13 +42,14 @@ pub mod parser {
io::{BufRead, BufReader},
path::PathBuf,
str::FromStr,
+ sync::Arc,
};
use indexmap::IndexMap;
use tracing::{trace, warn};
use super::{Config, ValueErr};
- use crate::config::{Line, Location};
+ use crate::config::{Inner, Line, Location};
#[derive(Debug, thiserror::Error)]
pub enum ConfigErr {
@@ -120,7 +122,7 @@ pub mod parser {
) -> ParserErr {
ParserErr::Line(msg.into(), path.into(), line, Some(cause.to_string()))
}
- struct Parser {
+ pub struct Parser {
sections: IndexMap<String, IndexMap<String, Line>>,
files: Vec<PathBuf>,
install_path: PathBuf,
@@ -128,7 +130,7 @@ pub mod parser {
}
impl Parser {
- fn empty() -> Self {
+ pub fn empty() -> Self {
Self {
sections: IndexMap::new(),
files: Vec::new(),
@@ -137,7 +139,7 @@ pub mod parser {
}
}
- fn load_env(&mut self, src: ConfigSource) -> Result<(), ParserErr> {
+ pub fn load_env(&mut self, src: ConfigSource) -> Result<(), ParserErr> {
let ConfigSource { project_name, .. } = src;
// Load default path
@@ -190,7 +192,15 @@ pub mod parser {
Ok(())
}
- fn parse_file(&mut self, src: PathBuf, depth: u8) -> Result<(), ParserErr> {
+ pub fn parse_str(&mut self, str: &str) -> Result<(), ParserErr> {
+ self.parse(
+ std::io::Cursor::new(str),
+ PathBuf::from_str("mem").unwrap(),
+ 0,
+ )
+ }
+
+ pub fn parse_file(&mut self, src: PathBuf, depth: u8) -> Result<(), ParserErr> {
trace!(target: "config", "load file at '{}'", src.to_string_lossy());
match std::fs::File::open(&src) {
Ok(file) => self.parse(BufReader::new(file), src, depth + 1),
@@ -332,11 +342,11 @@ pub mod parser {
pub fn finish(self) -> Config {
// Convert to a readonly config struct without location info
- Config {
+ Config(Arc::new(Inner {
sections: self.sections,
files: self.files,
install_path: self.install_path,
- }
+ }))
}
}
@@ -462,22 +472,14 @@ pub mod parser {
pub fn from_mem(str: &str) -> Result<Config, ParserErr> {
let mut parser = Parser::empty();
- parser.parse(
- std::io::Cursor::new(str),
- PathBuf::from_str("mem").unwrap(),
- 0,
- )?;
+ parser.parse_str(str)?;
Ok(parser.finish())
}
pub fn from_mem_with_env(src: ConfigSource, str: &str) -> Result<Config, ParserErr> {
let mut parser = Parser::empty();
parser.load_env(src)?;
- parser.parse(
- std::io::Cursor::new(str),
- PathBuf::from_str("mem").unwrap(),
- 0,
- )?;
+ parser.parse_str(str)?;
Ok(parser.finish())
}
}
@@ -525,24 +527,33 @@ struct Line {
loc: Option<Location>,
}
-#[derive(Debug, Clone)]
-pub struct Config {
+#[derive(Debug)]
+pub struct Inner {
sections: IndexMap<String, IndexMap<String, Line>>,
files: Vec<PathBuf>,
install_path: PathBuf,
}
+#[derive(Clone)]
+pub struct Config(Arc<Inner>);
+
+impl Debug for Config {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
impl Config {
pub fn section<'cfg, 'arg>(&'cfg self, section: &'arg str) -> Section<'cfg, 'arg> {
Section {
name: section,
config: self,
- values: self.sections.get(§ion.to_uppercase()),
+ values: self.0.sections.get(§ion.to_uppercase()),
}
}
pub fn sections<'cfg>(&'cfg self) -> impl Iterator<Item = Section<'cfg, 'cfg>> {
- self.sections.iter().map(|(section, values)| Section {
+ self.0.sections.iter().map(|(section, values)| Section {
name: section,
config: self,
values: Some(values),
@@ -566,6 +577,7 @@ impl Config {
/** Lookup for variable value from PATHS section in the configuration and environment variables */
fn lookup(cfg: &Config, name: &str, depth: u8) -> Option<Result<String, PathsubErr>> {
if let Some(path_res) = cfg
+ .0
.sections
.get("PATHS")
.and_then(|section| section.get(name))
@@ -649,24 +661,25 @@ impl Config {
}
pub fn print(&self, mut f: impl std::io::Write, diagnostics: bool) -> std::io::Result<()> {
+ let Inner {
+ sections,
+ files,
+ install_path,
+ } = self.0.as_ref();
if diagnostics {
writeln!(f, "#")?;
writeln!(f, "# Configuration file diagnostics")?;
writeln!(f, "#")?;
writeln!(f, "# File Loaded:")?;
- for path in &self.files {
+ for path in files {
writeln!(f, "# {}", path.to_string_lossy())?;
}
writeln!(f, "#")?;
- writeln!(
- f,
- "# Installation path: {}",
- self.install_path.to_string_lossy()
- )?;
+ writeln!(f, "# Installation path: {}", install_path.to_string_lossy())?;
writeln!(f, "#")?;
writeln!(f)?;
}
- for (sect, values) in &self.sections {
+ for (sect, values) in sections {
writeln!(f, "[{sect}]")?;
if diagnostics {
writeln!(f)?;
@@ -675,7 +688,7 @@ impl Config {
if diagnostics {
match loc {
Some(Location { file, line }) => {
- let path = &self.files[*file];
+ let path = &files[*file];
writeln!(f, "# {}:{line}", path.to_string_lossy())?;
}
None => writeln!(f, "# default")?,
diff --git a/common/taler-common/src/lib.rs b/common/taler-common/src/lib.rs
@@ -56,7 +56,7 @@ pub struct CommonArgs {
pub fn taler_main(
src: ConfigSource,
args: CommonArgs,
- app: impl AsyncFnOnce(Config) -> Result<(), anyhow::Error>,
+ app: impl AsyncFnOnce(&Config) -> Result<(), anyhow::Error>,
) {
taler_logger(args.log, args.verbose).init();
let cfg = match Config::from_file(src, args.config) {
@@ -74,7 +74,7 @@ pub fn taler_main(
.unwrap();
// Run app
- let result = runtime.block_on(app(cfg));
+ let result = runtime.block_on(app(&cfg));
if let Err(err) = result {
error!(target: "cli", "{}", err);
std::process::exit(1);
diff --git a/common/taler-test-utils/Cargo.toml b/common/taler-test-utils/Cargo.toml
@@ -27,4 +27,8 @@ sqlx.workspace = true
http-body-util.workspace = true
url.workspace = true
aws-lc-rs.workspace = true
-jiff.workspace = true
-\ No newline at end of file
+jiff.workspace = true
+clap.workspace = true
+reedline = "0.48"
+shlex = "2.0"
+nu-ansi-term = "0.50"
+\ No newline at end of file
diff --git a/common/taler-test-utils/src/cli.rs b/common/taler-test-utils/src/cli.rs
@@ -0,0 +1,26 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2026 Taler Systems SA
+
+ TALER 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.
+
+ TALER 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
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+
+pub fn clap_parse<T: clap::Parser>(cmd: &str) -> Result<T, clap::Error> {
+ let parts = shlex::split(cmd).unwrap();
+ let cmd = T::command();
+ if cmd.is_no_binary_name_set() {
+ T::try_parse_from(parts)
+ } else {
+ let args = std::iter::once(cmd.get_name()).chain(parts.iter().map(|it| it.as_str()));
+ T::try_parse_from(args)
+ }
+}
diff --git a/common/taler-test-utils/src/lib.rs b/common/taler-test-utils/src/lib.rs
@@ -19,8 +19,10 @@ use taler_common::log::taler_logger;
use tracing::Level;
use tracing_subscriber::util::SubscriberInitExt;
+pub mod cli;
pub mod db;
pub mod json;
+pub mod repl;
pub mod routine;
pub mod server;
diff --git a/common/taler-test-utils/src/repl.rs b/common/taler-test-utils/src/repl.rs
@@ -0,0 +1,212 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2026 Taler Systems SA
+
+ TALER 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.
+
+ TALER 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
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+
+use std::borrow::Cow;
+
+use nu_ansi_term::Color;
+use reedline::{
+ ColumnarMenu, DefaultHinter, FileBackedHistory, KeyCode, KeyModifiers, MenuBuilder as _,
+ Reedline, ReedlineEvent, ReedlineMenu, Signal, Span, Suggestion, Vi,
+ default_vi_insert_keybindings, default_vi_normal_keybindings,
+};
+
+use crate::cli::clap_parse;
+
+pub async fn test_repl<T: clap::Parser>(
+ history: &str,
+ prompt: String,
+ prepare: impl AsyncFn(),
+ run: impl AsyncFn(T) -> bool,
+) {
+ let history = Box::new(
+ FileBackedHistory::with_file(1000, history.into())
+ .expect("Error configuring history with file"),
+ );
+ let completion_menu = Box::new(
+ ColumnarMenu::default()
+ .with_name("completion_menu")
+ .with_columns(4)
+ .with_column_width(None)
+ .with_column_padding(2),
+ );
+
+ let mut insert = default_vi_insert_keybindings();
+ insert.add_binding(
+ KeyModifiers::NONE,
+ KeyCode::Tab,
+ ReedlineEvent::UntilFound(vec![
+ ReedlineEvent::Menu("completion_menu".to_string()),
+ ReedlineEvent::MenuNext,
+ ]),
+ );
+ let mut relp = Reedline::create()
+ .with_history(history)
+ .with_completer(Box::new(ClapCompleter(T::command())))
+ .with_menu(ReedlineMenu::EngineCompleter(completion_menu))
+ .with_quick_completions(true)
+ .with_partial_completions(true)
+ .with_hinter(Box::new(DefaultHinter::default().with_style(
+ nu_ansi_term::Style::new().italic().fg(Color::LightGray),
+ )))
+ .with_edit_mode(Box::new(Vi::new(insert, default_vi_normal_keybindings())));
+
+ let prompt = ReplPrompt(prompt);
+ loop {
+ prepare().await;
+ let Signal::Success(buf) = relp.read_line(&prompt).unwrap() else {
+ break;
+ };
+ match clap_parse(&buf) {
+ Ok(cmd) => {
+ if run(cmd).await {
+ break;
+ }
+ }
+ Err(e) => {
+ println!("{e}");
+ }
+ }
+ }
+}
+
+struct ClapCompleter(clap::Command);
+
+impl reedline::Completer for ClapCompleter {
+ fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
+ let mut suggestions = Vec::new();
+ let text_before_cursor = &line[..pos];
+
+ let words = shlex::split(text_before_cursor).unwrap_or_default();
+ let is_whitespace_at_end = text_before_cursor.ends_with(' ');
+
+ let mut active_cmd = &self.0;
+ let mut last_word = "";
+
+ // Determine the word currently being typed
+ if !is_whitespace_at_end && !words.is_empty() {
+ last_word = words.last().unwrap();
+ }
+
+ // Identify which subcommand we are currently inside by walking the tokens
+ let tokens = if is_whitespace_at_end {
+ &words[..]
+ } else {
+ &words[..words.len().saturating_sub(1)]
+ };
+
+ for token in tokens {
+ if let Some(subcmd) = active_cmd
+ .get_subcommands()
+ .find(|s| s.get_name() == *token)
+ {
+ active_cmd = subcmd;
+ }
+ }
+
+ let start = pos - last_word.len();
+ let span = Span::new(start, pos);
+
+ // Suggest Long Flags
+ if last_word.starts_with("--") {
+ for arg in active_cmd.get_arguments() {
+ if let Some(long) = arg.get_long() {
+ let flag = format!("--{}", long);
+ if flag.starts_with(last_word) {
+ suggestions.push(Suggestion {
+ value: flag,
+ description: arg.get_help().map(|s| s.to_string()),
+ span,
+ append_whitespace: true,
+ ..Suggestion::default()
+ });
+ }
+ }
+ }
+ }
+ // Suggest Short & Long Flags
+ else if last_word.starts_with("-") {
+ for arg in active_cmd.get_arguments() {
+ if let Some(short) = arg.get_short() {
+ let flag = format!("-{}", short);
+ if flag.starts_with(last_word) {
+ suggestions.push(Suggestion {
+ value: flag,
+ description: arg.get_help().map(|s| s.to_string()),
+ span,
+ append_whitespace: true,
+ ..Suggestion::default()
+ });
+ }
+ }
+ if let Some(long) = arg.get_long() {
+ let flag = format!("--{}", long);
+ if flag.starts_with(last_word) {
+ suggestions.push(Suggestion {
+ value: flag,
+ description: arg.get_help().map(|s| s.to_string()),
+ span,
+ append_whitespace: true,
+ ..Suggestion::default()
+ });
+ }
+ }
+ }
+ }
+ // Suggest Subcommands
+ else {
+ for subcmd in active_cmd.get_subcommands() {
+ if subcmd.get_name().starts_with(last_word) {
+ suggestions.push(Suggestion {
+ value: subcmd.get_name().to_string(),
+ description: subcmd.get_about().map(|s| s.to_string()),
+ span,
+ append_whitespace: true,
+ ..Suggestion::default()
+ });
+ }
+ }
+ }
+
+ suggestions
+ }
+}
+
+struct ReplPrompt(String);
+
+impl reedline::Prompt for ReplPrompt {
+ fn render_prompt_left(&self) -> Cow<'_, str> {
+ Cow::Borrowed(&self.0)
+ }
+
+ fn render_prompt_right(&self) -> Cow<'_, str> {
+ Cow::Borrowed("")
+ }
+
+ fn render_prompt_indicator(&self, _: reedline::PromptEditMode) -> Cow<'_, str> {
+ Cow::Borrowed(">")
+ }
+
+ fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> {
+ Cow::Borrowed(":")
+ }
+
+ fn render_prompt_history_search_indicator(
+ &self,
+ _: reedline::PromptHistorySearch,
+ ) -> Cow<'_, str> {
+ Cow::Borrowed(">")
+ }
+}
diff --git a/taler-apns-relay/src/main.rs b/taler-apns-relay/src/main.rs
@@ -91,6 +91,6 @@ async fn run(cmd: Command, cfg: &Config) -> anyhow::Result<()> {
fn main() {
let args = Args::parse();
taler_main(CONFIG_SOURCE, args.common, async |cfg| {
- run(args.cmd, &cfg).await
+ run(args.cmd, cfg).await
});
}
diff --git a/taler-cyclos/src/bin/cyclos-harness.rs b/taler-cyclos/src/bin/cyclos-harness.rs
@@ -14,7 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-use std::time::Duration;
+use std::{assert_matches, time::Duration};
use clap::Parser as _;
use compact_str::CompactString;
@@ -175,10 +175,10 @@ impl<'a> Harness<'a> {
)
.await
.unwrap();
- assert!(matches!(
+ assert_matches!(
transfer.first().unwrap(),
IncomingBankTransaction::Reserve { reserve_pub, .. } if *reserve_pub == key,
- ));
+ );
}
async fn custom_transfer(&self, amount: Decimal, creditor_id: i64, creditor_name: &str) -> u64 {
@@ -496,10 +496,10 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> {
let amount = decimal("10.3");
harness.transfer(amount).await;
set_failure_scenario(&["direct-payment"]);
- assert!(matches!(
+ assert_matches!(
harness.worker().await.unwrap_err(),
WorkerError::Injected(InjectedErr("direct-payment"))
- ));
+ );
harness.worker().await?;
balance.expect_sub(amount).await;
harness.worker().await?;
@@ -513,10 +513,10 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> {
balance.expect_add(amount).await;
// Sync and bounce
set_failure_scenario(&["chargeback"]);
- assert!(matches!(
+ assert_matches!(
harness.worker().await.unwrap_err(),
WorkerError::Injected(InjectedErr("chargeback"))
- ));
+ );
balance.expect_sub(amount).await;
// Sync recover
harness.worker().await?;
@@ -627,7 +627,7 @@ async fn online_harness(config: &Config, reset: bool) -> anyhow::Result<()> {
fn main() {
let args = Args::parse();
taler_main(CONFIG_SOURCE, args.common, async |cfg| match args.cmd {
- Command::Logic { reset } => logic_harness(&cfg, reset).await,
- Command::Online { reset } => online_harness(&cfg, reset).await,
+ Command::Logic { reset } => logic_harness(cfg, reset).await,
+ Command::Online { reset } => online_harness(cfg, reset).await,
});
}
diff --git a/taler-cyclos/src/db.rs b/taler-cyclos/src/db.rs
@@ -913,6 +913,8 @@ impl CyclosTypeHelper for PgRow {
#[cfg(test)]
mod test {
+ use std::assert_matches;
+
use compact_str::CompactString;
use jiff::{Span, Timestamp};
use serde_json::json;
@@ -1185,7 +1187,7 @@ mod test {
creditor_name: "Name".into(),
valued_at: now,
};
- assert!(matches!(
+ assert_matches!(
db::make_transfer(
&pool,
&Transfer {
@@ -1202,7 +1204,7 @@ mod test {
.await
.unwrap(),
TransferResult::Success { .. }
- ));
+ );
db::initiated_submit_success(&mut db, 1, &Timestamp::now(), transfer_id)
.await
.expect("status success");
diff --git a/taler-cyclos/src/main.rs b/taler-cyclos/src/main.rs
@@ -145,6 +145,6 @@ async fn run(cmd: Command, cfg: &Config) -> anyhow::Result<()> {
fn main() {
let args = Args::parse();
taler_main(CONFIG_SOURCE, args.common, async |cfg| {
- run(args.cmd, &cfg).await
+ run(args.cmd, cfg).await
});
}
diff --git a/taler-magnet-bank/src/bin/magnet-bank-harness.rs b/taler-magnet-bank/src/bin/magnet-bank-harness.rs
@@ -14,7 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-use std::{fmt::Debug, time::Duration};
+use std::{assert_matches, fmt::Debug, time::Duration};
use aws_lc_rs::signature::EcdsaKeyPair;
use clap::Parser as _;
@@ -196,10 +196,10 @@ impl<'a> Harness<'a> {
)
.await
.unwrap();
- assert!(matches!(
+ assert_matches!(
transfer.first().unwrap(),
IncomingBankTransaction::Reserve { reserve_pub, .. } if *reserve_pub == key,
- ));
+ );
}
/// Send a transaction between two magnet accounts
@@ -436,10 +436,10 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> {
step("Test transfer failure init-tx");
harness.transfer(10).await;
set_failure_scenario(&["init-tx"]);
- assert!(matches!(
+ assert_matches!(
harness.worker().await,
Err(WorkerError::Injected(InjectedErr("init-tx")))
- ));
+ );
harness.worker().await?;
balance.expect(-10).await;
harness.worker().await?;
@@ -447,10 +447,10 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> {
step("Test transfer failure submit-tx");
harness.transfer(11).await;
set_failure_scenario(&["submit-tx"]);
- assert!(matches!(
+ assert_matches!(
harness.worker().await,
Err(WorkerError::Injected(InjectedErr("submit-tx")))
- ));
+ );
harness.worker().await?;
balance.expect(-11).await;
harness.worker().await?;
@@ -458,14 +458,14 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> {
step("Test transfer all failures");
harness.transfer(13).await;
set_failure_scenario(&["init-tx", "submit-tx"]);
- assert!(matches!(
+ assert_matches!(
harness.worker().await,
Err(WorkerError::Injected(InjectedErr("init-tx")))
- ));
- assert!(matches!(
+ );
+ assert_matches!(
harness.worker().await,
Err(WorkerError::Injected(InjectedErr("submit-tx")))
- ));
+ );
harness.worker().await?;
balance.expect(-13).await;
harness.worker().await?;
@@ -579,7 +579,7 @@ async fn online_harness(config: &Config, reset: bool) -> anyhow::Result<()> {
fn main() {
let args = Args::parse();
taler_main(CONFIG_SOURCE, args.common, async |cfg| match args.cmd {
- Command::Logic { reset } => logic_harness(&cfg, reset).await,
- Command::Online { reset } => online_harness(&cfg, reset).await,
+ Command::Logic { reset } => logic_harness(cfg, reset).await,
+ Command::Online { reset } => online_harness(cfg, reset).await,
});
}
diff --git a/taler-magnet-bank/src/db.rs b/taler-magnet-bank/src/db.rs
@@ -880,6 +880,8 @@ pub async fn transfer_unregister(db: &PgPool, req: &Unregistration) -> sqlx::Res
#[cfg(test)]
mod test {
+ use std::assert_matches;
+
use jiff::{Span, Timestamp, Zoned};
use serde_json::json;
use sqlx::{PgPool, Postgres, pool::PoolConnection, postgres::PgRow};
@@ -1153,7 +1155,7 @@ mod test {
value_date: date,
status: TxStatus::Completed,
};
- assert!(matches!(
+ assert_matches!(
make_transfer(
&pool,
&db::Transfer {
@@ -1169,7 +1171,7 @@ mod test {
.await
.unwrap(),
TransferResult::Success { .. }
- ));
+ );
db::initiated_submit_success(&mut db, 1, &Timestamp::now(), tx.code)
.await
.expect("status success");
diff --git a/taler-magnet-bank/src/main.rs b/taler-magnet-bank/src/main.rs
@@ -145,6 +145,6 @@ async fn run(cmd: Command, cfg: &Config) -> anyhow::Result<()> {
fn main() {
let args = Args::parse();
taler_main(CONFIG_SOURCE, args.common, async |cfg| {
- run(args.cmd, &cfg).await
+ run(args.cmd, cfg).await
});
}