exchange

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

test_purse_deletion.c (11893B)


      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_deletion.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `purse_deletion`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_do_purse_delete() and
     23  * #TALER_EXCHANGEDB_iterate_all_purse_deletions_above_serial_id().
     24  *
     25  * Deleting a purse is only possible while it is undecided: the two
     26  * negative answers of do_purse_delete() are "no such purse" and "already
     27  * decided", and the checks below produce both.
     28  */
     29 #include "test_common.h"
     30 #include "exchange-database/do_expire_purse.h"
     31 #include "exchange-database/do_purse_delete.h"
     32 #include "exchange-database/do_purse_deposit.h"
     33 #include "exchange-database/get_purse.h"
     34 #include "exchange-database/iterate_all_purse_deletions_above_serial_id.h"
     35 
     36 
     37 /**
     38  * Denomination the checks deposit into purses.
     39  */
     40 static struct TDB_Denom denom;
     41 
     42 
     43 /**
     44  * Build a timestamp from a number of seconds since the epoch.
     45  *
     46  * @param secs seconds since the epoch
     47  * @return the timestamp
     48  */
     49 static struct GNUNET_TIME_Timestamp
     50 ts (uint64_t secs)
     51 {
     52   struct GNUNET_TIME_Absolute abs = {
     53     .abs_value_us = secs * 1000LLU * 1000LLU
     54   };
     55 
     56   return GNUNET_TIME_absolute_to_timestamp (abs);
     57 }
     58 
     59 
     60 /**
     61  * Closure for #deletion_cb().
     62  */
     63 struct DeletionContext
     64 {
     65   /**
     66    * How many rows did the callback see?
     67    */
     68   unsigned int total;
     69 
     70   /**
     71    * Stop after this many rows; 0 for no limit.
     72    */
     73   unsigned int stop_after;
     74 
     75   /**
     76    * Purse we are looking for, NULL to match nothing.
     77    */
     78   const struct TALER_PurseContractPublicKeyP *purse_pub;
     79 
     80   /**
     81    * How many times did we see it?
     82    */
     83   unsigned int matched;
     84 
     85   /**
     86    * Signature reported for it.
     87    */
     88   struct TALER_PurseContractSignatureP purse_sig;
     89 };
     90 
     91 
     92 /**
     93  * Callback for
     94  * #TALER_EXCHANGEDB_iterate_all_purse_deletions_above_serial_id().
     95  *
     96  * @param cls a `struct DeletionContext *`
     97  * @param rowid row of the deletion
     98  * @param purse_pub the purse that was deleted
     99  * @param purse_sig signature affirming the deletion
    100  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
    101  */
    102 static enum GNUNET_GenericReturnValue
    103 deletion_cb (void *cls,
    104              uint64_t rowid,
    105              const struct TALER_PurseContractPublicKeyP *purse_pub,
    106              const struct TALER_PurseContractSignatureP *purse_sig)
    107 {
    108   struct DeletionContext *ctx = cls;
    109 
    110   (void) rowid;
    111   ctx->total++;
    112   if ( (NULL != ctx->purse_pub) &&
    113        (0 == GNUNET_memcmp (purse_pub,
    114                             ctx->purse_pub)) )
    115   {
    116     ctx->matched++;
    117     ctx->purse_sig = *purse_sig;
    118   }
    119   if ( (0 != ctx->stop_after) &&
    120        (ctx->total >= ctx->stop_after) )
    121     return GNUNET_SYSERR;
    122   return GNUNET_OK;
    123 }
    124 
    125 
    126 /**
    127  * A purse that does not exist cannot be deleted.
    128  *
    129  * @param pg the database context
    130  * @return 0 on success
    131  */
    132 static int
    133 check_unknown_purse (struct TALER_EXCHANGEDB_PostgresContext *pg)
    134 {
    135   struct TALER_PurseContractPublicKeyP purse_pub;
    136   struct TALER_PurseContractSignatureP purse_sig;
    137   struct DeletionContext ctx = { 0 };
    138   bool decided = true;
    139   bool found = true;
    140 
    141   TDB_denom (pg,
    142              10,
    143              "5",
    144              "0.1",
    145              &denom);
    146   TDB_FILL (purse_pub,
    147             1);
    148   TDB_FILL (purse_sig,
    149             1);
    150   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    151           TALER_EXCHANGEDB_do_purse_delete (pg,
    152                                             &purse_pub,
    153                                             &purse_sig,
    154                                             &decided,
    155                                             &found));
    156   FAILIF (found);
    157   FAILIF (0 != TDB_count (pg,
    158                           "FROM purse_deletion"));
    159   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    160           TALER_EXCHANGEDB_iterate_all_purse_deletions_above_serial_id (
    161             pg,
    162             0,
    163             &deletion_cb,
    164             &ctx));
    165   FAILIF (0 != ctx.total);
    166   return 0;
    167 }
    168 
    169 
    170 /**
    171  * Deleting an undecided purse records the deletion, refunds the coins in
    172  * it and shows up in the purse's status.
    173  *
    174  * @param pg the database context
    175  * @return 0 on success
    176  */
    177 static int
    178 check_delete (struct TALER_EXCHANGEDB_PostgresContext *pg)
    179 {
    180   struct TDB_Purse purse;
    181   struct TALER_CoinPublicInfo coin;
    182   struct TALER_PurseContractSignatureP purse_sig;
    183   struct TALER_CoinSpendSignatureP coin_sig;
    184   struct TALER_Amount amount = TDB_amount ("1");
    185   struct GNUNET_TIME_Timestamp purse_creation;
    186   struct GNUNET_TIME_Timestamp purse_expiration;
    187   struct GNUNET_TIME_Timestamp merge_timestamp;
    188   struct TALER_Amount target;
    189   struct TALER_Amount deposited;
    190   struct TALER_PrivateContractHashP h_contract_terms;
    191   struct DeletionContext ctx;
    192   bool balance_ok;
    193   bool too_late;
    194   bool conflict;
    195   bool decided = true;
    196   bool found = false;
    197   bool purse_deleted = false;
    198   bool purse_refunded = true;
    199   char *hex;
    200 
    201   TDB_purse (pg,
    202              10,
    203              "5",
    204              ts (1700000000),
    205              &purse);
    206   TDB_coin (pg,
    207             &denom,
    208             20,
    209             &coin,
    210             NULL);
    211   TDB_FILL (coin_sig,
    212             20);
    213   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    214             TALER_EXCHANGEDB_do_purse_deposit (pg,
    215                                                &purse.purse_pub,
    216                                                &coin.coin_pub,
    217                                                &amount,
    218                                                &coin_sig,
    219                                                &amount,
    220                                                &balance_ok,
    221                                                &too_late,
    222                                                &conflict),
    223             TDB_coin_free (&coin));
    224   FAILIF_C (! balance_ok,
    225             TDB_coin_free (&coin));
    226 
    227   TDB_FILL (purse_sig,
    228             10);
    229   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    230             TALER_EXCHANGEDB_do_purse_delete (pg,
    231                                               &purse.purse_pub,
    232                                               &purse_sig,
    233                                               &decided,
    234                                               &found),
    235             TDB_coin_free (&coin));
    236   FAILIF_C (! found,
    237             TDB_coin_free (&coin));
    238   FAILIF_C (decided,
    239             TDB_coin_free (&coin));
    240   FAILIF_C (1 != TDB_count (pg,
    241                             "FROM purse_deletion"),
    242             TDB_coin_free (&coin));
    243   /* the coin got its money back */
    244   hex = TDB_hex (&coin.coin_pub,
    245                  sizeof (coin.coin_pub));
    246   FAILIF_C (1 != TDB_count (pg,
    247                             "FROM known_coins"
    248                             " WHERE coin_pub=decode('%s','hex')"
    249                             "   AND remaining=ROW(5,0)::taler_amount",
    250                             hex),
    251             GNUNET_free (hex); TDB_coin_free (&coin));
    252   GNUNET_free (hex);
    253   TDB_coin_free (&coin);
    254 
    255   /* the purse reports itself as deleted */
    256   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    257           TALER_EXCHANGEDB_get_purse (pg,
    258                                       &purse.purse_pub,
    259                                       &purse_creation,
    260                                       &purse_expiration,
    261                                       &target,
    262                                       &deposited,
    263                                       &h_contract_terms,
    264                                       &merge_timestamp,
    265                                       &purse_deleted,
    266                                       &purse_refunded));
    267   FAILIF (! purse_deleted);
    268 
    269   /* the deletion is reported with the signature it was made with */
    270   memset (&ctx,
    271           0,
    272           sizeof (ctx));
    273   ctx.purse_pub = &purse.purse_pub;
    274   FAILIF (0 >=
    275           TALER_EXCHANGEDB_iterate_all_purse_deletions_above_serial_id (
    276             pg,
    277             0,
    278             &deletion_cb,
    279             &ctx));
    280   FAILIF (1 != ctx.matched);
    281   FAILIF (0 != GNUNET_memcmp (&ctx.purse_sig,
    282                               &purse_sig));
    283 
    284   /* deleting it again finds it already decided */
    285   decided = false;
    286   found = false;
    287   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    288           TALER_EXCHANGEDB_do_purse_delete (pg,
    289                                             &purse.purse_pub,
    290                                             &purse_sig,
    291                                             &decided,
    292                                             &found));
    293   FAILIF (! found);
    294   FAILIF (! decided);
    295   FAILIF (1 != TDB_count (pg,
    296                           "FROM purse_deletion"));
    297   return 0;
    298 }
    299 
    300 
    301 /**
    302  * A purse that expired first can no longer be deleted.
    303  *
    304  * @param pg the database context
    305  * @return 0 on success
    306  */
    307 static int
    308 check_already_decided (struct TALER_EXCHANGEDB_PostgresContext *pg)
    309 {
    310   struct TDB_Purse purse;
    311   struct TALER_PurseContractSignatureP purse_sig;
    312   struct GNUNET_TIME_Absolute expiration;
    313   bool decided = false;
    314   bool found = false;
    315 
    316   TDB_purse (pg,
    317              11,
    318              "5",
    319              ts (1600000000),
    320              &purse);
    321   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    322           TALER_EXCHANGEDB_do_expire_purse (pg,
    323                                             GNUNET_TIME_UNIT_ZERO_ABS,
    324                                             GNUNET_TIME_absolute_get (),
    325                                             &expiration));
    326   TDB_FILL (purse_sig,
    327             11);
    328   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    329           TALER_EXCHANGEDB_do_purse_delete (pg,
    330                                             &purse.purse_pub,
    331                                             &purse_sig,
    332                                             &decided,
    333                                             &found));
    334   FAILIF (! found);
    335   FAILIF (! decided);
    336   FAILIF (1 != TDB_count (pg,
    337                           "FROM purse_deletion"));
    338   return 0;
    339 }
    340 
    341 
    342 /**
    343  * The iterator's serial bound and abort return behave as documented.
    344  *
    345  * @param pg the database context
    346  * @return 0 on success
    347  */
    348 static int
    349 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    350 {
    351   struct DeletionContext ctx;
    352 
    353   memset (&ctx,
    354           0,
    355           sizeof (ctx));
    356   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    357           TALER_EXCHANGEDB_iterate_all_purse_deletions_above_serial_id (
    358             pg,
    359             1000,
    360             &deletion_cb,
    361             &ctx));
    362   FAILIF (0 != ctx.total);
    363   memset (&ctx,
    364           0,
    365           sizeof (ctx));
    366   ctx.stop_after = 1;
    367   FAILIF (1 !=
    368           TALER_EXCHANGEDB_iterate_all_purse_deletions_above_serial_id (
    369             pg,
    370             0,
    371             &deletion_cb,
    372             &ctx));
    373   FAILIF (1 != ctx.total);
    374   return 0;
    375 }
    376 
    377 
    378 /**
    379  * The checks to run, in order.
    380  */
    381 static const struct TDB_Test tests[] = {
    382   { "purse-deletion-unknown-purse",
    383     &check_unknown_purse },
    384   { "purse-deletion-delete",
    385     &check_delete },
    386   { "purse-deletion-already-decided",
    387     &check_already_decided },
    388   { "purse-deletion-iterate",
    389     &check_iterate },
    390   { NULL, NULL }
    391 };
    392 
    393 
    394 int
    395 main (int argc,
    396       char *const *argv)
    397 {
    398   int ret;
    399 
    400   ret = TDB_main (argc,
    401                   argv,
    402                   "test-purse-deletion",
    403                   "Tests for the exchangedb `purse_deletion' table",
    404                   tests);
    405   TDB_denom_free (&denom);
    406   return ret;
    407 }
    408 
    409 
    410 /* end of test_purse_deletion.c */