exchange

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

test_purse_decision.c (12590B)


      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_purse_decision.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `purse_decision`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_do_expire_purse(),
     23  * #TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id() and
     24  * #TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id().
     25  *
     26  * A purse is decided either because it was merged or because it expired.
     27  * do_expire_purse() handles the second case: it decides the oldest expired
     28  * purse in the window it is given, marks the decision as a refund and
     29  * gives the coins their money back -- which is what the checks look at.
     30  */
     31 #include "test_common.h"
     32 #include "exchange-database/do_expire_purse.h"
     33 #include "exchange-database/do_purse_deposit.h"
     34 #include "exchange-database/iterate_all_purse_decisions_above_serial_id.h"
     35 #include "exchange-database/iterate_purse_decisions_above_serial_id.h"
     36 
     37 
     38 /**
     39  * Denomination the checks deposit into purses.
     40  */
     41 static struct TDB_Denom denom;
     42 
     43 
     44 /**
     45  * Build a timestamp from a number of seconds since the epoch.
     46  *
     47  * @param secs seconds since the epoch
     48  * @return the timestamp
     49  */
     50 static struct GNUNET_TIME_Timestamp
     51 ts (uint64_t secs)
     52 {
     53   struct GNUNET_TIME_Absolute abs = {
     54     .abs_value_us = secs * 1000LLU * 1000LLU
     55   };
     56 
     57   return GNUNET_TIME_absolute_to_timestamp (abs);
     58 }
     59 
     60 
     61 /**
     62  * Closure for #decision_cb() and #all_decision_cb().
     63  */
     64 struct DecisionContext
     65 {
     66   /**
     67    * How many rows did the callback see?
     68    */
     69   unsigned int total;
     70 
     71   /**
     72    * Stop after this many rows; 0 for no limit.
     73    */
     74   unsigned int stop_after;
     75 
     76   /**
     77    * Purse we are looking for, NULL to match nothing.
     78    */
     79   const struct TALER_PurseContractPublicKeyP *purse_pub;
     80 
     81   /**
     82    * How many times did we see it?
     83    */
     84   unsigned int matched;
     85 
     86   /**
     87    * Was it reported as refunded?
     88    */
     89   bool refunded;
     90 
     91   /**
     92    * Was a reserve reported for it?
     93    */
     94   bool have_reserve;
     95 };
     96 
     97 
     98 /**
     99  * Callback for
    100  * #TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id().
    101  *
    102  * @param cls a `struct DecisionContext *`
    103  * @param rowid row of the decision
    104  * @param purse_pub the purse
    105  * @param reserve_pub reserve it went to, NULL if refunded
    106  * @param purse_value target value of the purse
    107  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
    108  */
    109 static enum GNUNET_GenericReturnValue
    110 decision_cb (void *cls,
    111              uint64_t rowid,
    112              const struct TALER_PurseContractPublicKeyP *purse_pub,
    113              const struct TALER_ReservePublicKeyP *reserve_pub,
    114              const struct TALER_Amount *purse_value)
    115 {
    116   struct DecisionContext *ctx = cls;
    117 
    118   (void) rowid;
    119   (void) purse_value;
    120   ctx->total++;
    121   if ( (NULL != ctx->purse_pub) &&
    122        (0 == GNUNET_memcmp (purse_pub,
    123                             ctx->purse_pub)) )
    124   {
    125     ctx->matched++;
    126     ctx->have_reserve = (NULL != reserve_pub);
    127   }
    128   if ( (0 != ctx->stop_after) &&
    129        (ctx->total >= ctx->stop_after) )
    130     return GNUNET_SYSERR;
    131   return GNUNET_OK;
    132 }
    133 
    134 
    135 /**
    136  * Callback for
    137  * #TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id().
    138  *
    139  * @param cls a `struct DecisionContext *`
    140  * @param rowid row of the decision
    141  * @param purse_pub the purse
    142  * @param refunded whether the decision was to refund
    143  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
    144  */
    145 static enum GNUNET_GenericReturnValue
    146 all_decision_cb (void *cls,
    147                  uint64_t rowid,
    148                  const struct TALER_PurseContractPublicKeyP *purse_pub,
    149                  bool refunded)
    150 {
    151   struct DecisionContext *ctx = cls;
    152 
    153   (void) rowid;
    154   ctx->total++;
    155   if ( (NULL != ctx->purse_pub) &&
    156        (0 == GNUNET_memcmp (purse_pub,
    157                             ctx->purse_pub)) )
    158   {
    159     ctx->matched++;
    160     ctx->refunded = refunded;
    161   }
    162   if ( (0 != ctx->stop_after) &&
    163        (ctx->total >= ctx->stop_after) )
    164     return GNUNET_SYSERR;
    165   return GNUNET_OK;
    166 }
    167 
    168 
    169 /**
    170  * With no purse to expire, nothing is decided and nothing is reported.
    171  *
    172  * @param pg the database context
    173  * @return 0 on success
    174  */
    175 static int
    176 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
    177 {
    178   struct DecisionContext ctx = { 0 };
    179   struct GNUNET_TIME_Absolute expiration;
    180 
    181   TDB_denom (pg,
    182              10,
    183              "5",
    184              "0.1",
    185              &denom);
    186   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    187           TALER_EXCHANGEDB_do_expire_purse (pg,
    188                                             GNUNET_TIME_UNIT_ZERO_ABS,
    189                                             GNUNET_TIME_absolute_get (),
    190                                             &expiration));
    191   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    192           TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id (
    193             pg,
    194             0,
    195             false,
    196             &decision_cb,
    197             &ctx));
    198   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    199           TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id (
    200             pg,
    201             0,
    202             &all_decision_cb,
    203             &ctx));
    204   FAILIF (0 != ctx.total);
    205   return 0;
    206 }
    207 
    208 
    209 /**
    210  * A purse that has not expired yet is left alone.
    211  *
    212  * @param pg the database context
    213  * @return 0 on success
    214  */
    215 static int
    216 check_not_expired (struct TALER_EXCHANGEDB_PostgresContext *pg)
    217 {
    218   struct TDB_Purse purse;
    219   struct GNUNET_TIME_Absolute expiration;
    220 
    221   /* expires in 2023 */
    222   TDB_purse (pg,
    223              10,
    224              "5",
    225              ts (1700000000),
    226              &purse);
    227   /* ...but we only look at purses that expired before 2020 */
    228   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    229           TALER_EXCHANGEDB_do_expire_purse (pg,
    230                                             GNUNET_TIME_UNIT_ZERO_ABS,
    231                                             ts (1600000000).abs_time,
    232                                             &expiration));
    233   FAILIF (0 != TDB_count (pg,
    234                           "FROM purse_decision"));
    235   return 0;
    236 }
    237 
    238 
    239 /**
    240  * Expiring a purse decides it as a refund and gives the coins their
    241  * money back.
    242  *
    243  * @param pg the database context
    244  * @return 0 on success
    245  */
    246 static int
    247 check_expire (struct TALER_EXCHANGEDB_PostgresContext *pg)
    248 {
    249   struct TDB_Purse purse;
    250   struct TALER_CoinPublicInfo coin;
    251   struct GNUNET_TIME_Absolute expiration;
    252   struct DecisionContext ctx;
    253   struct TALER_CoinSpendSignatureP coin_sig;
    254   struct TALER_Amount amount = TDB_amount ("1");
    255   bool balance_ok;
    256   bool too_late;
    257   bool conflict;
    258   char *hex;
    259 
    260   /* a purse that expired in 2020, with a coin paid into it */
    261   TDB_purse (pg,
    262              11,
    263              "5",
    264              ts (1600000000),
    265              &purse);
    266   TDB_coin (pg,
    267             &denom,
    268             20,
    269             &coin,
    270             NULL);
    271   TDB_FILL (coin_sig,
    272             20);
    273   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    274             TALER_EXCHANGEDB_do_purse_deposit (pg,
    275                                                &purse.purse_pub,
    276                                                &coin.coin_pub,
    277                                                &amount,
    278                                                &coin_sig,
    279                                                &amount,
    280                                                &balance_ok,
    281                                                &too_late,
    282                                                &conflict),
    283             TDB_coin_free (&coin));
    284   FAILIF_C (! balance_ok,
    285             TDB_coin_free (&coin));
    286   hex = TDB_hex (&coin.coin_pub,
    287                  sizeof (coin.coin_pub));
    288   FAILIF_C (1 != TDB_count (pg,
    289                             "FROM known_coins"
    290                             " WHERE coin_pub=decode('%s','hex')"
    291                             "   AND remaining=ROW(4,0)::taler_amount",
    292                             hex),
    293             GNUNET_free (hex); TDB_coin_free (&coin));
    294 
    295   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    296             TALER_EXCHANGEDB_do_expire_purse (pg,
    297                                               GNUNET_TIME_UNIT_ZERO_ABS,
    298                                               GNUNET_TIME_absolute_get (),
    299                                               &expiration),
    300             GNUNET_free (hex); TDB_coin_free (&coin));
    301   FAILIF_C (expiration.abs_value_us !=
    302             ts (1600000000).abs_time.abs_value_us,
    303             GNUNET_free (hex); TDB_coin_free (&coin));
    304   FAILIF_C (1 != TDB_count (pg,
    305                             "FROM purse_decision"),
    306             GNUNET_free (hex); TDB_coin_free (&coin));
    307   /* the coin got its money back */
    308   FAILIF_C (1 != TDB_count (pg,
    309                             "FROM known_coins"
    310                             " WHERE coin_pub=decode('%s','hex')"
    311                             "   AND remaining=ROW(5,0)::taler_amount",
    312                             hex),
    313             GNUNET_free (hex); TDB_coin_free (&coin));
    314   GNUNET_free (hex);
    315   TDB_coin_free (&coin);
    316 
    317   /* the decision is a refund, so the by-refund iterator sees it... */
    318   memset (&ctx,
    319           0,
    320           sizeof (ctx));
    321   ctx.purse_pub = &purse.purse_pub;
    322   FAILIF (0 >=
    323           TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id (
    324             pg,
    325             0,
    326             true,
    327             &decision_cb,
    328             &ctx));
    329   FAILIF (1 != ctx.matched);
    330   /* ...a refunded purse has no reserve to credit */
    331   FAILIF (ctx.have_reserve);
    332 
    333   /* ...and the non-refunded view does not */
    334   memset (&ctx,
    335           0,
    336           sizeof (ctx));
    337   ctx.purse_pub = &purse.purse_pub;
    338   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    339           TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id (
    340             pg,
    341             0,
    342             false,
    343             &decision_cb,
    344             &ctx));
    345   FAILIF (0 != ctx.matched);
    346 
    347   /* the all-decisions view reports it with its refund flag */
    348   memset (&ctx,
    349           0,
    350           sizeof (ctx));
    351   ctx.purse_pub = &purse.purse_pub;
    352   FAILIF (0 >=
    353           TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id (
    354             pg,
    355             0,
    356             &all_decision_cb,
    357             &ctx));
    358   FAILIF (1 != ctx.matched);
    359   FAILIF (! ctx.refunded);
    360 
    361   /* a second sweep finds nothing left to expire */
    362   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    363           TALER_EXCHANGEDB_do_expire_purse (pg,
    364                                             GNUNET_TIME_UNIT_ZERO_ABS,
    365                                             ts (1650000000).abs_time,
    366                                             &expiration));
    367   FAILIF (1 != TDB_count (pg,
    368                           "FROM purse_decision"));
    369   return 0;
    370 }
    371 
    372 
    373 /**
    374  * The iterators' serial bounds and abort returns behave as documented.
    375  *
    376  * @param pg the database context
    377  * @return 0 on success
    378  */
    379 static int
    380 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    381 {
    382   struct DecisionContext ctx;
    383 
    384   memset (&ctx,
    385           0,
    386           sizeof (ctx));
    387   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    388           TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id (
    389             pg,
    390             1000,
    391             &all_decision_cb,
    392             &ctx));
    393   FAILIF (0 != ctx.total);
    394   memset (&ctx,
    395           0,
    396           sizeof (ctx));
    397   ctx.stop_after = 1;
    398   FAILIF (1 !=
    399           TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id (
    400             pg,
    401             0,
    402             &all_decision_cb,
    403             &ctx));
    404   FAILIF (1 != ctx.total);
    405   return 0;
    406 }
    407 
    408 
    409 /**
    410  * The checks to run, in order.
    411  */
    412 static const struct TDB_Test tests[] = {
    413   { "purse-decision-empty",
    414     &check_empty },
    415   { "purse-decision-not-expired",
    416     &check_not_expired },
    417   { "purse-decision-expire",
    418     &check_expire },
    419   { "purse-decision-iterate",
    420     &check_iterate },
    421   { NULL, NULL }
    422 };
    423 
    424 
    425 int
    426 main (int argc,
    427       char *const *argv)
    428 {
    429   int ret;
    430 
    431   ret = TDB_main (argc,
    432                   argv,
    433                   "test-purse-decision",
    434                   "Tests for the exchangedb `purse_decision' table",
    435                   tests);
    436   TDB_denom_free (&denom);
    437   return ret;
    438 }
    439 
    440 
    441 /* end of test_purse_decision.c */