mod.rs (9660B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2024, 2025, 2026 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::sync::Arc; 18 19 use db::notification_listener; 20 use jiff::Timestamp; 21 use sqlx::PgPool; 22 use taler_api::{ 23 api::{ 24 Router, TalerApi, TalerRouter as _, revenue::Revenue, transfer::PreparedTransfer, 25 wire::WireGateway, 26 }, 27 auth::AuthMethod, 28 error::{ApiResult, failure, failure_code}, 29 subject::fmt_in_subject, 30 }; 31 use taler_common::{ 32 api_params::{History, Page}, 33 api_revenue::RevenueIncomingHistory, 34 api_transfer::{ 35 RegistrationRequest, RegistrationResponse, SubjectFormat, TransferSubject, Unregistration, 36 }, 37 api_wire::{ 38 AddIncomingRequest, AddIncomingResponse, AddKycauthRequest, AddMappedRequest, 39 IncomingHistory, OutgoingHistory, TransferList, TransferRequest, TransferResponse, 40 TransferState, TransferStatus, 41 }, 42 db::IncomingType, 43 error_code::ErrorCode, 44 types::{ 45 amount::Currency, 46 payto::{FullQuery, payto}, 47 timestamp::TalerTimestamp, 48 }, 49 }; 50 use taler_test_utils::db::db_test_setup_manual; 51 use tokio::sync::watch::Sender; 52 53 use crate::common::db::AddIncomingResult; 54 55 pub mod db; 56 57 /// Taler API implementation for tests 58 pub struct TestApi { 59 currency: Currency, 60 pool: PgPool, 61 outgoing_channel: Sender<i64>, 62 incoming_channel: Sender<i64>, 63 } 64 65 impl TalerApi for TestApi { 66 fn currency(&self) -> &str { 67 self.currency.as_ref() 68 } 69 70 fn implementation(&self) -> &'static str { 71 "urn:net:taler:specs:taler-test-api:taler-rust" 72 } 73 } 74 75 impl WireGateway for TestApi { 76 async fn transfer(&self, req: TransferRequest) -> ApiResult<TransferResponse> { 77 req.credit_account.query::<FullQuery>()?; 78 let result = db::transfer(&self.pool, &req).await?; 79 match result { 80 db::TransferResult::Success(transfer_response) => Ok(transfer_response), 81 db::TransferResult::RequestUidReuse => { 82 Err(failure_code(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED)) 83 } 84 db::TransferResult::WtidReuse => { 85 Err(failure_code(ErrorCode::BANK_TRANSFER_WTID_REUSED)) 86 } 87 } 88 } 89 90 async fn transfer_page( 91 &self, 92 page: Page, 93 status: Option<TransferState>, 94 ) -> ApiResult<TransferList> { 95 Ok(TransferList { 96 transfers: db::transfer_page(&self.pool, &status, &page, &self.currency).await?, 97 debit_account: payto("payto://test"), 98 }) 99 } 100 101 async fn transfer_by_id(&self, id: u64) -> ApiResult<Option<TransferStatus>> { 102 Ok(db::transfer_by_id(&self.pool, id, &self.currency).await?) 103 } 104 105 async fn outgoing_history(&self, params: History) -> ApiResult<OutgoingHistory> { 106 let txs = db::outgoing_revenue(&self.pool, ¶ms, &self.currency, || { 107 self.outgoing_channel.subscribe() 108 }) 109 .await?; 110 Ok(OutgoingHistory { 111 outgoing_transactions: txs, 112 debit_account: payto("payto://test"), 113 }) 114 } 115 116 async fn incoming_history(&self, params: History) -> ApiResult<IncomingHistory> { 117 let txs = db::incoming_history(&self.pool, ¶ms, &self.currency, || { 118 self.incoming_channel.subscribe() 119 }) 120 .await?; 121 Ok(IncomingHistory { 122 incoming_transactions: txs, 123 credit_account: payto("payto://test"), 124 }) 125 } 126 127 async fn add_incoming_reserve( 128 &self, 129 req: AddIncomingRequest, 130 ) -> ApiResult<AddIncomingResponse> { 131 let res = db::add_incoming( 132 &self.pool, 133 &req.amount, 134 &req.debit_account, 135 "", 136 &Timestamp::now(), 137 IncomingType::reserve, 138 &req.reserve_pub, 139 ) 140 .await?; 141 match res { 142 AddIncomingResult::Success { id, created_at } => Ok(AddIncomingResponse { 143 timestamp: created_at.into(), 144 row_id: id, 145 }), 146 AddIncomingResult::ReservePubReuse => { 147 Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)) 148 } 149 AddIncomingResult::UnknownMapping | AddIncomingResult::MappingReuse => { 150 unreachable!("mapping not used") 151 } 152 } 153 } 154 155 async fn add_incoming_kyc(&self, req: AddKycauthRequest) -> ApiResult<AddIncomingResponse> { 156 let res = db::add_incoming( 157 &self.pool, 158 &req.amount, 159 &req.debit_account, 160 "", 161 &Timestamp::now(), 162 IncomingType::kyc, 163 &req.account_pub, 164 ) 165 .await?; 166 match res { 167 AddIncomingResult::Success { id, created_at } => Ok(AddIncomingResponse { 168 timestamp: created_at.into(), 169 row_id: id, 170 }), 171 AddIncomingResult::ReservePubReuse => { 172 Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)) 173 } 174 AddIncomingResult::UnknownMapping | AddIncomingResult::MappingReuse => { 175 unreachable!("mapping not used") 176 } 177 } 178 } 179 180 async fn add_incoming_mapped(&self, req: AddMappedRequest) -> ApiResult<AddIncomingResponse> { 181 let res = db::add_incoming( 182 &self.pool, 183 &req.amount, 184 &req.debit_account, 185 "", 186 &Timestamp::now(), 187 IncomingType::map, 188 &req.authorization_pub, 189 ) 190 .await?; 191 match res { 192 AddIncomingResult::Success { id, created_at } => Ok(AddIncomingResponse { 193 timestamp: created_at.into(), 194 row_id: id, 195 }), 196 AddIncomingResult::ReservePubReuse => { 197 Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)) 198 } 199 AddIncomingResult::UnknownMapping => { 200 Err(failure_code(ErrorCode::BANK_TRANSFER_MAPPING_UNKNOWN)) 201 } 202 AddIncomingResult::MappingReuse => { 203 Err(failure_code(ErrorCode::BANK_TRANSFER_MAPPING_REUSED)) 204 } 205 } 206 } 207 208 fn support_account_check(&self) -> bool { 209 false 210 } 211 } 212 213 impl Revenue for TestApi { 214 async fn history(&self, params: History) -> ApiResult<RevenueIncomingHistory> { 215 let txs = db::revenue_history(&self.pool, ¶ms, &self.currency, || { 216 self.incoming_channel.subscribe() 217 }) 218 .await?; 219 Ok(RevenueIncomingHistory { 220 incoming_transactions: txs, 221 credit_account: payto("payto://test"), 222 }) 223 } 224 } 225 226 impl PreparedTransfer for TestApi { 227 fn supported_formats(&self) -> &[SubjectFormat] { 228 &[SubjectFormat::SIMPLE] 229 } 230 231 async fn registration(&self, req: RegistrationRequest) -> ApiResult<RegistrationResponse> { 232 match db::transfer_register(&self.pool, &req).await? { 233 db::RegistrationResult::Success => { 234 let simple = TransferSubject::Simple { 235 credit_amount: req.credit_amount, 236 subject: if req.authorization_pub == req.account_pub && !req.recurrent { 237 fmt_in_subject(req.r#type.into(), &req.account_pub) 238 } else { 239 fmt_in_subject(IncomingType::map, &req.authorization_pub) 240 }, 241 }; 242 ApiResult::Ok(RegistrationResponse { 243 subjects: vec![simple], 244 expiration: TalerTimestamp::Never, 245 }) 246 } 247 db::RegistrationResult::ReservePubReuse => { 248 ApiResult::Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)) 249 } 250 } 251 } 252 253 async fn unregistration(&self, req: Unregistration) -> ApiResult<()> { 254 if !db::transfer_unregister(&self.pool, &req).await? { 255 Err(failure( 256 ErrorCode::BANK_TRANSACTION_NOT_FOUND, 257 format!("Prepared transfer '{}' not found", req.authorization_pub), 258 )) 259 } else { 260 Ok(()) 261 } 262 } 263 } 264 265 pub fn test_api(pool: PgPool, currency: Currency) -> Router { 266 let outgoing_channel = Sender::new(0); 267 let incoming_channel = Sender::new(0); 268 let wg = TestApi { 269 currency, 270 pool: pool.clone(), 271 outgoing_channel: outgoing_channel.clone(), 272 incoming_channel: incoming_channel.clone(), 273 }; 274 tokio::spawn(notification_listener( 275 pool, 276 outgoing_channel, 277 incoming_channel, 278 )); 279 let state = Arc::new(wg); 280 Router::new() 281 .wire_gateway(state.clone(), AuthMethod::None) 282 .prepared_transfer(state.clone()) 283 .revenue(state, AuthMethod::None) 284 } 285 286 pub async fn setup() -> (Router, PgPool) { 287 let (_, pool) = db_test_setup_manual("db".as_ref(), "taler-api").await; 288 ( 289 test_api(pool.clone(), "EUR".parse().unwrap()).finalize(), 290 pool, 291 ) 292 }