taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

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, &params, &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, &params, &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, &params, &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 }