exchange

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

test_misc.c (19653B)


      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_misc.c
     18  * @brief tests for the exchangedb functions that have no table of their own
     19  * @author Christian Grothoff
     20  *
     21  * Covers #TALER_EXCHANGEDB_start(), #TALER_EXCHANGEDB_start_read_only(),
     22  * #TALER_EXCHANGEDB_start_read_committed(),
     23  * #TALER_EXCHANGEDB_start_deferred_wire_out(), #TALER_EXCHANGEDB_commit(),
     24  * #TALER_EXCHANGEDB_rollback(), #TALER_EXCHANGEDB_preflight(),
     25  * #TALER_EXCHANGEDB_compute_shard(),
     26  * #TALER_EXCHANGEDB_calculate_transaction_list_totals(),
     27  * #TALER_EXCHANGEDB_free_coin_transaction_list(),
     28  * #TALER_EXCHANGEDB_free_reserve_history(),
     29  * #TALER_EXCHANGEDB_event_listen(),
     30  * #TALER_EXCHANGEDB_event_listen_cancel(),
     31  * #TALER_EXCHANGEDB_event_notify(), #TALER_EXCHANGEDB_enable_rules(),
     32  * #TALER_EXCHANGEDB_disable_rules(),
     33  * #TALER_EXCHANGEDB_inject_auditor_triggers(),
     34  * #TALER_EXCHANGEDB_drop_tables() and #TALER_EXCHANGEDB_create_tables().
     35  *
     36  * These are the connection-level functions: transactions, notifications,
     37  * schema management and the two list destructors.  The schema checks come
     38  * last on purpose -- they take the tables away and put them back, so
     39  * nothing may run after them.
     40  */
     41 #include "test_common.h"
     42 #include "taler/taler_dbevents.h"
     43 #include "exchange-database/commit.h"
     44 #include "exchange-database/compute_shard.h"
     45 #include "exchange-database/create_tables.h"
     46 #include "exchange-database/disable_rules.h"
     47 #include "exchange-database/drop_tables.h"
     48 #include "exchange-database/enable_rules.h"
     49 #include "exchange-database/event_listen.h"
     50 #include "exchange-database/event_listen_cancel.h"
     51 #include "exchange-database/event_notify.h"
     52 #include "exchange-database/free_coin_transaction_list.h"
     53 #include "exchange-database/free_reserve_history.h"
     54 #include "exchange-database/get_coin_transactions.h"
     55 #include "exchange-database/get_reserve_history.h"
     56 #include "exchange-database/inject_auditor_triggers.h"
     57 #include "exchange-database/preflight.h"
     58 #include "exchange-database/rollback.h"
     59 #include "exchange-database/start.h"
     60 #include "exchange-database/start_deferred_wire_out.h"
     61 #include "exchange-database/start_read_committed.h"
     62 #include "exchange-database/start_read_only.h"
     63 
     64 
     65 /**
     66  * Account the checks fund their reserves from.
     67  */
     68 static struct TDB_Account account;
     69 
     70 
     71 /**
     72  * Denomination the checks use.
     73  */
     74 static struct TDB_Denom denom;
     75 
     76 
     77 /**
     78  * The four ways to start a transaction.
     79  *
     80  * @param pg the database context
     81  * @param name name of the transaction
     82  * @return #GNUNET_OK on success
     83  */
     84 static enum GNUNET_GenericReturnValue
     85 start_serializable (struct TALER_EXCHANGEDB_PostgresContext *pg,
     86                     const char *name)
     87 {
     88   return TALER_EXCHANGEDB_start (pg,
     89                                  name);
     90 }
     91 
     92 
     93 /**
     94  * Start a read-only transaction.
     95  *
     96  * @param pg the database context
     97  * @param name name of the transaction
     98  * @return #GNUNET_OK on success
     99  */
    100 static enum GNUNET_GenericReturnValue
    101 start_read_only (struct TALER_EXCHANGEDB_PostgresContext *pg,
    102                  const char *name)
    103 {
    104   return TALER_EXCHANGEDB_start_read_only (pg,
    105                                            name);
    106 }
    107 
    108 
    109 /**
    110  * Start a read-committed transaction.
    111  *
    112  * @param pg the database context
    113  * @param name name of the transaction
    114  * @return #GNUNET_OK on success
    115  */
    116 static enum GNUNET_GenericReturnValue
    117 start_read_committed (struct TALER_EXCHANGEDB_PostgresContext *pg,
    118                       const char *name)
    119 {
    120   return TALER_EXCHANGEDB_start_read_committed (pg,
    121                                                 name);
    122 }
    123 
    124 
    125 /**
    126  * Start a transaction with deferred wire-out constraints.
    127  *
    128  * @param pg the database context
    129  * @param name name of the transaction
    130  * @return #GNUNET_OK on success
    131  */
    132 static enum GNUNET_GenericReturnValue
    133 start_deferred (struct TALER_EXCHANGEDB_PostgresContext *pg,
    134                 const char *name)
    135 {
    136   (void) name;
    137   return TALER_EXCHANGEDB_start_deferred_wire_out (pg);
    138 }
    139 
    140 
    141 /**
    142  * All four transaction starters behave the same way as far as the
    143  * connection's bookkeeping is concerned.
    144  *
    145  * @param pg the database context
    146  * @return 0 on success
    147  */
    148 static int
    149 check_transactions (struct TALER_EXCHANGEDB_PostgresContext *pg)
    150 {
    151   static enum GNUNET_GenericReturnValue (*const starters[])(
    152     struct TALER_EXCHANGEDB_PostgresContext *,
    153     const char *) = {
    154     &start_serializable,
    155     &start_read_only,
    156     &start_read_committed,
    157     &start_deferred
    158   };
    159 
    160   FAILIF (NULL != pg->transaction_name);
    161   for (unsigned int i = 0; i < 4; i++)
    162   {
    163     /* commit ends the transaction ... */
    164     FAILIF (GNUNET_OK !=
    165             starters[i](pg,
    166                         "misc-commit"));
    167     FAILIF (NULL == pg->transaction_name);
    168     FAILIF (0 > TALER_EXCHANGEDB_commit (pg));
    169     FAILIF (NULL != pg->transaction_name);
    170 
    171     /* ... and so does a rollback */
    172     FAILIF (GNUNET_OK !=
    173             starters[i](pg,
    174                         "misc-rollback"));
    175     FAILIF (NULL == pg->transaction_name);
    176     TALER_EXCHANGEDB_rollback (pg);
    177     FAILIF (NULL != pg->transaction_name);
    178   }
    179 
    180   /* rolling back when nothing is open is a no-op */
    181   TALER_EXCHANGEDB_rollback (pg);
    182   FAILIF (NULL != pg->transaction_name);
    183   return 0;
    184 }
    185 
    186 
    187 /**
    188  * A transaction really isolates: what it wrote is gone after a rollback
    189  * and there after a commit.
    190  *
    191  * @param pg the database context
    192  * @return 0 on success
    193  */
    194 static int
    195 check_isolation (struct TALER_EXCHANGEDB_PostgresContext *pg)
    196 {
    197   FAILIF (GNUNET_OK !=
    198           TALER_EXCHANGEDB_start (pg,
    199                                   "misc-isolation"));
    200   TDB_account (pg,
    201                10,
    202                &account);
    203   FAILIF (1 != TDB_count (pg,
    204                           "FROM wire_targets"));
    205   TALER_EXCHANGEDB_rollback (pg);
    206   FAILIF (0 != TDB_count (pg,
    207                           "FROM wire_targets"));
    208   TDB_account_free (&account);
    209 
    210   FAILIF (GNUNET_OK !=
    211           TALER_EXCHANGEDB_start (pg,
    212                                   "misc-isolation"));
    213   TDB_account (pg,
    214                10,
    215                &account);
    216   FAILIF (0 > TALER_EXCHANGEDB_commit (pg));
    217   FAILIF (1 != TDB_count (pg,
    218                           "FROM wire_targets"));
    219   return 0;
    220 }
    221 
    222 
    223 /**
    224  * Preflight is happy on an idle connection and cleans up after a caller
    225  * that forgot to end its transaction.
    226  *
    227  * @param pg the database context
    228  * @return 0 on success
    229  */
    230 static int
    231 check_preflight (struct TALER_EXCHANGEDB_PostgresContext *pg)
    232 {
    233   FAILIF (GNUNET_OK !=
    234           TALER_EXCHANGEDB_preflight (pg));
    235 
    236   /* a forgotten transaction is rolled back, and preflight says so */
    237   FAILIF (GNUNET_OK !=
    238           TALER_EXCHANGEDB_start (pg,
    239                                   "misc-leak"));
    240   FAILIF (GNUNET_NO !=
    241           TALER_EXCHANGEDB_preflight (pg));
    242   FAILIF (NULL != pg->transaction_name);
    243   FAILIF (GNUNET_OK !=
    244           TALER_EXCHANGEDB_preflight (pg));
    245 
    246   /* which is also what start() does for us */
    247   FAILIF (GNUNET_OK !=
    248           TALER_EXCHANGEDB_start (pg,
    249                                   "misc-leak"));
    250   FAILIF (GNUNET_OK !=
    251           TALER_EXCHANGEDB_start (pg,
    252                                   "misc-after-leak"));
    253   TALER_EXCHANGEDB_rollback (pg);
    254   return 0;
    255 }
    256 
    257 
    258 /**
    259  * The shard of a merchant key is deterministic and stays in range.
    260  *
    261  * @param pg the database context
    262  * @return 0 on success
    263  */
    264 static int
    265 check_compute_shard (struct TALER_EXCHANGEDB_PostgresContext *pg)
    266 {
    267   struct TALER_MerchantPublicKeyP m1;
    268   struct TALER_MerchantPublicKeyP m2;
    269   uint64_t s1;
    270   uint64_t s2;
    271 
    272   (void) pg;
    273   TDB_FILL (m1,
    274             1);
    275   TDB_FILL (m2,
    276             2);
    277   s1 = TALER_EXCHANGEDB_compute_shard (&m1);
    278   s2 = TALER_EXCHANGEDB_compute_shard (&m2);
    279   FAILIF (s1 > INT32_MAX);
    280   FAILIF (s2 > INT32_MAX);
    281   FAILIF (s1 == s2);
    282   /* the same key always maps to the same shard, otherwise the shard
    283      locks of the aggregator would not partition the work */
    284   FAILIF (s1 != TALER_EXCHANGEDB_compute_shard (&m1));
    285   FAILIF (s2 != TALER_EXCHANGEDB_compute_shard (&m2));
    286   return 0;
    287 }
    288 
    289 
    290 /**
    291  * The coin transaction list adds up and can be released.
    292  *
    293  * @param pg the database context
    294  * @return 0 on success
    295  */
    296 static int
    297 check_coin_list (struct TALER_EXCHANGEDB_PostgresContext *pg)
    298 {
    299   struct TALER_EXCHANGEDB_TransactionList *tl = NULL;
    300   struct TALER_CoinPublicInfo coin;
    301   struct TDB_Deposit dep;
    302   struct TALER_Amount balance;
    303   struct TALER_Amount total;
    304   struct TALER_Amount zero = TDB_amount ("0");
    305   struct TALER_Amount one = TDB_amount ("1");
    306   struct TALER_Amount two = TDB_amount ("2");
    307   struct TALER_DenominationHashP h_denom_pub;
    308   uint64_t etag = 0;
    309 
    310   /* an empty list is worth the offset it started from */
    311   FAILIF (GNUNET_OK !=
    312           TALER_EXCHANGEDB_calculate_transaction_list_totals (NULL,
    313                                                               &one,
    314                                                               &total));
    315   FAILIF (0 != TALER_amount_cmp (&total,
    316                                  &one));
    317   /* and freeing it is a no-op */
    318   TALER_EXCHANGEDB_free_coin_transaction_list (NULL);
    319 
    320   TDB_denom (pg,
    321              10,
    322              "5",
    323              "0.1",
    324              &denom);
    325   TDB_coin (pg,
    326             &denom,
    327             20,
    328             &coin,
    329             NULL);
    330   TDB_deposit (pg,
    331                &account,
    332                &coin,
    333                30,
    334                "1",
    335                "0.1",
    336                GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS),
    337                GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS),
    338                &dep);
    339   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    340             TALER_EXCHANGEDB_get_coin_transactions (pg,
    341                                                     true,
    342                                                     &coin.coin_pub,
    343                                                     0,
    344                                                     0,
    345                                                     &etag,
    346                                                     &balance,
    347                                                     &h_denom_pub,
    348                                                     &tl),
    349             TDB_coin_free (&coin));
    350   TDB_coin_free (&coin);
    351   FAILIF (NULL == tl);
    352   FAILIF_C (GNUNET_OK !=
    353             TALER_EXCHANGEDB_calculate_transaction_list_totals (tl,
    354                                                                 &zero,
    355                                                                 &total),
    356             TALER_EXCHANGEDB_free_coin_transaction_list (tl));
    357   FAILIF_C (0 != TALER_amount_cmp (&total,
    358                                    &one),
    359             TALER_EXCHANGEDB_free_coin_transaction_list (tl));
    360   /* the offset is added on top */
    361   FAILIF_C (GNUNET_OK !=
    362             TALER_EXCHANGEDB_calculate_transaction_list_totals (tl,
    363                                                                 &one,
    364                                                                 &total),
    365             TALER_EXCHANGEDB_free_coin_transaction_list (tl));
    366   FAILIF_C (0 != TALER_amount_cmp (&total,
    367                                    &two),
    368             TALER_EXCHANGEDB_free_coin_transaction_list (tl));
    369   TALER_EXCHANGEDB_free_coin_transaction_list (tl);
    370   return 0;
    371 }
    372 
    373 
    374 /**
    375  * The reserve history can be released.
    376  *
    377  * @param pg the database context
    378  * @return 0 on success
    379  */
    380 static int
    381 check_reserve_history (struct TALER_EXCHANGEDB_PostgresContext *pg)
    382 {
    383   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
    384   struct TALER_ReservePublicKeyP reserve_pub;
    385   struct TALER_Amount balance;
    386   uint64_t etag = 0;
    387   unsigned int cnt = 0;
    388 
    389   /* freeing nothing is a no-op */
    390   TALER_EXCHANGEDB_free_reserve_history (NULL);
    391 
    392   TDB_reserve_in (pg,
    393                   &account,
    394                   11,
    395                   "10",
    396                   &reserve_pub);
    397   (void) TDB_withdraw (pg,
    398                        &denom,
    399                        &reserve_pub,
    400                        11,
    401                        "5");
    402   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    403           TALER_EXCHANGEDB_get_reserve_history (pg,
    404                                                 &reserve_pub,
    405                                                 0,
    406                                                 0,
    407                                                 &etag,
    408                                                 &balance,
    409                                                 &rh));
    410   FAILIF (NULL == rh);
    411   for (const struct TALER_EXCHANGEDB_ReserveHistory *pos = rh;
    412        NULL != pos;
    413        pos = pos->next)
    414     cnt++;
    415   FAILIF_C (2 != cnt,
    416             TALER_EXCHANGEDB_free_reserve_history (rh));
    417   TALER_EXCHANGEDB_free_reserve_history (rh);
    418   return 0;
    419 }
    420 
    421 
    422 /**
    423  * State of the event check.
    424  */
    425 struct EventState
    426 {
    427   /**
    428    * How often did the callback fire with a payload?
    429    */
    430   unsigned int fired;
    431 
    432   /**
    433    * Payload of the last notification.
    434    */
    435   char payload[16];
    436 
    437   /**
    438    * Number of bytes in @e payload.
    439    */
    440   size_t payload_len;
    441 };
    442 
    443 
    444 /**
    445  * Payload of the notification.
    446  */
    447 #define PAYLOAD "hello"
    448 
    449 
    450 /**
    451  * Called when the notification arrives (or the listener times out).
    452  *
    453  * @param cls a `struct EventState *`
    454  * @param extra the payload, NULL on timeout
    455  * @param extra_size number of bytes in @a extra
    456  */
    457 static void
    458 event_cb (void *cls,
    459           const void *extra,
    460           size_t extra_size)
    461 {
    462   struct EventState *st = cls;
    463 
    464   if (NULL == extra)
    465     return; /* timeout event, not what we are after */
    466   st->fired++;
    467   st->payload_len = GNUNET_MIN (extra_size,
    468                                 sizeof (st->payload));
    469   memcpy (st->payload,
    470           extra,
    471           st->payload_len);
    472 }
    473 
    474 
    475 /**
    476  * A notification sent on the connection reaches a listener registered on
    477  * it, and stops reaching it once the listener is cancelled.
    478  *
    479  * @param pg the database context
    480  * @return 0 on success
    481  */
    482 static int
    483 check_events (struct TALER_EXCHANGEDB_PostgresContext *pg)
    484 {
    485   struct GNUNET_DB_EventHeaderP es = {
    486     .size = htons (sizeof (es)),
    487     .type = htons (TALER_DBEVENT_EXCHANGE_KYC_COMPLETED)
    488   };
    489   struct GNUNET_DB_EventHandler *eh;
    490   struct EventState st = { 0 };
    491 
    492   eh = TALER_EXCHANGEDB_event_listen (pg,
    493                                       GNUNET_TIME_UNIT_MINUTES,
    494                                       &es,
    495                                       &event_cb,
    496                                       &st);
    497   FAILIF (NULL == eh);
    498   TALER_EXCHANGEDB_event_notify (pg,
    499                                  &es,
    500                                  PAYLOAD,
    501                                  strlen (PAYLOAD));
    502   /* the scheduler task that would pick this up does not get to run while
    503      the checks are executing, so poll for it ourselves -- which is also
    504      what the exchange does after every notifying transaction */
    505   GNUNET_PQ_event_do_poll (pg->conn);
    506   FAILIF_C (1 != st.fired,
    507             TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
    508   FAILIF_C (strlen (PAYLOAD) != st.payload_len,
    509             TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
    510   FAILIF_C (0 != memcmp (st.payload,
    511                          PAYLOAD,
    512                          st.payload_len),
    513             TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
    514 
    515   /* an unrelated event does not wake us */
    516   {
    517     struct GNUNET_DB_EventHeaderP other = {
    518       .size = htons (sizeof (other)),
    519       .type = htons (TALER_DBEVENT_EXCHANGE_NEW_KYC_ATTRIBUTES)
    520     };
    521 
    522     TALER_EXCHANGEDB_event_notify (pg,
    523                                    &other,
    524                                    PAYLOAD,
    525                                    strlen (PAYLOAD));
    526     GNUNET_PQ_event_do_poll (pg->conn);
    527     FAILIF_C (1 != st.fired,
    528               TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
    529   }
    530 
    531   /* and after cancelling, neither does the one we listened for */
    532   TALER_EXCHANGEDB_event_listen_cancel (pg,
    533                                         eh);
    534   TALER_EXCHANGEDB_event_notify (pg,
    535                                  &es,
    536                                  PAYLOAD,
    537                                  strlen (PAYLOAD));
    538   GNUNET_PQ_event_do_poll (pg->conn);
    539   FAILIF (1 != st.fired);
    540   return 0;
    541 }
    542 
    543 
    544 /**
    545  * Customization rules that were never deployed cannot be dropped.
    546  *
    547  * @param pg the database context
    548  * @return 0 on success
    549  */
    550 static int
    551 check_rules (struct TALER_EXCHANGEDB_PostgresContext *pg)
    552 {
    553   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    554           TALER_EXCHANGEDB_disable_rules (pg,
    555                                           "no-such-customization"));
    556   /* and a schema without SQL files on disk cannot be deployed; the
    557      search_path must be back to `exchange' afterwards either way */
    558   FAILIF (GNUNET_SYSERR ==
    559           TALER_EXCHANGEDB_enable_rules (pg,
    560                                          "no-such-customization"));
    561   /* the search_path is back to `exchange', so an unqualified query
    562      still finds the exchange's own tables */
    563   FAILIF (1 != TDB_count (pg,
    564                           "FROM wire_targets"));
    565   return 0;
    566 }
    567 
    568 
    569 /**
    570  * The auditor triggers can be injected into an existing schema.
    571  *
    572  * @param pg the database context
    573  * @return 0 on success
    574  */
    575 static int
    576 check_auditor_triggers (struct TALER_EXCHANGEDB_PostgresContext *pg)
    577 {
    578   FAILIF (GNUNET_OK !=
    579           TALER_EXCHANGEDB_inject_auditor_triggers (pg));
    580   return 0;
    581 }
    582 
    583 
    584 /**
    585  * Dropping and re-creating the schema leaves an empty but usable
    586  * database.  Nothing may run after this check.
    587  *
    588  * @param pg the database context
    589  * @return 0 on success
    590  */
    591 static int
    592 check_schema (struct TALER_EXCHANGEDB_PostgresContext *pg)
    593 {
    594   FAILIF (0 == TDB_count (pg,
    595                           "FROM wire_targets"));
    596   FAILIF (GNUNET_OK !=
    597           TALER_EXCHANGEDB_drop_tables (pg));
    598   /* dropping twice fails: drop.sql says `DROP SCHEMA exchange CASCADE'
    599      without IF EXISTS, so there is nothing left to drop the second time */
    600   FAILIF (GNUNET_OK ==
    601           TALER_EXCHANGEDB_drop_tables (pg));
    602   FAILIF (GNUNET_OK !=
    603           TALER_EXCHANGEDB_create_tables (pg,
    604                                           false,
    605                                           0));
    606   FAILIF (0 != TDB_count (pg,
    607                           "FROM wire_targets"));
    608   return 0;
    609 }
    610 
    611 
    612 /**
    613  * The checks to run, in order.
    614  */
    615 static const struct TDB_Test tests[] = {
    616   { "misc-transactions",
    617     &check_transactions },
    618   { "misc-isolation",
    619     &check_isolation },
    620   { "misc-preflight",
    621     &check_preflight },
    622   { "misc-compute-shard",
    623     &check_compute_shard },
    624   { "misc-coin-list",
    625     &check_coin_list },
    626   { "misc-reserve-history",
    627     &check_reserve_history },
    628   { "misc-events",
    629     &check_events },
    630   { "misc-rules",
    631     &check_rules },
    632   { "misc-auditor-triggers",
    633     &check_auditor_triggers },
    634   { "misc-schema",
    635     &check_schema },
    636   { NULL, NULL }
    637 };
    638 
    639 
    640 int
    641 main (int argc,
    642       char *const *argv)
    643 {
    644   int ret;
    645 
    646   ret = TDB_main (argc,
    647                   argv,
    648                   "test-misc",
    649                   "Tests for the exchangedb functions without a table",
    650                   tests);
    651   TDB_denom_free (&denom);
    652   TDB_account_free (&account);
    653   return ret;
    654 }
    655 
    656 
    657 /* end of test_misc.c */