cyclos-harness.rs (11591B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2025 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 use std::{str::FromStr as _, time::Duration}; 18 19 use clap::Parser as _; 20 use jiff::Timestamp; 21 use owo_colors::OwoColorize as _; 22 use sqlx::PgPool; 23 use taler_build::long_version; 24 use taler_common::{ 25 CommonArgs, 26 api_common::{EddsaPublicKey, HashCode, ShortHashCode, rand_edsa_pub_key}, 27 api_params::{History, Page}, 28 api_wire::{IncomingBankTransaction, TransferRequest, TransferState}, 29 config::Config, 30 db::{dbinit, pool}, 31 taler_main, 32 types::{ 33 amount::{Amount, Currency, Decimal, decimal}, 34 url, 35 }, 36 }; 37 use taler_cyclos::{ 38 CyclosId, FullCyclosPayto, 39 config::{AccountType, parse_db_cfg}, 40 constants::CONFIG_SOURCE, 41 cyclos_api::{api::CyclosAuth, client::Client}, 42 db::{self, TransferResult}, 43 worker::{Worker, WorkerResult}, 44 }; 45 46 /// Cyclos Adapter harness test suite 47 #[derive(clap::Parser, Debug)] 48 #[command(long_version = long_version(), about, long_about = None)] 49 struct Args { 50 #[clap(flatten)] 51 common: CommonArgs, 52 53 #[command(subcommand)] 54 cmd: Command, 55 } 56 57 #[derive(clap::Subcommand, Debug)] 58 enum Command { 59 /// Run logic tests 60 Logic { 61 #[clap(long, short)] 62 reset: bool, 63 }, 64 /// Run online tests 65 Online { 66 #[clap(long, short)] 67 reset: bool, 68 }, 69 } 70 71 fn step(step: &str) { 72 println!("{}", step.green()); 73 } 74 75 struct Harness<'a> { 76 pool: &'a PgPool, 77 client: Client<'a>, 78 wire: Client<'a>, 79 client_id: u64, 80 wire_id: u64, 81 currency: Currency, 82 } 83 84 impl<'a> Harness<'a> { 85 async fn balance(&self) -> (Decimal, Decimal) { 86 let (exchange, client) = 87 tokio::try_join!(self.wire.accounts(), self.client.accounts()).unwrap(); 88 ( 89 exchange[0].status.available_balance, 90 client[0].status.available_balance, 91 ) 92 } 93 94 /// Send transaction from client to exchange 95 async fn client_send(&self, subject: &str, amount: Decimal) { 96 self.client 97 .direct_payment(self.wire_id, amount, subject) 98 .await 99 .unwrap(); 100 } 101 102 /// Send transaction from exchange to client 103 async fn exchange_send(&self, subject: &str, amount: Decimal) { 104 self.wire 105 .direct_payment(self.client_id, amount, subject) 106 .await 107 .unwrap(); 108 } 109 110 /// Run the worker once 111 async fn worker(&'a self) -> WorkerResult { 112 let db = &mut self.pool.acquire().await.unwrap().detach(); 113 let account = self.wire.accounts().await.unwrap()[0].clone(); 114 Worker { 115 db, 116 currency: Currency::from_str("TEST").unwrap(), 117 client: &self.wire, 118 account_type_id: *account.ty.id, 119 account_type: AccountType::Exchange, 120 } 121 .run() 122 .await 123 } 124 125 async fn expect_incoming(&self, key: EddsaPublicKey) { 126 let transfer = db::incoming_history( 127 self.pool, 128 &History { 129 page: Page { 130 limit: -1, 131 offset: None, 132 }, 133 timeout_ms: None, 134 }, 135 &self.currency, 136 || tokio::sync::watch::channel(0).1, 137 ) 138 .await 139 .unwrap(); 140 assert!(matches!( 141 transfer.first().unwrap(), 142 IncomingBankTransaction::Reserve { reserve_pub, .. } if *reserve_pub == key, 143 )); 144 } 145 146 async fn custom_transfer(&self, amount: Decimal, creditor: &FullCyclosPayto) -> u64 { 147 let res = db::make_transfer( 148 self.pool, 149 &TransferRequest { 150 request_uid: HashCode::rand(), 151 amount: Amount::new_decimal(&self.currency, amount), 152 exchange_base_url: url("https://test.com"), 153 wtid: ShortHashCode::rand(), 154 credit_account: creditor.as_payto(), 155 }, 156 creditor, 157 &Timestamp::now(), 158 ) 159 .await 160 .unwrap(); 161 match res { 162 TransferResult::Success { id, .. } => id, 163 TransferResult::RequestUidReuse | TransferResult::WtidReuse => unreachable!(), 164 } 165 } 166 167 async fn expect_transfer_status(&self, id: u64, status: TransferState, msg: Option<&str>) { 168 let mut attempts = 0; 169 loop { 170 let transfer = db::transfer_by_id(self.pool, id, &self.currency) 171 .await 172 .unwrap() 173 .unwrap(); 174 if (transfer.status, transfer.status_msg.as_deref()) == (status, msg) { 175 return; 176 } 177 if attempts > 40 { 178 assert_eq!( 179 (transfer.status, transfer.status_msg.as_deref()), 180 (status, msg) 181 ); 182 } 183 attempts += 1; 184 tokio::time::sleep(Duration::from_millis(200)).await; 185 } 186 } 187 } 188 189 struct Balances<'a> { 190 client: &'a Harness<'a>, 191 exchange_balance: Decimal, 192 client_balance: Decimal, 193 } 194 195 impl<'a> Balances<'a> { 196 pub async fn new(client: &'a Harness<'a>) -> Self { 197 let (exchange_balance, client_balance) = client.balance().await; 198 Self { 199 client, 200 exchange_balance, 201 client_balance, 202 } 203 } 204 205 async fn expect_add(&mut self, diff: Decimal) { 206 self.exchange_balance = self.exchange_balance.try_add(&diff).unwrap(); 207 self.client_balance = self.client_balance.try_sub(&diff).unwrap(); 208 209 let current = self.client.balance().await; 210 assert_eq!( 211 current, 212 (self.exchange_balance, self.client_balance), 213 "{current:?} {diff}" 214 ); 215 } 216 217 async fn expect_sub(&mut self, diff: Decimal) { 218 self.exchange_balance = self.exchange_balance.try_sub(&diff).unwrap(); 219 self.client_balance = self.client_balance.try_add(&diff).unwrap(); 220 221 let current = self.client.balance().await; 222 assert_eq!( 223 current, 224 (self.exchange_balance, self.client_balance), 225 "{current:?} {diff}" 226 ); 227 } 228 } 229 230 /// Run logic tests against real Cyclos backend 231 async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { 232 step("Run Cyclos logic harness tests"); 233 234 step("Prepare db"); 235 let db_cfg = parse_db_cfg(cfg)?; 236 let pool = pool(db_cfg.cfg, "cyclos").await?; 237 let mut db = pool.acquire().await?.detach(); 238 dbinit(&mut db, db_cfg.sql_dir.as_ref(), "cyclos", reset).await?; 239 240 let client = reqwest::Client::new(); 241 let api_url = reqwest::Url::from_str("http://localhost:8080/api/").unwrap(); 242 let wire = Client { 243 client: &client, 244 api_url: &api_url, 245 auth: &CyclosAuth::Basic { 246 username: "wire".into(), 247 password: "f20n4X3qV44dNoZUmpeU".into(), 248 }, 249 }; 250 let client = Client { 251 client: &client, 252 api_url: &api_url, 253 auth: &CyclosAuth::Basic { 254 username: "client".into(), 255 password: "1EkY5JJMrkwyvv9yK7x4".into(), 256 }, 257 }; 258 let currency = Currency::from_str("TEST").unwrap(); 259 260 let harness = Harness { 261 pool: &pool, 262 client_id: *client.whoami().await.unwrap().id, 263 wire_id: *wire.whoami().await.unwrap().id, 264 client, 265 wire, 266 currency, 267 }; 268 269 step("Warmup"); 270 harness.worker().await.unwrap(); 271 272 let now = Timestamp::now(); 273 let balance = &mut Balances::new(&harness).await; 274 275 step("Test incoming talerable transaction"); 276 // Send talerable transaction 277 let reserve_pub = rand_edsa_pub_key(); 278 let amount = decimal("3.3"); 279 harness 280 .client_send(&format!("Taler {reserve_pub}"), amount) 281 .await; 282 // Sync and register 283 harness.worker().await?; 284 harness.expect_incoming(reserve_pub).await; 285 balance.expect_add(amount).await; 286 287 step("Test incoming malformed transaction"); 288 // Send malformed transaction 289 let amount = decimal("3.4"); 290 harness 291 .client_send(&format!("Malformed test {now}"), amount) 292 .await; 293 balance.expect_add(amount).await; 294 // Sync and bounce 295 harness.worker().await?; 296 balance.expect_sub(amount).await; 297 298 step("Test transfer transactions"); 299 let amount = decimal("3.5"); 300 // Init a transfer to client 301 let transfer_id = harness 302 .custom_transfer( 303 amount, 304 &FullCyclosPayto::new(CyclosId(harness.client_id), "Client".to_string()), 305 ) 306 .await; 307 // Check transfer pending 308 harness 309 .expect_transfer_status(transfer_id, TransferState::pending, None) 310 .await; 311 // Should send 312 harness.worker().await?; 313 // Wait for transaction to finalize 314 balance.expect_sub(amount).await; 315 // Should register 316 harness.worker().await?; 317 // Check transfer is now successful 318 harness 319 .expect_transfer_status(transfer_id, TransferState::success, None) 320 .await; 321 322 step("Test transfer to self"); 323 // Init a transfer to self 324 let transfer_id = harness 325 .custom_transfer( 326 decimal("10.1"), 327 &FullCyclosPayto::new(CyclosId(harness.wire_id), "Self".to_string()), 328 ) 329 .await; 330 // Should failed 331 harness.worker().await?; 332 // Check transfer failed 333 harness 334 .expect_transfer_status( 335 transfer_id, 336 TransferState::permanent_failure, 337 Some("permissionDenied - The operation was denied because a required permission was not granted"), 338 ) 339 .await; 340 341 step("Test transfer to unknown account"); 342 // Init a transfer to self 343 let transfer_id = harness 344 .custom_transfer( 345 decimal("10.1"), 346 &FullCyclosPayto::new(CyclosId(42), "Unknown".to_string()), 347 ) 348 .await; 349 // Should failed 350 harness.worker().await?; 351 // Check transfer failed 352 harness 353 .expect_transfer_status( 354 transfer_id, 355 TransferState::permanent_failure, 356 Some("unknown BasicUser 42"), 357 ) 358 .await; 359 360 step("Test unexpected outgoing"); 361 // Manual tx from the exchange 362 let amount = decimal("4"); 363 harness 364 .exchange_send(&format!("What is this ? {now}"), amount) 365 .await; 366 harness.worker().await?; 367 // Wait for transaction to finalize 368 balance.expect_sub(amount).await; 369 harness.worker().await?; 370 371 step("Finish"); 372 373 Ok(()) 374 } 375 376 /// Run online tests against real Cyclos backend 377 async fn online_harness(config: &Config, reset: bool) -> anyhow::Result<()> { 378 step("Run Cyclos harness tests"); 379 380 step("Finish"); 381 382 Ok(()) 383 } 384 385 fn main() { 386 let args = Args::parse(); 387 taler_main(CONFIG_SOURCE, args.common, |cfg| async move { 388 match args.cmd { 389 Command::Logic { reset } => logic_harness(&cfg, reset).await, 390 Command::Online { reset } => online_harness(&cfg, reset).await, 391 } 392 }); 393 }