exchange

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

test_purse_requests.c (15590B)


      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_requests.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `purse_requests`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_insert_purse_request(),
     23  * #TALER_EXCHANGEDB_get_purse_request(),
     24  * #TALER_EXCHANGEDB_get_purse_by_merge_pub(),
     25  * #TALER_EXCHANGEDB_get_purse() and
     26  * #TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id().
     27  *
     28  * `purse_requests` has no foreign keys -- a purse is created before
     29  * anything is paid into it.  The interesting case of the insert is the
     30  * conflict: re-creating a purse with the same key but different meta data
     31  * is reported through @e in_conflict rather than as an error.
     32  */
     33 #include "test_common.h"
     34 #include "exchange-database/get_purse.h"
     35 #include "exchange-database/get_purse_by_merge_pub.h"
     36 #include "exchange-database/get_purse_request.h"
     37 #include "exchange-database/insert_purse_request.h"
     38 #include "exchange-database/iterate_purse_requests_above_serial_id.h"
     39 
     40 
     41 /**
     42  * Build a timestamp from a number of seconds since the epoch.
     43  *
     44  * @param secs seconds since the epoch
     45  * @return the timestamp
     46  */
     47 static struct GNUNET_TIME_Timestamp
     48 ts (uint64_t secs)
     49 {
     50   struct GNUNET_TIME_Absolute abs = {
     51     .abs_value_us = secs * 1000LLU * 1000LLU
     52   };
     53 
     54   return GNUNET_TIME_absolute_to_timestamp (abs);
     55 }
     56 
     57 
     58 /**
     59  * Closure for #purse_cb().
     60  */
     61 struct PurseContext
     62 {
     63   /**
     64    * How many rows did the callback see?
     65    */
     66   unsigned int total;
     67 
     68   /**
     69    * Stop after this many rows; 0 for no limit.
     70    */
     71   unsigned int stop_after;
     72 
     73   /**
     74    * Purse we are looking for, NULL to match nothing.
     75    */
     76   const struct TALER_PurseContractPublicKeyP *purse_pub;
     77 
     78   /**
     79    * How many times did we see it?
     80    */
     81   unsigned int matched;
     82 
     83   /**
     84    * Target amount reported for it.
     85    */
     86   struct TALER_Amount amount;
     87 
     88   /**
     89    * Age limit reported for it.
     90    */
     91   uint32_t age_limit;
     92 };
     93 
     94 
     95 /**
     96  * Callback for #TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id().
     97  *
     98  * @param cls a `struct PurseContext *`
     99  * @param rowid row of the purse
    100  * @param purse_pub the purse
    101  * @param merge_pub its merge capability key
    102  * @param purse_creation when it was created
    103  * @param purse_expiration when it expires
    104  * @param h_contract_terms contract it is for
    105  * @param age_limit age limit on deposits into it
    106  * @param target_amount how much it is meant to hold
    107  * @param purse_sig signature over its meta data
    108  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
    109  */
    110 static enum GNUNET_GenericReturnValue
    111 purse_cb (void *cls,
    112           uint64_t rowid,
    113           const struct TALER_PurseContractPublicKeyP *purse_pub,
    114           const struct TALER_PurseMergePublicKeyP *merge_pub,
    115           struct GNUNET_TIME_Timestamp purse_creation,
    116           struct GNUNET_TIME_Timestamp purse_expiration,
    117           const struct TALER_PrivateContractHashP *h_contract_terms,
    118           uint32_t age_limit,
    119           const struct TALER_Amount *target_amount,
    120           const struct TALER_PurseContractSignatureP *purse_sig)
    121 {
    122   struct PurseContext *ctx = cls;
    123 
    124   (void) rowid;
    125   (void) merge_pub;
    126   (void) purse_creation;
    127   (void) purse_expiration;
    128   (void) h_contract_terms;
    129   (void) purse_sig;
    130   ctx->total++;
    131   if ( (NULL != ctx->purse_pub) &&
    132        (0 == GNUNET_memcmp (purse_pub,
    133                             ctx->purse_pub)) )
    134   {
    135     ctx->matched++;
    136     ctx->amount = *target_amount;
    137     ctx->age_limit = age_limit;
    138   }
    139   if ( (0 != ctx->stop_after) &&
    140        (ctx->total >= ctx->stop_after) )
    141     return GNUNET_SYSERR;
    142   return GNUNET_OK;
    143 }
    144 
    145 
    146 /**
    147  * Nothing is known while the table is empty.
    148  *
    149  * @param pg the database context
    150  * @return 0 on success
    151  */
    152 static int
    153 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
    154 {
    155   struct TALER_PurseContractPublicKeyP purse_pub;
    156   struct TALER_PurseMergePublicKeyP merge_pub;
    157   struct GNUNET_TIME_Timestamp purse_expiration;
    158   struct GNUNET_TIME_Timestamp purse_creation;
    159   struct GNUNET_TIME_Timestamp merge_timestamp;
    160   struct TALER_PrivateContractHashP h_contract_terms;
    161   struct TALER_Amount target_amount;
    162   struct TALER_Amount balance;
    163   struct TALER_PurseContractSignatureP purse_sig;
    164   struct PurseContext ctx = { 0 };
    165   uint32_t age_limit;
    166   bool purse_deleted;
    167   bool purse_refunded;
    168 
    169   TDB_FILL (purse_pub,
    170             1);
    171   TDB_FILL (merge_pub,
    172             1);
    173   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    174           TALER_EXCHANGEDB_get_purse_request (pg,
    175                                               &purse_pub,
    176                                               &merge_pub,
    177                                               &purse_expiration,
    178                                               &h_contract_terms,
    179                                               &age_limit,
    180                                               &target_amount,
    181                                               &balance,
    182                                               &purse_sig));
    183   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    184           TALER_EXCHANGEDB_get_purse_by_merge_pub (pg,
    185                                                    &merge_pub,
    186                                                    &purse_pub,
    187                                                    &purse_expiration,
    188                                                    &h_contract_terms,
    189                                                    &age_limit,
    190                                                    &target_amount,
    191                                                    &balance,
    192                                                    &purse_sig));
    193   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    194           TALER_EXCHANGEDB_get_purse (pg,
    195                                       &purse_pub,
    196                                       &purse_creation,
    197                                       &purse_expiration,
    198                                       &target_amount,
    199                                       &balance,
    200                                       &h_contract_terms,
    201                                       &merge_timestamp,
    202                                       &purse_deleted,
    203                                       &purse_refunded));
    204   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    205           TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id (
    206             pg,
    207             0,
    208             &purse_cb,
    209             &ctx));
    210   FAILIF (0 != ctx.total);
    211   return 0;
    212 }
    213 
    214 
    215 /**
    216  * A created purse is found by its own key, by its merge key and by the
    217  * combined status lookup.
    218  *
    219  * @param pg the database context
    220  * @return 0 on success
    221  */
    222 static int
    223 check_create_and_lookup (struct TALER_EXCHANGEDB_PostgresContext *pg)
    224 {
    225   struct TDB_Purse purse;
    226   struct TALER_PurseContractPublicKeyP got_pub;
    227   struct TALER_PurseMergePublicKeyP got_merge;
    228   struct GNUNET_TIME_Timestamp purse_expiration;
    229   struct GNUNET_TIME_Timestamp purse_creation;
    230   struct GNUNET_TIME_Timestamp merge_timestamp;
    231   struct TALER_PrivateContractHashP h_contract_terms;
    232   struct TALER_Amount target_amount;
    233   struct TALER_Amount balance;
    234   struct TALER_Amount zero = TDB_amount ("0");
    235   struct TALER_PurseContractSignatureP purse_sig;
    236   uint32_t age_limit;
    237   bool purse_deleted = true;
    238   bool purse_refunded = true;
    239 
    240   TDB_purse (pg,
    241              10,
    242              "5",
    243              ts (1700000000),
    244              &purse);
    245   FAILIF (1 != TDB_count (pg,
    246                           "FROM purse_requests"));
    247 
    248   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    249           TALER_EXCHANGEDB_get_purse_request (pg,
    250                                               &purse.purse_pub,
    251                                               &got_merge,
    252                                               &purse_expiration,
    253                                               &h_contract_terms,
    254                                               &age_limit,
    255                                               &target_amount,
    256                                               &balance,
    257                                               &purse_sig));
    258   FAILIF (0 != GNUNET_memcmp (&got_merge,
    259                               &purse.merge_pub));
    260   FAILIF (0 != GNUNET_memcmp (&h_contract_terms,
    261                               &purse.h_contract_terms));
    262   FAILIF (0 != GNUNET_memcmp (&purse_sig,
    263                               &purse.purse_sig));
    264   FAILIF (GNUNET_TIME_timestamp_cmp (purse_expiration,
    265                                      !=,
    266                                      purse.purse_expiration));
    267   FAILIF (0 != TALER_amount_cmp (&target_amount,
    268                                  &purse.amount));
    269   /* nothing has been paid in yet */
    270   FAILIF (0 != TALER_amount_cmp (&balance,
    271                                  &zero));
    272   FAILIF (0 != age_limit);
    273 
    274   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    275           TALER_EXCHANGEDB_get_purse_by_merge_pub (pg,
    276                                                    &purse.merge_pub,
    277                                                    &got_pub,
    278                                                    &purse_expiration,
    279                                                    &h_contract_terms,
    280                                                    &age_limit,
    281                                                    &target_amount,
    282                                                    &balance,
    283                                                    &purse_sig));
    284   FAILIF (0 != GNUNET_memcmp (&got_pub,
    285                               &purse.purse_pub));
    286 
    287   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    288           TALER_EXCHANGEDB_get_purse (pg,
    289                                       &purse.purse_pub,
    290                                       &purse_creation,
    291                                       &purse_expiration,
    292                                       &target_amount,
    293                                       &balance,
    294                                       &h_contract_terms,
    295                                       &merge_timestamp,
    296                                       &purse_deleted,
    297                                       &purse_refunded));
    298   FAILIF (purse_deleted);
    299   FAILIF (purse_refunded);
    300   FAILIF (0 != TALER_amount_cmp (&balance,
    301                                  &zero));
    302   /* the purse has not been merged */
    303   FAILIF (! GNUNET_TIME_absolute_is_never (merge_timestamp.abs_time));
    304 
    305   /* a merge key nobody used finds nothing */
    306   {
    307     struct TALER_PurseMergePublicKeyP other;
    308 
    309     TDB_FILL (other,
    310               99);
    311     FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    312             TALER_EXCHANGEDB_get_purse_by_merge_pub (pg,
    313                                                      &other,
    314                                                      &got_pub,
    315                                                      &purse_expiration,
    316                                                      &h_contract_terms,
    317                                                      &age_limit,
    318                                                      &target_amount,
    319                                                      &balance,
    320                                                      &purse_sig));
    321   }
    322   return 0;
    323 }
    324 
    325 
    326 /**
    327  * Re-creating a purse with the same key is idempotent if the meta data
    328  * matches and a conflict if it does not.
    329  *
    330  * @param pg the database context
    331  * @return 0 on success
    332  */
    333 static int
    334 check_conflict (struct TALER_EXCHANGEDB_PostgresContext *pg)
    335 {
    336   struct TDB_Purse purse;
    337   struct TALER_Amount purse_fee = TDB_amount ("0");
    338   struct TALER_Amount other_amount = TDB_amount ("9");
    339   bool in_conflict = false;
    340 
    341   /* TDB_purse() with the same seed replays the identical request */
    342   TDB_purse (pg,
    343              10,
    344              "5",
    345              ts (1700000000),
    346              &purse);
    347   FAILIF (1 != TDB_count (pg,
    348                           "FROM purse_requests"));
    349   /* An identical replay is reported as "nothing to do".  Note that the
    350      function leaves @e in_conflict untouched on that path, so only the
    351      status says so. */
    352   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    353           TALER_EXCHANGEDB_insert_purse_request (
    354             pg,
    355             &purse.purse_pub,
    356             &purse.merge_pub,
    357             purse.purse_expiration,
    358             &purse.h_contract_terms,
    359             0,
    360             TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA,
    361             &purse_fee,
    362             &purse.amount,
    363             &purse.purse_sig,
    364             &in_conflict));
    365   FAILIF (in_conflict);
    366 
    367   /* the same purse key with a different target amount is a conflict */
    368   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    369           TALER_EXCHANGEDB_insert_purse_request (
    370             pg,
    371             &purse.purse_pub,
    372             &purse.merge_pub,
    373             purse.purse_expiration,
    374             &purse.h_contract_terms,
    375             0,
    376             TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA,
    377             &purse_fee,
    378             &other_amount,
    379             &purse.purse_sig,
    380             &in_conflict));
    381   FAILIF (! in_conflict);
    382   FAILIF (1 != TDB_count (pg,
    383                           "FROM purse_requests"));
    384   return 0;
    385 }
    386 
    387 
    388 /**
    389  * The iterator walks the table by serial and honours an aborting
    390  * callback.
    391  *
    392  * @param pg the database context
    393  * @return 0 on success
    394  */
    395 static int
    396 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    397 {
    398   struct TDB_Purse p2;
    399   struct PurseContext ctx;
    400 
    401   TDB_purse (pg,
    402              11,
    403              "3",
    404              ts (1700003600),
    405              &p2);
    406   FAILIF (2 != TDB_count (pg,
    407                           "FROM purse_requests"));
    408   memset (&ctx,
    409           0,
    410           sizeof (ctx));
    411   ctx.purse_pub = &p2.purse_pub;
    412   FAILIF (2 !=
    413           TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id (
    414             pg,
    415             0,
    416             &purse_cb,
    417             &ctx));
    418   FAILIF (1 != ctx.matched);
    419   FAILIF (0 != TALER_amount_cmp (&ctx.amount,
    420                                  &p2.amount));
    421   FAILIF (0 != ctx.age_limit);
    422 
    423   memset (&ctx,
    424           0,
    425           sizeof (ctx));
    426   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    427           TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id (
    428             pg,
    429             1000,
    430             &purse_cb,
    431             &ctx));
    432   FAILIF (0 != ctx.total);
    433 
    434   memset (&ctx,
    435           0,
    436           sizeof (ctx));
    437   ctx.stop_after = 1;
    438   FAILIF (2 !=
    439           TALER_EXCHANGEDB_iterate_purse_requests_above_serial_id (
    440             pg,
    441             0,
    442             &purse_cb,
    443             &ctx));
    444   FAILIF (1 != ctx.total);
    445   return 0;
    446 }
    447 
    448 
    449 /**
    450  * The checks to run, in order.
    451  */
    452 static const struct TDB_Test tests[] = {
    453   { "purse-requests-empty",
    454     &check_empty },
    455   { "purse-requests-create-and-lookup",
    456     &check_create_and_lookup },
    457   { "purse-requests-conflict",
    458     &check_conflict },
    459   { "purse-requests-iterate",
    460     &check_iterate },
    461   { NULL, NULL }
    462 };
    463 
    464 
    465 int
    466 main (int argc,
    467       char *const *argv)
    468 {
    469   return TDB_main (argc,
    470                    argv,
    471                    "test-purse-requests",
    472                    "Tests for the exchangedb `purse_requests' table",
    473                    tests);
    474 }
    475 
    476 
    477 /* end of test_purse_requests.c */