taler-rust

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

test.rs (8988B)


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