exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

test_wire_fee.c (13202B)


      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 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 General Public License for more details.
     12 
     13   You should have received a copy of the GNU General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file exchangedb/test_wire_fee.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `wire_fee`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_insert_wire_fee(),
     23  * #TALER_EXCHANGEDB_get_wire_fee(),
     24  * #TALER_EXCHANGEDB_get_wire_fee_by_time() and
     25  * #TALER_EXCHANGEDB_iterate_wire_fees().
     26  *
     27  * `wire_fee` has no foreign keys.  What the checks pin down is the shape of
     28  * the validity intervals: `get_wire_fee` treats them as [start,end), and
     29  * `get_wire_fee_by_time` returns an invalid amount rather than an error
     30  * when the range it is asked about spans two different fees.
     31  */
     32 #include "test_common.h"
     33 #include "exchange-database/insert_wire_fee.h"
     34 #include "exchange-database/get_wire_fee.h"
     35 #include "exchange-database/get_wire_fee_by_time.h"
     36 #include "exchange-database/iterate_wire_fees.h"
     37 
     38 
     39 /**
     40  * Wire method the checks use.
     41  */
     42 #define METHOD "x-taler-bank"
     43 
     44 /**
     45  * Start of the first fee period; a fixed point in time so that the checks
     46  * do not depend on when they run.
     47  */
     48 #define T0 1600000000
     49 
     50 /**
     51  * One hour, in seconds.
     52  */
     53 #define HOUR 3600
     54 
     55 
     56 /**
     57  * Build a timestamp from a number of seconds since the epoch.
     58  *
     59  * @param secs seconds since the epoch
     60  * @return the timestamp
     61  */
     62 static struct GNUNET_TIME_Timestamp
     63 ts (uint64_t secs)
     64 {
     65   struct GNUNET_TIME_Absolute abs = {
     66     .abs_value_us = secs * 1000LLU * 1000LLU
     67   };
     68 
     69   return GNUNET_TIME_absolute_to_timestamp (abs);
     70 }
     71 
     72 
     73 /**
     74  * Closure for #fee_cb().
     75  */
     76 struct FeeContext
     77 {
     78   /**
     79    * How many fee rows did the callback see?
     80    */
     81   unsigned int total;
     82 
     83   /**
     84    * Sum of the whole-unit parts of the wire fees seen, as a cheap way to
     85    * tell which rows were reported.
     86    */
     87   uint64_t value_sum;
     88 };
     89 
     90 
     91 /**
     92  * Callback for #TALER_EXCHANGEDB_iterate_wire_fees().
     93  *
     94  * @param cls a `struct FeeContext *`
     95  * @param fees the fees of this period
     96  * @param start_date start of the period
     97  * @param end_date end of the period
     98  * @param master_sig signature over the period
     99  */
    100 static void
    101 fee_cb (void *cls,
    102         const struct TALER_WireFeeSet *fees,
    103         struct GNUNET_TIME_Timestamp start_date,
    104         struct GNUNET_TIME_Timestamp end_date,
    105         const struct TALER_MasterSignatureP *master_sig)
    106 {
    107   struct FeeContext *ctx = cls;
    108 
    109   (void) start_date;
    110   (void) end_date;
    111   (void) master_sig;
    112   ctx->total++;
    113   ctx->value_sum += fees->wire.value;
    114 }
    115 
    116 
    117 /**
    118  * Insert a fee period.
    119  *
    120  * @param pg the database context
    121  * @param from start of the period
    122  * @param until end of the period
    123  * @param wire wire fee, e.g. "1"
    124  * @param closing closing fee, e.g. "2"
    125  * @param seed seed for the master signature
    126  * @return transaction status
    127  */
    128 static enum GNUNET_DB_QueryStatus
    129 add_fee (struct TALER_EXCHANGEDB_PostgresContext *pg,
    130          uint64_t from,
    131          uint64_t until,
    132          const char *wire,
    133          const char *closing,
    134          uint32_t seed)
    135 {
    136   struct TALER_WireFeeSet fees;
    137   struct TALER_MasterSignatureP master_sig;
    138 
    139   fees.wire = TDB_amount (wire);
    140   fees.closing = TDB_amount (closing);
    141   TDB_FILL (master_sig,
    142             seed);
    143   return TALER_EXCHANGEDB_insert_wire_fee (pg,
    144                                            METHOD,
    145                                            ts (from),
    146                                            ts (until),
    147                                            &fees,
    148                                            &master_sig);
    149 }
    150 
    151 
    152 /**
    153  * No fees means no lookups succeed and the iterator sees nothing.
    154  *
    155  * @param pg the database context
    156  * @return 0 on success
    157  */
    158 static int
    159 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
    160 {
    161   struct GNUNET_TIME_Timestamp start_date;
    162   struct GNUNET_TIME_Timestamp end_date;
    163   struct TALER_WireFeeSet fees;
    164   struct TALER_MasterSignatureP master_sig;
    165   struct FeeContext ctx = { 0 };
    166   uint64_t rowid;
    167 
    168   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    169           TALER_EXCHANGEDB_get_wire_fee (pg,
    170                                          METHOD,
    171                                          ts (T0),
    172                                          &rowid,
    173                                          &start_date,
    174                                          &end_date,
    175                                          &fees,
    176                                          &master_sig));
    177   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    178           TALER_EXCHANGEDB_get_wire_fee_by_time (pg,
    179                                                  METHOD,
    180                                                  ts (T0),
    181                                                  ts (T0 + HOUR),
    182                                                  &fees));
    183   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    184           TALER_EXCHANGEDB_iterate_wire_fees (pg,
    185                                               METHOD,
    186                                               &fee_cb,
    187                                               &ctx));
    188   FAILIF (0 != ctx.total);
    189   return 0;
    190 }
    191 
    192 
    193 /**
    194  * A single fee period is found inside its interval and nowhere else.
    195  *
    196  * @param pg the database context
    197  * @return 0 on success
    198  */
    199 static int
    200 check_single_period (struct TALER_EXCHANGEDB_PostgresContext *pg)
    201 {
    202   struct GNUNET_TIME_Timestamp start_date;
    203   struct GNUNET_TIME_Timestamp end_date;
    204   struct TALER_WireFeeSet fees;
    205   struct TALER_Amount expect_wire = TDB_amount ("1");
    206   struct TALER_Amount expect_closing = TDB_amount ("2");
    207   struct TALER_MasterSignatureP master_sig;
    208   struct TALER_MasterSignatureP expect_sig;
    209   uint64_t rowid = 0;
    210 
    211   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    212           add_fee (pg,
    213                    T0,
    214                    T0 + HOUR,
    215                    "1",
    216                    "2",
    217                    10));
    218   TDB_FILL (expect_sig,
    219             10);
    220   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    221           TALER_EXCHANGEDB_get_wire_fee (pg,
    222                                          METHOD,
    223                                          ts (T0),
    224                                          &rowid,
    225                                          &start_date,
    226                                          &end_date,
    227                                          &fees,
    228                                          &master_sig));
    229   FAILIF (0 == rowid);
    230   FAILIF (GNUNET_TIME_timestamp_cmp (start_date,
    231                                      !=,
    232                                      ts (T0)));
    233   FAILIF (GNUNET_TIME_timestamp_cmp (end_date,
    234                                      !=,
    235                                      ts (T0 + HOUR)));
    236   FAILIF (0 != TALER_amount_cmp (&fees.wire,
    237                                  &expect_wire));
    238   FAILIF (0 != TALER_amount_cmp (&fees.closing,
    239                                  &expect_closing));
    240   FAILIF (0 != GNUNET_memcmp (&master_sig,
    241                               &expect_sig));
    242 
    243   /* the interval is half-open: the end is not covered... */
    244   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    245           TALER_EXCHANGEDB_get_wire_fee (pg,
    246                                          METHOD,
    247                                          ts (T0 + HOUR),
    248                                          &rowid,
    249                                          &start_date,
    250                                          &end_date,
    251                                          &fees,
    252                                          &master_sig));
    253   /* ...and neither is anything before the start */
    254   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    255           TALER_EXCHANGEDB_get_wire_fee (pg,
    256                                          METHOD,
    257                                          ts (T0 - 1),
    258                                          &rowid,
    259                                          &start_date,
    260                                          &end_date,
    261                                          &fees,
    262                                          &master_sig));
    263   /* another wire method has no fees at all */
    264   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    265           TALER_EXCHANGEDB_get_wire_fee (pg,
    266                                          "iban",
    267                                          ts (T0),
    268                                          &rowid,
    269                                          &start_date,
    270                                          &end_date,
    271                                          &fees,
    272                                          &master_sig));
    273   return 0;
    274 }
    275 
    276 
    277 /**
    278  * Over a range with one fee, get_wire_fee_by_time() returns it; over a
    279  * range spanning two different fees it returns an invalid amount.
    280  *
    281  * @param pg the database context
    282  * @return 0 on success
    283  */
    284 static int
    285 check_by_time (struct TALER_EXCHANGEDB_PostgresContext *pg)
    286 {
    287   struct TALER_WireFeeSet fees;
    288   struct TALER_Amount expect_wire = TDB_amount ("1");
    289   struct FeeContext ctx;
    290 
    291   /* a second period with the *same* fees, and a third with different ones */
    292   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    293           add_fee (pg,
    294                    T0 + HOUR,
    295                    T0 + 2 * HOUR,
    296                    "1",
    297                    "2",
    298                    11));
    299   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    300           add_fee (pg,
    301                    T0 + 2 * HOUR,
    302                    T0 + 3 * HOUR,
    303                    "5",
    304                    "6",
    305                    12));
    306 
    307   /* one period: the fee itself */
    308   memset (&fees,
    309           0,
    310           sizeof (fees));
    311   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    312           TALER_EXCHANGEDB_get_wire_fee_by_time (pg,
    313                                                  METHOD,
    314                                                  ts (T0),
    315                                                  ts (T0 + HOUR),
    316                                                  &fees));
    317   FAILIF (0 != TALER_amount_cmp (&fees.wire,
    318                                  &expect_wire));
    319 
    320   /* two periods that agree: still the fee.  The status is the number of
    321      rows the range covered, so it is 2 rather than ONE_RESULT. */
    322   memset (&fees,
    323           0,
    324           sizeof (fees));
    325   FAILIF (2 !=
    326           TALER_EXCHANGEDB_get_wire_fee_by_time (pg,
    327                                                  METHOD,
    328                                                  ts (T0),
    329                                                  ts (T0 + 2 * HOUR),
    330                                                  &fees));
    331   FAILIF (0 != TALER_amount_cmp (&fees.wire,
    332                                  &expect_wire));
    333 
    334   /* three periods that disagree: success, but an invalid amount */
    335   memset (&fees,
    336           0,
    337           sizeof (fees));
    338   FAILIF (0 >
    339           TALER_EXCHANGEDB_get_wire_fee_by_time (pg,
    340                                                  METHOD,
    341                                                  ts (T0),
    342                                                  ts (T0 + 3 * HOUR),
    343                                                  &fees));
    344   FAILIF (GNUNET_OK ==
    345           TALER_amount_is_valid (&fees.wire));
    346   FAILIF (GNUNET_OK ==
    347           TALER_amount_is_valid (&fees.closing));
    348 
    349   /* a range outside every period finds nothing */
    350   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    351           TALER_EXCHANGEDB_get_wire_fee_by_time (pg,
    352                                                  METHOD,
    353                                                  ts (T0 + 10 * HOUR),
    354                                                  ts (T0 + 11 * HOUR),
    355                                                  &fees));
    356 
    357   memset (&ctx,
    358           0,
    359           sizeof (ctx));
    360   FAILIF (0 >=
    361           TALER_EXCHANGEDB_iterate_wire_fees (pg,
    362                                               METHOD,
    363                                               &fee_cb,
    364                                               &ctx));
    365   FAILIF (3 != ctx.total);
    366   FAILIF (1 + 1 + 5 != ctx.value_sum);
    367 
    368   memset (&ctx,
    369           0,
    370           sizeof (ctx));
    371   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    372           TALER_EXCHANGEDB_iterate_wire_fees (pg,
    373                                               "iban",
    374                                               &fee_cb,
    375                                               &ctx));
    376   FAILIF (0 != ctx.total);
    377   return 0;
    378 }
    379 
    380 
    381 /**
    382  * The checks to run, in order.
    383  */
    384 static const struct TDB_Test tests[] = {
    385   { "wire-fee-empty",
    386     &check_empty },
    387   { "wire-fee-single-period",
    388     &check_single_period },
    389   { "wire-fee-by-time",
    390     &check_by_time },
    391   { NULL, NULL }
    392 };
    393 
    394 
    395 int
    396 main (int argc,
    397       char *const *argv)
    398 {
    399   return TDB_main (argc,
    400                    argv,
    401                    "test-wire-fee",
    402                    "Tests for the exchangedb `wire_fee' table",
    403                    tests);
    404 }
    405 
    406 
    407 /* end of test_wire_fee.c */