taler-rust

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

test.rs (9999B)


      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_macros::db_test;
     45 use taler_test_utils::{
     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(db: &PgPool) -> Router {
     98     test_api(db.clone(), "EUR".parse().unwrap()).finalize()
     99 }
    100 
    101 #[db_test(sql_dir = "db", component = "taler-api")]
    102 async fn body_parsing(db: PgPool) {
    103     let server = setup(&db).await;
    104     let normal_body = TransferRequest {
    105         request_uid: Base32::rand(),
    106         amount: Amount::zero(&Currency::EUR),
    107         exchange_base_url: url("https://test.com"),
    108         wtid: Base32::rand(),
    109         credit_account: EXCHANGE.clone(),
    110         metadata: None,
    111     };
    112 
    113     // Check OK
    114     server
    115         .post("/taler-wire-gateway/transfer")
    116         .json(&normal_body)
    117         .deflate()
    118         .await
    119         .assert_ok_json::<TransferResponse>();
    120 
    121     // Headers check
    122     server
    123         .post("/taler-wire-gateway/transfer")
    124         .json(&normal_body)
    125         .remove(header::CONTENT_TYPE)
    126         .await
    127         .assert_error_status(
    128             ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED,
    129             StatusCode::UNSUPPORTED_MEDIA_TYPE,
    130         );
    131     server
    132         .post("/taler-wire-gateway/transfer")
    133         .json(&normal_body)
    134         .deflate()
    135         .remove(header::CONTENT_ENCODING)
    136         .await
    137         .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    138     server
    139         .post("/taler-wire-gateway/transfer")
    140         .json(&normal_body)
    141         .header(header::CONTENT_TYPE, "invalid")
    142         .await
    143         .assert_error_status(
    144             ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED,
    145             StatusCode::UNSUPPORTED_MEDIA_TYPE,
    146         );
    147     server
    148         .post("/taler-wire-gateway/transfer")
    149         .json(&normal_body)
    150         .header(header::CONTENT_ENCODING, "deflate")
    151         .await
    152         .assert_error(ErrorCode::GENERIC_COMPRESSION_INVALID);
    153     server
    154         .post("/taler-wire-gateway/transfer")
    155         .json(&normal_body)
    156         .header(header::CONTENT_ENCODING, "invalid")
    157         .await
    158         .assert_error_status(
    159             ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED,
    160             StatusCode::UNSUPPORTED_MEDIA_TYPE,
    161         );
    162 
    163     // Body size limit
    164     let huge_body = TransferRequest {
    165         credit_account: payto(format!(
    166             "payto:://test?message={:A<1$}",
    167             "payout", MAX_BODY_LENGTH
    168         )),
    169         ..normal_body
    170     };
    171     server
    172         .post("/taler-wire-gateway/transfer")
    173         .json(&huge_body)
    174         .await
    175         .assert_error(ErrorCode::GENERIC_UPLOAD_EXCEEDS_LIMIT);
    176     server
    177         .post("/taler-wire-gateway/transfer")
    178         .json(&huge_body)
    179         .deflate()
    180         .await
    181         .assert_error(ErrorCode::GENERIC_UPLOAD_EXCEEDS_LIMIT);
    182 }
    183 
    184 #[db_test(sql_dir = "db", component = "taler-api")]
    185 async fn errors(db: PgPool) {
    186     let server = setup(&db).await;
    187     server
    188         .get("/unknown")
    189         .await
    190         .assert_error(ErrorCode::GENERIC_ENDPOINT_UNKNOWN);
    191     server
    192         .post("/taler-revenue/config")
    193         .await
    194         .assert_error(ErrorCode::GENERIC_METHOD_INVALID);
    195 }
    196 
    197 #[db_test(sql_dir = "db", component = "taler-api")]
    198 async fn config(db: PgPool) {
    199     let server = setup(&db).await;
    200     server
    201         .get("/taler-wire-gateway/config")
    202         .await
    203         .assert_ok_json::<WireConfig>();
    204     server
    205         .get("/taler-prepared-transfer/config")
    206         .await
    207         .assert_ok_json::<PreparedTransferConfig>();
    208     server
    209         .get("/taler-revenue/config")
    210         .await
    211         .assert_ok_json::<RevenueConfig>();
    212 }
    213 
    214 #[db_test(sql_dir = "db", component = "taler-api")]
    215 async fn transfer(db: PgPool) {
    216     let server = setup(&db).await;
    217     transfer_routine(
    218         &server.prefix("/taler-wire-gateway"),
    219         TransferState::success,
    220         &EXCHANGE,
    221     )
    222     .await;
    223 }
    224 
    225 #[db_test(sql_dir = "db", component = "taler-api")]
    226 async fn outgoing_history(db: PgPool) {
    227     let server = setup(&db).await;
    228     out_history_routine(
    229         &server.prefix("/taler-wire-gateway"),
    230         tasks!({
    231             server
    232                 .post("/taler-wire-gateway/transfer")
    233                 .json(json!({
    234                     "request_uid": HashCode::rand(),
    235                     "amount": amount("EUR:1"),
    236                     "exchange_base_url": url("http://exchange.taler"),
    237                     "wtid": ShortHashCode::rand(),
    238                     "credit_account": EXCHANGE.clone(),
    239                 }))
    240                 .await
    241                 .assert_ok_json::<TransferResponse>();
    242         }),
    243         tasks!(),
    244     )
    245     .await;
    246 }
    247 
    248 #[db_test(sql_dir = "db", component = "taler-api")]
    249 async fn admin_add_incoming(db: PgPool) {
    250     let server = setup(&db).await;
    251     admin_add_incoming_routine(
    252         &server.prefix("/taler-wire-gateway"),
    253         &server.prefix("/taler-prepared-transfer"),
    254         &EXCHANGE,
    255         &EXCHANGE,
    256     )
    257     .await;
    258 }
    259 
    260 #[db_test(sql_dir = "db", component = "taler-api")]
    261 async fn in_history(db: PgPool) {
    262     let server = setup(&db).await;
    263     in_history_routine(
    264         &server.prefix("/taler-wire-gateway"),
    265         &server.prefix("/taler-prepared-transfer"),
    266         &EXCHANGE,
    267         &EXCHANGE,
    268         tasks!(),
    269         tasks!(),
    270     )
    271     .await;
    272 }
    273 
    274 #[db_test(sql_dir = "db", component = "taler-api")]
    275 async fn revenue(db: PgPool) {
    276     let server = setup(&db).await;
    277     revenue_routine(
    278         &server.prefix("/taler-wire-gateway"),
    279         &server.prefix("/taler-revenue"),
    280         &EXCHANGE,
    281         tasks!(),
    282         tasks!(),
    283     )
    284     .await;
    285 }
    286 
    287 #[db_test(sql_dir = "db", component = "taler-api")]
    288 async fn account_check(db: PgPool) {
    289     let server = setup(&db).await;
    290     server
    291         .get("/taler-wire-gateway/account/check")
    292         .query("account", "payto://test")
    293         .await
    294         .assert_status(StatusCode::NOT_IMPLEMENTED);
    295 }
    296 
    297 async fn check_in(pool: &PgPool) -> Vec<Status> {
    298     sqlx::query(
    299             "
    300             SELECT pending_recurrent_in.authorization_pub IS NOT NULL, bounced.tx_in_id IS NOT NULL, type, taler_in.account_pub
    301             FROM tx_in
    302                 LEFT JOIN taler_in USING (tx_in_id)
    303                 LEFT JOIN pending_recurrent_in USING (tx_in_id)
    304                 LEFT JOIN bounced USING (tx_in_id)
    305             ORDER BY tx_in.tx_in_id
    306         ",
    307         )
    308         .try_map(|r: PgRow| {
    309             Ok(
    310                 if r.try_get_flag(0)? {
    311                     Status::Pending
    312                 } else if r.try_get_flag(1)? {
    313                     Status::Bounced
    314                 } else {
    315                     match r.try_get(2)? {
    316                         None => Status::Simple,
    317                         Some(IncomingType::reserve) => Status::Reserve(r.try_get(3)?),
    318                         Some(IncomingType::kyc) => Status::Kyc(r.try_get(3)?),
    319                         Some(e) => unreachable!("{e:?}")
    320                     }
    321                 }
    322             )
    323         })
    324         .fetch_all(pool)
    325         .await
    326         .unwrap()
    327 }
    328 
    329 #[db_test(sql_dir = "db", component = "taler-api")]
    330 async fn registration(db: PgPool) {
    331     let server = setup(&db).await;
    332     registration_routine(
    333         &server.prefix("/taler-wire-gateway"),
    334         &server.prefix("/taler-prepared-transfer"),
    335         &EXCHANGE,
    336         &EXCHANGE,
    337         &UNKNOWN,
    338         || check_in(&db),
    339     )
    340     .await;
    341 }