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 (19622B)


      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 
    209   FAILIF (GNUNET_OK !=
    210           TALER_EXCHANGEDB_start (pg,
    211                                   "misc-isolation"));
    212   TDB_account (pg,
    213                10,
    214                &account);
    215   FAILIF (0 > TALER_EXCHANGEDB_commit (pg));
    216   FAILIF (1 != TDB_count (pg,
    217                           "FROM wire_targets"));
    218   return 0;
    219 }
    220 
    221 
    222 /**
    223  * Preflight is happy on an idle connection and cleans up after a caller
    224  * that forgot to end its transaction.
    225  *
    226  * @param pg the database context
    227  * @return 0 on success
    228  */
    229 static int
    230 check_preflight (struct TALER_EXCHANGEDB_PostgresContext *pg)
    231 {
    232   FAILIF (GNUNET_OK !=
    233           TALER_EXCHANGEDB_preflight (pg));
    234 
    235   /* a forgotten transaction is rolled back, and preflight says so */
    236   FAILIF (GNUNET_OK !=
    237           TALER_EXCHANGEDB_start (pg,
    238                                   "misc-leak"));
    239   FAILIF (GNUNET_NO !=
    240           TALER_EXCHANGEDB_preflight (pg));
    241   FAILIF (NULL != pg->transaction_name);
    242   FAILIF (GNUNET_OK !=
    243           TALER_EXCHANGEDB_preflight (pg));
    244 
    245   /* which is also what start() does for us */
    246   FAILIF (GNUNET_OK !=
    247           TALER_EXCHANGEDB_start (pg,
    248                                   "misc-leak"));
    249   FAILIF (GNUNET_OK !=
    250           TALER_EXCHANGEDB_start (pg,
    251                                   "misc-after-leak"));
    252   TALER_EXCHANGEDB_rollback (pg);
    253   return 0;
    254 }
    255 
    256 
    257 /**
    258  * The shard of a merchant key is deterministic and stays in range.
    259  *
    260  * @param pg the database context
    261  * @return 0 on success
    262  */
    263 static int
    264 check_compute_shard (struct TALER_EXCHANGEDB_PostgresContext *pg)
    265 {
    266   struct TALER_MerchantPublicKeyP m1;
    267   struct TALER_MerchantPublicKeyP m2;
    268   uint64_t s1;
    269   uint64_t s2;
    270 
    271   (void) pg;
    272   TDB_FILL (m1,
    273             1);
    274   TDB_FILL (m2,
    275             2);
    276   s1 = TALER_EXCHANGEDB_compute_shard (&m1);
    277   s2 = TALER_EXCHANGEDB_compute_shard (&m2);
    278   FAILIF (s1 > INT32_MAX);
    279   FAILIF (s2 > INT32_MAX);
    280   FAILIF (s1 == s2);
    281   /* the same key always maps to the same shard, otherwise the shard
    282      locks of the aggregator would not partition the work */
    283   FAILIF (s1 != TALER_EXCHANGEDB_compute_shard (&m1));
    284   FAILIF (s2 != TALER_EXCHANGEDB_compute_shard (&m2));
    285   return 0;
    286 }
    287 
    288 
    289 /**
    290  * The coin transaction list adds up and can be released.
    291  *
    292  * @param pg the database context
    293  * @return 0 on success
    294  */
    295 static int
    296 check_coin_list (struct TALER_EXCHANGEDB_PostgresContext *pg)
    297 {
    298   struct TALER_EXCHANGEDB_TransactionList *tl = NULL;
    299   struct TALER_CoinPublicInfo coin;
    300   struct TDB_Deposit dep;
    301   struct TALER_Amount balance;
    302   struct TALER_Amount total;
    303   struct TALER_Amount zero = TDB_amount ("0");
    304   struct TALER_Amount one = TDB_amount ("1");
    305   struct TALER_Amount two = TDB_amount ("2");
    306   struct TALER_DenominationHashP h_denom_pub;
    307   uint64_t etag = 0;
    308 
    309   /* an empty list is worth the offset it started from */
    310   FAILIF (GNUNET_OK !=
    311           TALER_EXCHANGEDB_calculate_transaction_list_totals (NULL,
    312                                                               &one,
    313                                                               &total));
    314   FAILIF (0 != TALER_amount_cmp (&total,
    315                                  &one));
    316   /* and freeing it is a no-op */
    317   TALER_EXCHANGEDB_free_coin_transaction_list (NULL);
    318 
    319   TDB_denom (pg,
    320              10,
    321              "5",
    322              "0.1",
    323              &denom);
    324   TDB_coin (pg,
    325             &denom,
    326             20,
    327             &coin,
    328             NULL);
    329   TDB_deposit (pg,
    330                &account,
    331                &coin,
    332                30,
    333                "1",
    334                "0.1",
    335                GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS),
    336                GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS),
    337                &dep);
    338   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    339             TALER_EXCHANGEDB_get_coin_transactions (pg,
    340                                                     true,
    341                                                     &coin.coin_pub,
    342                                                     0,
    343                                                     0,
    344                                                     &etag,
    345                                                     &balance,
    346                                                     &h_denom_pub,
    347                                                     &tl),
    348             TDB_coin_free (&coin));
    349   TDB_coin_free (&coin);
    350   FAILIF (NULL == tl);
    351   FAILIF_C (GNUNET_OK !=
    352             TALER_EXCHANGEDB_calculate_transaction_list_totals (tl,
    353                                                                 &zero,
    354                                                                 &total),
    355             TALER_EXCHANGEDB_free_coin_transaction_list (tl));
    356   FAILIF_C (0 != TALER_amount_cmp (&total,
    357                                    &one),
    358             TALER_EXCHANGEDB_free_coin_transaction_list (tl));
    359   /* the offset is added on top */
    360   FAILIF_C (GNUNET_OK !=
    361             TALER_EXCHANGEDB_calculate_transaction_list_totals (tl,
    362                                                                 &one,
    363                                                                 &total),
    364             TALER_EXCHANGEDB_free_coin_transaction_list (tl));
    365   FAILIF_C (0 != TALER_amount_cmp (&total,
    366                                    &two),
    367             TALER_EXCHANGEDB_free_coin_transaction_list (tl));
    368   TALER_EXCHANGEDB_free_coin_transaction_list (tl);
    369   return 0;
    370 }
    371 
    372 
    373 /**
    374  * The reserve history can be released.
    375  *
    376  * @param pg the database context
    377  * @return 0 on success
    378  */
    379 static int
    380 check_reserve_history (struct TALER_EXCHANGEDB_PostgresContext *pg)
    381 {
    382   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
    383   struct TALER_ReservePublicKeyP reserve_pub;
    384   struct TALER_Amount balance;
    385   uint64_t etag = 0;
    386   unsigned int cnt = 0;
    387 
    388   /* freeing nothing is a no-op */
    389   TALER_EXCHANGEDB_free_reserve_history (NULL);
    390 
    391   TDB_reserve_in (pg,
    392                   &account,
    393                   11,
    394                   "10",
    395                   &reserve_pub);
    396   (void) TDB_withdraw (pg,
    397                        &denom,
    398                        &reserve_pub,
    399                        11,
    400                        "5");
    401   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    402           TALER_EXCHANGEDB_get_reserve_history (pg,
    403                                                 &reserve_pub,
    404                                                 0,
    405                                                 0,
    406                                                 &etag,
    407                                                 &balance,
    408                                                 &rh));
    409   FAILIF (NULL == rh);
    410   for (const struct TALER_EXCHANGEDB_ReserveHistory *pos = rh;
    411        NULL != pos;
    412        pos = pos->next)
    413     cnt++;
    414   FAILIF_C (2 != cnt,
    415             TALER_EXCHANGEDB_free_reserve_history (rh));
    416   TALER_EXCHANGEDB_free_reserve_history (rh);
    417   return 0;
    418 }
    419 
    420 
    421 /**
    422  * State of the event check.
    423  */
    424 struct EventState
    425 {
    426   /**
    427    * How often did the callback fire with a payload?
    428    */
    429   unsigned int fired;
    430 
    431   /**
    432    * Payload of the last notification.
    433    */
    434   char payload[16];
    435 
    436   /**
    437    * Number of bytes in @e payload.
    438    */
    439   size_t payload_len;
    440 };
    441 
    442 
    443 /**
    444  * Payload of the notification.
    445  */
    446 #define PAYLOAD "hello"
    447 
    448 
    449 /**
    450  * Called when the notification arrives (or the listener times out).
    451  *
    452  * @param cls a `struct EventState *`
    453  * @param extra the payload, NULL on timeout
    454  * @param extra_size number of bytes in @a extra
    455  */
    456 static void
    457 event_cb (void *cls,
    458           const void *extra,
    459           size_t extra_size)
    460 {
    461   struct EventState *st = cls;
    462 
    463   if (NULL == extra)
    464     return; /* timeout event, not what we are after */
    465   st->fired++;
    466   st->payload_len = GNUNET_MIN (extra_size,
    467                                 sizeof (st->payload));
    468   memcpy (st->payload,
    469           extra,
    470           st->payload_len);
    471 }
    472 
    473 
    474 /**
    475  * A notification sent on the connection reaches a listener registered on
    476  * it, and stops reaching it once the listener is cancelled.
    477  *
    478  * @param pg the database context
    479  * @return 0 on success
    480  */
    481 static int
    482 check_events (struct TALER_EXCHANGEDB_PostgresContext *pg)
    483 {
    484   struct GNUNET_DB_EventHeaderP es = {
    485     .size = htons (sizeof (es)),
    486     .type = htons (TALER_DBEVENT_EXCHANGE_KYC_COMPLETED)
    487   };
    488   struct GNUNET_DB_EventHandler *eh;
    489   struct EventState st = { 0 };
    490 
    491   eh = TALER_EXCHANGEDB_event_listen (pg,
    492                                       GNUNET_TIME_UNIT_MINUTES,
    493                                       &es,
    494                                       &event_cb,
    495                                       &st);
    496   FAILIF (NULL == eh);
    497   TALER_EXCHANGEDB_event_notify (pg,
    498                                  &es,
    499                                  PAYLOAD,
    500                                  strlen (PAYLOAD));
    501   /* the scheduler task that would pick this up does not get to run while
    502      the checks are executing, so poll for it ourselves -- which is also
    503      what the exchange does after every notifying transaction */
    504   GNUNET_PQ_event_do_poll (pg->conn);
    505   FAILIF_C (1 != st.fired,
    506             TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
    507   FAILIF_C (strlen (PAYLOAD) != st.payload_len,
    508             TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
    509   FAILIF_C (0 != memcmp (st.payload,
    510                          PAYLOAD,
    511                          st.payload_len),
    512             TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
    513 
    514   /* an unrelated event does not wake us */
    515   {
    516     struct GNUNET_DB_EventHeaderP other = {
    517       .size = htons (sizeof (other)),
    518       .type = htons (TALER_DBEVENT_EXCHANGE_NEW_KYC_ATTRIBUTES)
    519     };
    520 
    521     TALER_EXCHANGEDB_event_notify (pg,
    522                                    &other,
    523                                    PAYLOAD,
    524                                    strlen (PAYLOAD));
    525     GNUNET_PQ_event_do_poll (pg->conn);
    526     FAILIF_C (1 != st.fired,
    527               TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
    528   }
    529 
    530   /* and after cancelling, neither does the one we listened for */
    531   TALER_EXCHANGEDB_event_listen_cancel (pg,
    532                                         eh);
    533   TALER_EXCHANGEDB_event_notify (pg,
    534                                  &es,
    535                                  PAYLOAD,
    536                                  strlen (PAYLOAD));
    537   GNUNET_PQ_event_do_poll (pg->conn);
    538   FAILIF (1 != st.fired);
    539   return 0;
    540 }
    541 
    542 
    543 /**
    544  * Customization rules that were never deployed cannot be dropped.
    545  *
    546  * @param pg the database context
    547  * @return 0 on success
    548  */
    549 static int
    550 check_rules (struct TALER_EXCHANGEDB_PostgresContext *pg)
    551 {
    552   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    553           TALER_EXCHANGEDB_disable_rules (pg,
    554                                           "no-such-customization"));
    555   /* and a schema without SQL files on disk cannot be deployed; the
    556      search_path must be back to `exchange' afterwards either way */
    557   FAILIF (GNUNET_SYSERR ==
    558           TALER_EXCHANGEDB_enable_rules (pg,
    559                                          "no-such-customization"));
    560   /* the search_path is back to `exchange', so an unqualified query
    561      still finds the exchange's own tables */
    562   FAILIF (1 != TDB_count (pg,
    563                           "FROM wire_targets"));
    564   return 0;
    565 }
    566 
    567 
    568 /**
    569  * The auditor triggers can be injected into an existing schema.
    570  *
    571  * @param pg the database context
    572  * @return 0 on success
    573  */
    574 static int
    575 check_auditor_triggers (struct TALER_EXCHANGEDB_PostgresContext *pg)
    576 {
    577   FAILIF (GNUNET_OK !=
    578           TALER_EXCHANGEDB_inject_auditor_triggers (pg));
    579   return 0;
    580 }
    581 
    582 
    583 /**
    584  * Dropping and re-creating the schema leaves an empty but usable
    585  * database.  Nothing may run after this check.
    586  *
    587  * @param pg the database context
    588  * @return 0 on success
    589  */
    590 static int
    591 check_schema (struct TALER_EXCHANGEDB_PostgresContext *pg)
    592 {
    593   FAILIF (0 == TDB_count (pg,
    594                           "FROM wire_targets"));
    595   FAILIF (GNUNET_OK !=
    596           TALER_EXCHANGEDB_drop_tables (pg));
    597   /* dropping twice fails: drop.sql says `DROP SCHEMA exchange CASCADE'
    598      without IF EXISTS, so there is nothing left to drop the second time */
    599   FAILIF (GNUNET_OK ==
    600           TALER_EXCHANGEDB_drop_tables (pg));
    601   FAILIF (GNUNET_OK !=
    602           TALER_EXCHANGEDB_create_tables (pg,
    603                                           false,
    604                                           0));
    605   FAILIF (0 != TDB_count (pg,
    606                           "FROM wire_targets"));
    607   return 0;
    608 }
    609 
    610 
    611 /**
    612  * The checks to run, in order.
    613  */
    614 static const struct TDB_Test tests[] = {
    615   { "misc-transactions",
    616     &check_transactions },
    617   { "misc-isolation",
    618     &check_isolation },
    619   { "misc-preflight",
    620     &check_preflight },
    621   { "misc-compute-shard",
    622     &check_compute_shard },
    623   { "misc-coin-list",
    624     &check_coin_list },
    625   { "misc-reserve-history",
    626     &check_reserve_history },
    627   { "misc-events",
    628     &check_events },
    629   { "misc-rules",
    630     &check_rules },
    631   { "misc-auditor-triggers",
    632     &check_auditor_triggers },
    633   { "misc-schema",
    634     &check_schema },
    635   { NULL, NULL }
    636 };
    637 
    638 
    639 int
    640 main (int argc,
    641       char *const *argv)
    642 {
    643   int ret;
    644 
    645   ret = TDB_main (argc,
    646                   argv,
    647                   "test-misc",
    648                   "Tests for the exchangedb functions without a table",
    649                   tests);
    650   TDB_denom_free (&denom);
    651   TDB_account_free (&account);
    652   return ret;
    653 }
    654 
    655 
    656 /* end of test_misc.c */