taler-rust

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

test.rs (9666B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 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::{
     18     str::FromStr,
     19     sync::{Arc, LazyLock},
     20 };
     21 
     22 use axum::{
     23     Router,
     24     http::{StatusCode, header},
     25 };
     26 use serde_json::json;
     27 use sqlx::{PgPool, Row as _, postgres::PgRow};
     28 use taler_common::{
     29     api::{
     30         HashCode, ShortHashCode,
     31         prepared::PreparedTransferConfig,
     32         revenue::RevenueConfig,
     33         wire::{TransferRequest, TransferResponse, TransferState, WireConfig},
     34     },
     35     db::IncomingType,
     36     error_code::ErrorCode,
     37     types::{
     38         amount::{Amount, Currency, amount},
     39         base32::Base32,
     40         payto::{FullIbanPayto, PaytoURI, payto},
     41         url,
     42     },
     43 };
     44 use taler_test_utils::{
     45     db::db_test_setup_manual,
     46     routine::{
     47         Status, admin_add_incoming_routine, in_history_routine, out_history_routine,
     48         registration_routine, revenue_routine, transfer_routine,
     49     },
     50     server::TestServer,
     51     tasks,
     52 };
     53 use tokio::sync::watch::Sender;
     54 
     55 use crate::{
     56     api::TalerRouter,
     57     auth::AuthMethod,
     58     constants::MAX_BODY_LENGTH,
     59     db::TypeHelper as _,
     60     test::{api::TestApi, db::notification_listener},
     61 };
     62 
     63 mod api;
     64 mod db;
     65 
     66 static PAYTO: LazyLock<FullIbanPayto> = LazyLock::new(|| {
     67     FullIbanPayto::from_str("payto://iban/HU02162000031000164800000000?receiver-name=Smith")
     68         .unwrap()
     69 });
     70 static EXCHANGE: LazyLock<PaytoURI> = LazyLock::new(|| PAYTO.as_uri());
     71 static UNKNOWN: LazyLock<PaytoURI> =
     72     LazyLock::new(|| payto("payto://iban/HU60162006491000639900000000?receiver-name=Unknown"));
     73 
     74 fn test_api(pool: PgPool, currency: Currency) -> Router {
     75     let outgoing_channel = Sender::new(0);
     76     let incoming_channel = Sender::new(0);
     77     let wg = TestApi::new(
     78         currency,
     79         pool.clone(),
     80         outgoing_channel.clone(),
     81         incoming_channel.clone(),
     82         PAYTO.clone(),
     83     );
     84     tokio::spawn(notification_listener(
     85         pool,
     86         outgoing_channel,
     87         incoming_channel,
     88     ));
     89     let state = Arc::new(wg);
     90     Router::new()
     91         .wire_gateway(state.clone(), AuthMethod::None)
     92         .prepared_transfer(state.clone())
     93         .revenue(state.clone(), AuthMethod::None)
     94         .observability(state, AuthMethod::None)
     95 }
     96 
     97 async fn setup() -> (Router, PgPool) {
     98     let (_, pool) = db_test_setup_manual("db".as_ref(), "taler-api").await;
     99     (
    100         test_api(pool.clone(), "EUR".parse().unwrap()).finalize(),
    101         pool,
    102     )
    103 }
    104 
    105 #[tokio::test]
    106 async fn body_parsing() {
    107     let (server, _) = setup().await;
    108     let normal_body = TransferRequest {
    109         request_uid: Base32::rand(),
    110         amount: Amount::zero(&Currency::EUR),
    111         exchange_base_url: url("https://test.com"),
    112         wtid: Base32::rand(),
    113         credit_account: EXCHANGE.clone(),
    114         metadata: None,
    115     };
    116 
    117     // Check OK
    118     server
    119         .post("/taler-wire-gateway/transfer")
    120         .json(&normal_body)
    121         .deflate()
    122         .await
    123         .assert_ok_json::<TransferResponse>();
    124 
    125     // Headers check
    126     server
    127         .post("/taler-wire-gateway/transfer")
    128         .json(&normal_body)
    129         .remove(header::CONTENT_TYPE)
    130         .await
    131         .assert_error_status(
    132             ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED,
    133             StatusCode::UNSUPPORTED_MEDIA_TYPE,
    134         );
    135     server
    136         .post("/taler-wire-gateway/transfer")
    137         .json(&normal_body)
    138         .deflate()
    139         .remove(header::CONTENT_ENCODING)
    140         .await
    141         .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    142     server
    143         .post("/taler-wire-gateway/transfer")
    144         .json(&normal_body)
    145         .header(header::CONTENT_TYPE, "invalid")
    146         .await
    147         .assert_error_status(
    148             ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED,
    149             StatusCode::UNSUPPORTED_MEDIA_TYPE,
    150         );
    151     server
    152         .post("/taler-wire-gateway/transfer")
    153         .json(&normal_body)
    154         .header(header::CONTENT_ENCODING, "deflate")
    155         .await
    156         .assert_error(ErrorCode::GENERIC_COMPRESSION_INVALID);
    157     server
    158         .post("/taler-wire-gateway/transfer")
    159         .json(&normal_body)
    160         .header(header::CONTENT_ENCODING, "invalid")
    161         .await
    162         .assert_error_status(
    163             ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED,
    164             StatusCode::UNSUPPORTED_MEDIA_TYPE,
    165         );
    166 
    167     // Body size limit
    168     let huge_body = TransferRequest {
    169         credit_account: payto(format!(
    170             "payto:://test?message={:A<1$}",
    171             "payout", MAX_BODY_LENGTH
    172         )),
    173         ..normal_body
    174     };
    175     server
    176         .post("/taler-wire-gateway/transfer")
    177         .json(&huge_body)
    178         .await
    179         .assert_error(ErrorCode::GENERIC_UPLOAD_EXCEEDS_LIMIT);
    180     server
    181         .post("/taler-wire-gateway/transfer")
    182         .json(&huge_body)
    183         .deflate()
    184         .await
    185         .assert_error(ErrorCode::GENERIC_UPLOAD_EXCEEDS_LIMIT);
    186 }
    187 
    188 #[tokio::test]
    189 async fn errors() {
    190     let (server, _) = setup().await;
    191     server
    192         .get("/unknown")
    193         .await
    194         .assert_error(ErrorCode::GENERIC_ENDPOINT_UNKNOWN);
    195     server
    196         .post("/taler-revenue/config")
    197         .await
    198         .assert_error(ErrorCode::GENERIC_METHOD_INVALID);
    199 }
    200 
    201 #[tokio::test]
    202 async fn config() {
    203     let (server, _) = setup().await;
    204     server
    205         .get("/taler-wire-gateway/config")
    206         .await
    207         .assert_ok_json::<WireConfig>();
    208     server
    209         .get("/taler-prepared-transfer/config")
    210         .await
    211         .assert_ok_json::<PreparedTransferConfig>();
    212     server
    213         .get("/taler-revenue/config")
    214         .await
    215         .assert_ok_json::<RevenueConfig>();
    216 }
    217 
    218 #[tokio::test]
    219 async fn transfer() {
    220     let (server, _) = setup().await;
    221     transfer_routine(
    222         &server.prefix("/taler-wire-gateway"),
    223         TransferState::success,
    224         &EXCHANGE,
    225     )
    226     .await;
    227 }
    228 
    229 #[tokio::test]
    230 async fn outgoing_history() {
    231     let (server, _) = &setup().await;
    232     out_history_routine(
    233         &server.prefix("/taler-wire-gateway"),
    234         tasks!({
    235             server
    236                 .post("/taler-wire-gateway/transfer")
    237                 .json(json!({
    238                     "request_uid": HashCode::rand(),
    239                     "amount": amount("EUR:1"),
    240                     "exchange_base_url": url("http://exchange.taler"),
    241                     "wtid": ShortHashCode::rand(),
    242                     "credit_account": EXCHANGE.clone(),
    243                 }))
    244                 .await
    245                 .assert_ok_json::<TransferResponse>();
    246         }),
    247         tasks!(),
    248     )
    249     .await;
    250 }
    251 
    252 #[tokio::test]
    253 async fn admin_add_incoming() {
    254     let (server, _) = setup().await;
    255     admin_add_incoming_routine(
    256         &server.prefix("/taler-wire-gateway"),
    257         &server.prefix("/taler-prepared-transfer"),
    258         &EXCHANGE,
    259         &EXCHANGE,
    260     )
    261     .await;
    262 }
    263 
    264 #[tokio::test]
    265 async fn in_history() {
    266     let (server, _) = setup().await;
    267     in_history_routine(
    268         &server.prefix("/taler-wire-gateway"),
    269         &server.prefix("/taler-prepared-transfer"),
    270         &EXCHANGE,
    271         &EXCHANGE,
    272         tasks!(),
    273         tasks!(),
    274     )
    275     .await;
    276 }
    277 
    278 #[tokio::test]
    279 async fn revenue() {
    280     let (server, _) = setup().await;
    281     revenue_routine(
    282         &server.prefix("/taler-wire-gateway"),
    283         &server.prefix("/taler-revenue"),
    284         &EXCHANGE,
    285         tasks!(),
    286         tasks!(),
    287     )
    288     .await;
    289 }
    290 
    291 #[tokio::test]
    292 async fn account_check() {
    293     let (server, _) = setup().await;
    294     server
    295         .get("/taler-wire-gateway/account/check")
    296         .query("account", "payto://test")
    297         .await
    298         .assert_status(StatusCode::NOT_IMPLEMENTED);
    299 }
    300 
    301 async fn check_in(pool: &PgPool) -> Vec<Status> {
    302     sqlx::query(
    303             "
    304             SELECT pending_recurrent_in.authorization_pub IS NOT NULL, bounced.tx_in_id IS NOT NULL, type, taler_in.account_pub
    305             FROM tx_in
    306                 LEFT JOIN taler_in USING (tx_in_id)
    307                 LEFT JOIN pending_recurrent_in USING (tx_in_id)
    308                 LEFT JOIN bounced USING (tx_in_id)
    309             ORDER BY tx_in.tx_in_id
    310         ",
    311         )
    312         .try_map(|r: PgRow| {
    313             Ok(
    314                 if r.try_get_flag(0)? {
    315                     Status::Pending
    316                 } else if r.try_get_flag(1)? {
    317                     Status::Bounced
    318                 } else {
    319                     match r.try_get(2)? {
    320                         None => Status::Simple,
    321                         Some(IncomingType::reserve) => Status::Reserve(r.try_get(3)?),
    322                         Some(IncomingType::kyc) => Status::Kyc(r.try_get(3)?),
    323                         Some(e) => unreachable!("{e:?}")
    324                     }
    325                 }
    326             )
    327         })
    328         .fetch_all(pool)
    329         .await
    330         .unwrap()
    331 }
    332 
    333 #[tokio::test]
    334 async fn registration() {
    335     let (server, pool) = setup().await;
    336     registration_routine(
    337         &server.prefix("/taler-wire-gateway"),
    338         &server.prefix("/taler-prepared-transfer"),
    339         &EXCHANGE,
    340         &EXCHANGE,
    341         &UNKNOWN,
    342         || check_in(&pool),
    343     )
    344     .await;
    345 }