exchange

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

test_close_requests.c (14973B)


      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_close_requests.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `close_requests`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_insert_close_request(),
     23  * #TALER_EXCHANGEDB_get_reserve_close_request_info() and
     24  * #TALER_EXCHANGEDB_iterate_unfinished_close_requests().
     25  *
     26  * The table references `reserves`, which TDB_reserve() creates.  The
     27  * iterator is destructive -- it marks everything it returns as done -- so
     28  * the checks that read the table run before it.
     29  */
     30 #include "test_common.h"
     31 #include "exchange-database/insert_close_request.h"
     32 #include "exchange-database/get_reserve_close_request_info.h"
     33 #include "exchange-database/iterate_unfinished_close_requests.h"
     34 
     35 
     36 /**
     37  * Account the checks ask to be paid out to.
     38  */
     39 #define PAYTO "payto://x-taler-bank/localhost/cr?receiver-name=CR"
     40 
     41 
     42 /**
     43  * Build a timestamp from a number of seconds since the epoch.
     44  *
     45  * @param secs seconds since the epoch
     46  * @return the timestamp
     47  */
     48 static struct GNUNET_TIME_Timestamp
     49 ts (uint64_t secs)
     50 {
     51   struct GNUNET_TIME_Absolute abs = {
     52     .abs_value_us = secs * 1000LLU * 1000LLU
     53   };
     54 
     55   return GNUNET_TIME_absolute_to_timestamp (abs);
     56 }
     57 
     58 
     59 /**
     60  * Closure for #expired_cb().
     61  */
     62 struct ExpiredContext
     63 {
     64   /**
     65    * How many requests did the callback see?
     66    */
     67   unsigned int total;
     68 
     69   /**
     70    * Reserve of the last request seen.
     71    */
     72   struct TALER_ReservePublicKeyP reserve_pub;
     73 
     74   /**
     75    * Balance of the last request seen.
     76    */
     77   struct TALER_Amount left;
     78 
     79   /**
     80    * Row of the last request seen.
     81    */
     82   uint64_t close_request_row;
     83 
     84   /**
     85    * Account of the last request seen, owned by this struct.
     86    */
     87   char *account;
     88 };
     89 
     90 
     91 /**
     92  * Callback for #TALER_EXCHANGEDB_iterate_unfinished_close_requests().
     93  *
     94  * @param cls a `struct ExpiredContext *`
     95  * @param reserve_pub the reserve to close
     96  * @param left balance to pay out
     97  * @param account_details where to pay it
     98  * @param expiration_date when the request was made
     99  * @param close_request_row row of the request
    100  * @return #GNUNET_OK
    101  */
    102 static enum GNUNET_GenericReturnValue
    103 expired_cb (void *cls,
    104             const struct TALER_ReservePublicKeyP *reserve_pub,
    105             const struct TALER_Amount *left,
    106             const struct TALER_FullPayto account_details,
    107             struct GNUNET_TIME_Timestamp expiration_date,
    108             uint64_t close_request_row)
    109 {
    110   struct ExpiredContext *ctx = cls;
    111 
    112   (void) expiration_date;
    113   ctx->total++;
    114   ctx->reserve_pub = *reserve_pub;
    115   ctx->left = *left;
    116   ctx->close_request_row = close_request_row;
    117   GNUNET_free (ctx->account);
    118   ctx->account = (NULL == account_details.full_payto)
    119     ? NULL
    120     : GNUNET_strdup (account_details.full_payto);
    121   return GNUNET_OK;
    122 }
    123 
    124 
    125 /**
    126  * Look up the row of the close request of a reserve.
    127  *
    128  * @param pg the database context
    129  * @param reserve_pub reserve to look up
    130  * @return the row
    131  */
    132 static uint64_t
    133 request_row (struct TALER_EXCHANGEDB_PostgresContext *pg,
    134              const struct TALER_ReservePublicKeyP *reserve_pub)
    135 {
    136   struct GNUNET_PQ_QueryParam params[] = {
    137     GNUNET_PQ_query_param_auto_from_type (reserve_pub),
    138     GNUNET_PQ_query_param_end
    139   };
    140   uint64_t row;
    141   struct GNUNET_PQ_ResultSpec rs[] = {
    142     GNUNET_PQ_result_spec_uint64 ("close_request_serial_id",
    143                                   &row),
    144     GNUNET_PQ_result_spec_end
    145   };
    146 
    147   GNUNET_assert (GNUNET_OK ==
    148                  GNUNET_PQ_prepare_anon (pg->conn,
    149                                          "SELECT close_request_serial_id"
    150                                          " FROM close_requests"
    151                                          " WHERE reserve_pub=$1;"));
    152   GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
    153                  GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
    154                                                            "",
    155                                                            params,
    156                                                            rs));
    157   return row;
    158 }
    159 
    160 
    161 /**
    162  * Ask for a reserve to be closed.
    163  *
    164  * @param pg the database context
    165  * @param reserve_pub reserve to close
    166  * @param seed seed for the reserve signature
    167  * @param when when the request was made, in seconds since the epoch
    168  * @param balance balance to pay out, e.g. "10"
    169  * @param fee closing fee, e.g. "0.5"
    170  * @return transaction status
    171  */
    172 static enum GNUNET_DB_QueryStatus
    173 add_request (struct TALER_EXCHANGEDB_PostgresContext *pg,
    174              const struct TALER_ReservePublicKeyP *reserve_pub,
    175              uint32_t seed,
    176              uint64_t when,
    177              const char *balance,
    178              const char *fee)
    179 {
    180   struct TALER_FullPayto payto = {
    181     .full_payto = (char *) PAYTO
    182   };
    183   struct TALER_ReserveSignatureP reserve_sig;
    184   struct TALER_Amount b = TDB_amount (balance);
    185   struct TALER_Amount f = TDB_amount (fee);
    186 
    187   TDB_fill (&reserve_sig,
    188             sizeof (reserve_sig),
    189             seed);
    190   return TALER_EXCHANGEDB_insert_close_request (pg,
    191                                                 reserve_pub,
    192                                                 payto,
    193                                                 &reserve_sig,
    194                                                 ts (when),
    195                                                 &b,
    196                                                 &f);
    197 }
    198 
    199 
    200 /**
    201  * Nothing is known while the table is empty.
    202  *
    203  * @param pg the database context
    204  * @return 0 on success
    205  */
    206 static int
    207 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
    208 {
    209   struct TALER_ReservePublicKeyP reserve_pub;
    210   struct TALER_ReserveSignatureP reserve_sig;
    211   struct GNUNET_TIME_Timestamp request_timestamp;
    212   struct TALER_Amount close_balance;
    213   struct TALER_Amount close_fee;
    214   struct TALER_FullPayto payto = { NULL };
    215   struct ExpiredContext ctx = { 0 };
    216 
    217   TDB_FILL (reserve_pub,
    218             1);
    219   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    220           TALER_EXCHANGEDB_get_reserve_close_request_info (pg,
    221                                                            &reserve_pub,
    222                                                            1,
    223                                                            &reserve_sig,
    224                                                            &request_timestamp,
    225                                                            &close_balance,
    226                                                            &close_fee,
    227                                                            &payto));
    228   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    229           TALER_EXCHANGEDB_iterate_unfinished_close_requests (pg,
    230                                                               &expired_cb,
    231                                                               &ctx));
    232   FAILIF (0 != ctx.total);
    233   return 0;
    234 }
    235 
    236 
    237 /**
    238  * A close request is stored and found by (reserve, row).
    239  *
    240  * @param pg the database context
    241  * @return 0 on success
    242  */
    243 static int
    244 check_insert_and_lookup (struct TALER_EXCHANGEDB_PostgresContext *pg)
    245 {
    246   struct TALER_ReservePublicKeyP reserve_pub;
    247   struct TALER_ReserveSignatureP reserve_sig;
    248   struct TALER_ReserveSignatureP expect_sig;
    249   struct GNUNET_TIME_Timestamp request_timestamp;
    250   struct TALER_Amount close_balance;
    251   struct TALER_Amount close_fee;
    252   struct TALER_Amount expect_balance = TDB_amount ("10");
    253   struct TALER_Amount expect_fee = TDB_amount ("0.5");
    254   struct TALER_FullPayto payto = { NULL };
    255   uint64_t row;
    256 
    257   TDB_reserve (pg,
    258                10,
    259                "10",
    260                &reserve_pub);
    261   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    262           add_request (pg,
    263                        &reserve_pub,
    264                        10,
    265                        1600000000,
    266                        "10",
    267                        "0.5"));
    268   row = request_row (pg,
    269                      &reserve_pub);
    270   TDB_FILL (expect_sig,
    271             10);
    272   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    273           TALER_EXCHANGEDB_get_reserve_close_request_info (pg,
    274                                                            &reserve_pub,
    275                                                            row,
    276                                                            &reserve_sig,
    277                                                            &request_timestamp,
    278                                                            &close_balance,
    279                                                            &close_fee,
    280                                                            &payto));
    281   FAILIF (0 != GNUNET_memcmp (&reserve_sig,
    282                               &expect_sig));
    283   FAILIF (GNUNET_TIME_timestamp_cmp (request_timestamp,
    284                                      !=,
    285                                      ts (1600000000)));
    286   FAILIF (0 != TALER_amount_cmp (&close_balance,
    287                                  &expect_balance));
    288   FAILIF (0 != TALER_amount_cmp (&close_fee,
    289                                  &expect_fee));
    290   FAILIF (NULL == payto.full_payto);
    291   FAILIF_C (0 != strcmp (payto.full_payto,
    292                          PAYTO),
    293             GNUNET_free (payto.full_payto));
    294   GNUNET_free (payto.full_payto);
    295 
    296   /* the row has to belong to the reserve that is asked about */
    297   payto.full_payto = NULL;
    298   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    299           TALER_EXCHANGEDB_get_reserve_close_request_info (pg,
    300                                                            &reserve_pub,
    301                                                            row + 1000,
    302                                                            &reserve_sig,
    303                                                            &request_timestamp,
    304                                                            &close_balance,
    305                                                            &close_fee,
    306                                                            &payto));
    307   {
    308     struct TALER_ReservePublicKeyP other;
    309 
    310     TDB_FILL (other,
    311               99);
    312     FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    313             TALER_EXCHANGEDB_get_reserve_close_request_info (pg,
    314                                                              &other,
    315                                                              row,
    316                                                              &reserve_sig,
    317                                                              &request_timestamp,
    318                                                              &close_balance,
    319                                                              &close_fee,
    320                                                              &payto));
    321   }
    322 
    323   /* a second request at the same timestamp is absorbed */
    324   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    325           add_request (pg,
    326                        &reserve_pub,
    327                        11,
    328                        1600000000,
    329                        "10",
    330                        "0.5"));
    331   FAILIF (1 != TDB_count (pg,
    332                           "FROM close_requests"));
    333   /* ...but one at another timestamp is a second request */
    334   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    335           add_request (pg,
    336                        &reserve_pub,
    337                        11,
    338                        1600003600,
    339                        "10",
    340                        "0.5"));
    341   FAILIF (2 != TDB_count (pg,
    342                           "FROM close_requests"));
    343   return 0;
    344 }
    345 
    346 
    347 /**
    348  * The iterator hands over every unfinished request exactly once, marking
    349  * it done as it goes.
    350  *
    351  * @param pg the database context
    352  * @return 0 on success
    353  */
    354 static int
    355 check_iterate_unfinished (struct TALER_EXCHANGEDB_PostgresContext *pg)
    356 {
    357   struct ExpiredContext ctx;
    358 
    359   FAILIF (2 != TDB_count (pg,
    360                           "FROM close_requests WHERE NOT done"));
    361   memset (&ctx,
    362           0,
    363           sizeof (ctx));
    364   FAILIF (2 !=
    365           TALER_EXCHANGEDB_iterate_unfinished_close_requests (pg,
    366                                                               &expired_cb,
    367                                                               &ctx));
    368   FAILIF_C (2 != ctx.total,
    369             GNUNET_free (ctx.account));
    370   FAILIF_C (NULL == ctx.account,
    371             GNUNET_free (ctx.account));
    372   FAILIF_C (0 != strcmp (ctx.account,
    373                          PAYTO),
    374             GNUNET_free (ctx.account));
    375   FAILIF_C (0 == ctx.close_request_row,
    376             GNUNET_free (ctx.account));
    377   GNUNET_free (ctx.account);
    378   FAILIF (0 != TDB_count (pg,
    379                           "FROM close_requests WHERE NOT done"));
    380 
    381   /* everything is done now, so a second sweep finds nothing */
    382   memset (&ctx,
    383           0,
    384           sizeof (ctx));
    385   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    386           TALER_EXCHANGEDB_iterate_unfinished_close_requests (pg,
    387                                                               &expired_cb,
    388                                                               &ctx));
    389   FAILIF (0 != ctx.total);
    390 
    391   /* a fresh request is picked up again */
    392   {
    393     struct TALER_ReservePublicKeyP reserve_pub;
    394 
    395     TDB_reserve (pg,
    396                  12,
    397                  "3",
    398                  &reserve_pub);
    399     FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    400             add_request (pg,
    401                          &reserve_pub,
    402                          12,
    403                          1600007200,
    404                          "3",
    405                          "0.5"));
    406     memset (&ctx,
    407             0,
    408             sizeof (ctx));
    409     FAILIF (1 !=
    410             TALER_EXCHANGEDB_iterate_unfinished_close_requests (pg,
    411                                                                 &expired_cb,
    412                                                                 &ctx));
    413     FAILIF_C (0 != GNUNET_memcmp (&ctx.reserve_pub,
    414                                   &reserve_pub),
    415               GNUNET_free (ctx.account));
    416     GNUNET_free (ctx.account);
    417   }
    418   return 0;
    419 }
    420 
    421 
    422 /**
    423  * The checks to run, in order.
    424  */
    425 static const struct TDB_Test tests[] = {
    426   { "close-requests-empty",
    427     &check_empty },
    428   { "close-requests-insert-and-lookup",
    429     &check_insert_and_lookup },
    430   { "close-requests-iterate-unfinished",
    431     &check_iterate_unfinished },
    432   { NULL, NULL }
    433 };
    434 
    435 
    436 int
    437 main (int argc,
    438       char *const *argv)
    439 {
    440   return TDB_main (argc,
    441                    argv,
    442                    "test-close-requests",
    443                    "Tests for the exchangedb `close_requests' table",
    444                    tests);
    445 }
    446 
    447 
    448 /* end of test_close_requests.c */