exchange

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

test_global_fee.c (14068B)


      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_global_fee.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `global_fee`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_insert_global_fee(),
     23  * #TALER_EXCHANGEDB_get_global_fee(),
     24  * #TALER_EXCHANGEDB_get_global_fee_by_time() and
     25  * #TALER_EXCHANGEDB_iterate_global_fees().
     26  *
     27  * `global_fee` has no foreign keys and, unlike `wire_fee`, no wire method
     28  * to key on -- the validity interval is all there is.  As with the wire
     29  * fees, the by-time lookup returns an invalid amount when the range covers
     30  * two different fee sets.
     31  */
     32 #include "test_common.h"
     33 #include "exchange-database/insert_global_fee.h"
     34 #include "exchange-database/get_global_fee.h"
     35 #include "exchange-database/get_global_fee_by_time.h"
     36 #include "exchange-database/iterate_global_fees.h"
     37 
     38 
     39 /**
     40  * One hour, in seconds.
     41  */
     42 #define HOUR 3600
     43 
     44 
     45 /**
     46  * Start of the first fee period, in seconds since the epoch.  It has to be
     47  * recent: #TALER_EXCHANGEDB_iterate_global_fees() only reports periods
     48  * that started within the last year.
     49  *
     50  * @return an hour ago, rounded down to a whole second
     51  */
     52 static uint64_t
     53 t0 (void)
     54 {
     55   static uint64_t cached;
     56 
     57   if (0 == cached)
     58     cached = GNUNET_TIME_absolute_get ().abs_value_us
     59              / (1000LLU * 1000LLU) - HOUR;
     60   return cached;
     61 }
     62 
     63 
     64 /**
     65  * Convenience alias for the start of the first fee period.
     66  */
     67 #define T0 (t0 ())
     68 
     69 
     70 /**
     71  * Build a timestamp from a number of seconds since the epoch.
     72  *
     73  * @param secs seconds since the epoch
     74  * @return the timestamp
     75  */
     76 static struct GNUNET_TIME_Timestamp
     77 ts (uint64_t secs)
     78 {
     79   struct GNUNET_TIME_Absolute abs = {
     80     .abs_value_us = secs * 1000LLU * 1000LLU
     81   };
     82 
     83   return GNUNET_TIME_absolute_to_timestamp (abs);
     84 }
     85 
     86 
     87 /**
     88  * Closure for #global_fee_cb().
     89  */
     90 struct FeeContext
     91 {
     92   /**
     93    * How many periods did the callback see?
     94    */
     95   unsigned int total;
     96 
     97   /**
     98    * Sum of the whole-unit parts of the history fees seen.
     99    */
    100   uint64_t value_sum;
    101 
    102   /**
    103    * Purse account limit of the last period seen.
    104    */
    105   uint32_t purse_account_limit;
    106 };
    107 
    108 
    109 /**
    110  * Callback for #TALER_EXCHANGEDB_iterate_global_fees().
    111  *
    112  * @param cls a `struct FeeContext *`
    113  * @param fees the fees of this period
    114  * @param purse_timeout when purses time out
    115  * @param history_expiration how long histories are kept
    116  * @param purse_account_limit free purses per account
    117  * @param start_date start of the period
    118  * @param end_date end of the period
    119  * @param master_sig signature over the period
    120  */
    121 static void
    122 global_fee_cb (void *cls,
    123                const struct TALER_GlobalFeeSet *fees,
    124                struct GNUNET_TIME_Relative purse_timeout,
    125                struct GNUNET_TIME_Relative history_expiration,
    126                uint32_t purse_account_limit,
    127                struct GNUNET_TIME_Timestamp start_date,
    128                struct GNUNET_TIME_Timestamp end_date,
    129                const struct TALER_MasterSignatureP *master_sig)
    130 {
    131   struct FeeContext *ctx = cls;
    132 
    133   (void) purse_timeout;
    134   (void) history_expiration;
    135   (void) start_date;
    136   (void) end_date;
    137   (void) master_sig;
    138   ctx->total++;
    139   ctx->value_sum += fees->history.value;
    140   ctx->purse_account_limit = purse_account_limit;
    141 }
    142 
    143 
    144 /**
    145  * Insert a fee period.
    146  *
    147  * @param pg the database context
    148  * @param from start of the period
    149  * @param until end of the period
    150  * @param history history fee, e.g. "1"
    151  * @param limit free purses per account
    152  * @param seed seed for the master signature
    153  * @return transaction status
    154  */
    155 static enum GNUNET_DB_QueryStatus
    156 add_fee (struct TALER_EXCHANGEDB_PostgresContext *pg,
    157          uint64_t from,
    158          uint64_t until,
    159          const char *history,
    160          uint32_t limit,
    161          uint32_t seed)
    162 {
    163   struct TALER_GlobalFeeSet fees;
    164   struct TALER_MasterSignatureP master_sig;
    165 
    166   fees.history = TDB_amount (history);
    167   fees.account = TDB_amount ("0.5");
    168   fees.purse = TDB_amount ("0.25");
    169   TDB_FILL (master_sig,
    170             seed);
    171   return TALER_EXCHANGEDB_insert_global_fee (pg,
    172                                              ts (from),
    173                                              ts (until),
    174                                              &fees,
    175                                              GNUNET_TIME_UNIT_HOURS,
    176                                              GNUNET_TIME_UNIT_DAYS,
    177                                              limit,
    178                                              &master_sig);
    179 }
    180 
    181 
    182 /**
    183  * No fees means no lookups succeed and the iterator sees nothing.
    184  *
    185  * @param pg the database context
    186  * @return 0 on success
    187  */
    188 static int
    189 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
    190 {
    191   struct GNUNET_TIME_Timestamp start_date;
    192   struct GNUNET_TIME_Timestamp end_date;
    193   struct GNUNET_TIME_Relative purse_timeout;
    194   struct GNUNET_TIME_Relative history_expiration;
    195   struct TALER_GlobalFeeSet fees;
    196   struct TALER_MasterSignatureP master_sig;
    197   struct FeeContext ctx = { 0 };
    198   uint32_t purse_account_limit;
    199 
    200   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    201           TALER_EXCHANGEDB_get_global_fee (pg,
    202                                            ts (T0),
    203                                            &start_date,
    204                                            &end_date,
    205                                            &fees,
    206                                            &purse_timeout,
    207                                            &history_expiration,
    208                                            &purse_account_limit,
    209                                            &master_sig));
    210   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    211           TALER_EXCHANGEDB_get_global_fee_by_time (pg,
    212                                                    ts (T0),
    213                                                    ts (T0 + HOUR),
    214                                                    &fees,
    215                                                    &purse_timeout,
    216                                                    &history_expiration,
    217                                                    &purse_account_limit));
    218   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    219           TALER_EXCHANGEDB_iterate_global_fees (pg,
    220                                                 &global_fee_cb,
    221                                                 &ctx));
    222   FAILIF (0 != ctx.total);
    223   return 0;
    224 }
    225 
    226 
    227 /**
    228  * A single fee period is found inside its interval and nowhere else.
    229  *
    230  * @param pg the database context
    231  * @return 0 on success
    232  */
    233 static int
    234 check_single_period (struct TALER_EXCHANGEDB_PostgresContext *pg)
    235 {
    236   struct GNUNET_TIME_Timestamp start_date;
    237   struct GNUNET_TIME_Timestamp end_date;
    238   struct GNUNET_TIME_Relative purse_timeout;
    239   struct GNUNET_TIME_Relative history_expiration;
    240   struct TALER_GlobalFeeSet fees;
    241   struct TALER_Amount expect_history = TDB_amount ("1");
    242   struct TALER_MasterSignatureP master_sig;
    243   struct TALER_MasterSignatureP expect_sig;
    244   uint32_t purse_account_limit = 0;
    245 
    246   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    247           add_fee (pg,
    248                    T0,
    249                    T0 + HOUR,
    250                    "1",
    251                    4,
    252                    20));
    253   TDB_FILL (expect_sig,
    254             20);
    255   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    256           TALER_EXCHANGEDB_get_global_fee (pg,
    257                                            ts (T0),
    258                                            &start_date,
    259                                            &end_date,
    260                                            &fees,
    261                                            &purse_timeout,
    262                                            &history_expiration,
    263                                            &purse_account_limit,
    264                                            &master_sig));
    265   FAILIF (GNUNET_TIME_timestamp_cmp (start_date,
    266                                      !=,
    267                                      ts (T0)));
    268   FAILIF (GNUNET_TIME_timestamp_cmp (end_date,
    269                                      !=,
    270                                      ts (T0 + HOUR)));
    271   FAILIF (0 != TALER_amount_cmp (&fees.history,
    272                                  &expect_history));
    273   FAILIF (4 != purse_account_limit);
    274   FAILIF (GNUNET_TIME_relative_cmp (purse_timeout,
    275                                     !=,
    276                                     GNUNET_TIME_UNIT_HOURS));
    277   FAILIF (GNUNET_TIME_relative_cmp (history_expiration,
    278                                     !=,
    279                                     GNUNET_TIME_UNIT_DAYS));
    280   FAILIF (0 != GNUNET_memcmp (&master_sig,
    281                               &expect_sig));
    282 
    283   /* the interval is half-open */
    284   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    285           TALER_EXCHANGEDB_get_global_fee (pg,
    286                                            ts (T0 + HOUR),
    287                                            &start_date,
    288                                            &end_date,
    289                                            &fees,
    290                                            &purse_timeout,
    291                                            &history_expiration,
    292                                            &purse_account_limit,
    293                                            &master_sig));
    294   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    295           TALER_EXCHANGEDB_get_global_fee (pg,
    296                                            ts (T0 - 1),
    297                                            &start_date,
    298                                            &end_date,
    299                                            &fees,
    300                                            &purse_timeout,
    301                                            &history_expiration,
    302                                            &purse_account_limit,
    303                                            &master_sig));
    304   return 0;
    305 }
    306 
    307 
    308 /**
    309  * Over a range with one fee set, get_global_fee_by_time() returns it; over
    310  * a range spanning two different ones it returns an invalid amount.
    311  *
    312  * @param pg the database context
    313  * @return 0 on success
    314  */
    315 static int
    316 check_by_time (struct TALER_EXCHANGEDB_PostgresContext *pg)
    317 {
    318   struct GNUNET_TIME_Relative purse_timeout;
    319   struct GNUNET_TIME_Relative history_expiration;
    320   struct TALER_GlobalFeeSet fees;
    321   struct TALER_Amount expect_history = TDB_amount ("1");
    322   struct FeeContext ctx;
    323   uint32_t purse_account_limit;
    324 
    325   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    326           add_fee (pg,
    327                    T0 + HOUR,
    328                    T0 + 2 * HOUR,
    329                    "1",
    330                    4,
    331                    21));
    332   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    333           add_fee (pg,
    334                    T0 + 2 * HOUR,
    335                    T0 + 3 * HOUR,
    336                    "9",
    337                    4,
    338                    22));
    339 
    340   /* Two periods that agree: still the fee.  The status is the number of
    341      rows the range covered, so it is 2 rather than ONE_RESULT. */
    342   memset (&fees,
    343           0,
    344           sizeof (fees));
    345   FAILIF (2 !=
    346           TALER_EXCHANGEDB_get_global_fee_by_time (pg,
    347                                                    ts (T0),
    348                                                    ts (T0 + 2 * HOUR),
    349                                                    &fees,
    350                                                    &purse_timeout,
    351                                                    &history_expiration,
    352                                                    &purse_account_limit));
    353   FAILIF (0 != TALER_amount_cmp (&fees.history,
    354                                  &expect_history));
    355   FAILIF (4 != purse_account_limit);
    356 
    357   memset (&fees,
    358           0,
    359           sizeof (fees));
    360   FAILIF (0 >
    361           TALER_EXCHANGEDB_get_global_fee_by_time (pg,
    362                                                    ts (T0),
    363                                                    ts (T0 + 3 * HOUR),
    364                                                    &fees,
    365                                                    &purse_timeout,
    366                                                    &history_expiration,
    367                                                    &purse_account_limit));
    368   FAILIF (GNUNET_OK ==
    369           TALER_amount_is_valid (&fees.history));
    370 
    371   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    372           TALER_EXCHANGEDB_get_global_fee_by_time (pg,
    373                                                    ts (T0 + 10 * HOUR),
    374                                                    ts (T0 + 11 * HOUR),
    375                                                    &fees,
    376                                                    &purse_timeout,
    377                                                    &history_expiration,
    378                                                    &purse_account_limit));
    379 
    380   memset (&ctx,
    381           0,
    382           sizeof (ctx));
    383   FAILIF (0 >=
    384           TALER_EXCHANGEDB_iterate_global_fees (pg,
    385                                                 &global_fee_cb,
    386                                                 &ctx));
    387   FAILIF (3 != ctx.total);
    388   FAILIF (1 + 1 + 9 != ctx.value_sum);
    389   return 0;
    390 }
    391 
    392 
    393 /**
    394  * The checks to run, in order.
    395  */
    396 static const struct TDB_Test tests[] = {
    397   { "global-fee-empty",
    398     &check_empty },
    399   { "global-fee-single-period",
    400     &check_single_period },
    401   { "global-fee-by-time",
    402     &check_by_time },
    403   { NULL, NULL }
    404 };
    405 
    406 
    407 int
    408 main (int argc,
    409       char *const *argv)
    410 {
    411   return TDB_main (argc,
    412                    argv,
    413                    "test-global-fee",
    414                    "Tests for the exchangedb `global_fee' table",
    415                    tests);
    416 }
    417 
    418 
    419 /* end of test_global_fee.c */