exchange

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

test_reserve_history.c (13865B)


      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_reserve_history.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `reserve_history`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_get_reserve_history().
     23  *
     24  * `reserve_history` is an index: AFTER INSERT triggers on the tables that
     25  * change a reserve add one row each, naming the table and the row.  The
     26  * function walks that index newest-first and looks each entry up in the
     27  * table it names.  So the checks build a reserve with several kinds of
     28  * operation on it and read the history back, and they check the ETag
     29  * short-circuit -- which must not leave a transaction open.
     30  */
     31 #include "test_common.h"
     32 #include "exchange-database/get_reserve_history.h"
     33 #include "exchange-database/insert_close_request.h"
     34 #include "exchange-database/start.h"
     35 #include "exchange-database/rollback.h"
     36 
     37 
     38 /**
     39  * Account the checks use.
     40  */
     41 static struct TDB_Account account;
     42 
     43 
     44 /**
     45  * Count the entries of @a rh, and how many of them have type @a type.
     46  *
     47  * @param rh history to walk
     48  * @param type operation to count
     49  * @param[out] matched set to the number of entries of that type
     50  * @return total number of entries
     51  */
     52 static unsigned int
     53 count_history (const struct TALER_EXCHANGEDB_ReserveHistory *rh,
     54                enum TALER_EXCHANGEDB_ReserveOperation type,
     55                unsigned int *matched)
     56 {
     57   unsigned int total = 0;
     58 
     59   *matched = 0;
     60   for (const struct TALER_EXCHANGEDB_ReserveHistory *p = rh;
     61        NULL != p;
     62        p = p->next)
     63   {
     64     total++;
     65     if (type == p->type)
     66       (*matched)++;
     67   }
     68   return total;
     69 }
     70 
     71 
     72 /**
     73  * An unknown reserve has no history at all.
     74  *
     75  * @param pg the database context
     76  * @return 0 on success
     77  */
     78 static int
     79 check_unknown_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg)
     80 {
     81   struct TALER_ReservePublicKeyP reserve_pub;
     82   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
     83   struct TALER_Amount balance;
     84   uint64_t etag_out = 42;
     85 
     86   TDB_FILL (reserve_pub,
     87             1);
     88   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     89           TALER_EXCHANGEDB_get_reserve_history (pg,
     90                                                 &reserve_pub,
     91                                                 0,
     92                                                 0,
     93                                                 &etag_out,
     94                                                 &balance,
     95                                                 &rh));
     96   FAILIF (NULL != rh);
     97   return 0;
     98 }
     99 
    100 
    101 /**
    102  * A reserve that exists but was never touched has no history either: the
    103  * `reserves` row alone does not create an index entry.
    104  *
    105  * @param pg the database context
    106  * @return 0 on success
    107  */
    108 static int
    109 check_untouched_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg)
    110 {
    111   struct TALER_ReservePublicKeyP reserve_pub;
    112   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
    113   struct TALER_Amount balance;
    114   uint64_t etag_out = 42;
    115 
    116   TDB_reserve (pg,
    117                2,
    118                "5",
    119                &reserve_pub);
    120   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    121           TALER_EXCHANGEDB_get_reserve_history (pg,
    122                                                 &reserve_pub,
    123                                                 0,
    124                                                 0,
    125                                                 &etag_out,
    126                                                 &balance,
    127                                                 &rh));
    128   FAILIF (NULL != rh);
    129   return 0;
    130 }
    131 
    132 
    133 /**
    134  * A bank transfer into the reserve shows up as one history entry.
    135  *
    136  * @param pg the database context
    137  * @return 0 on success
    138  */
    139 static int
    140 check_bank_transfer (struct TALER_EXCHANGEDB_PostgresContext *pg)
    141 {
    142   struct TALER_ReservePublicKeyP reserve_pub;
    143   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
    144   struct TALER_Amount balance;
    145   struct TALER_Amount expect = TDB_amount ("10");
    146   unsigned int matched;
    147   uint64_t etag_out = 0;
    148 
    149   TDB_account (pg,
    150                10,
    151                &account);
    152   TDB_reserve_in (pg,
    153                   &account,
    154                   10,
    155                   "10",
    156                   &reserve_pub);
    157   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    158           TALER_EXCHANGEDB_get_reserve_history (pg,
    159                                                 &reserve_pub,
    160                                                 0,
    161                                                 0,
    162                                                 &etag_out,
    163                                                 &balance,
    164                                                 &rh));
    165   FAILIF_C (NULL == rh,
    166             TALER_EXCHANGEDB_free_reserve_history (rh));
    167   FAILIF_C (0 == etag_out,
    168             TALER_EXCHANGEDB_free_reserve_history (rh));
    169   FAILIF_C (0 != TALER_amount_cmp (&balance,
    170                                    &expect),
    171             TALER_EXCHANGEDB_free_reserve_history (rh));
    172   FAILIF_C (1 != count_history (rh,
    173                                 TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE,
    174                                 &matched),
    175             TALER_EXCHANGEDB_free_reserve_history (rh));
    176   FAILIF_C (1 != matched,
    177             TALER_EXCHANGEDB_free_reserve_history (rh));
    178   FAILIF_C (0 != TALER_amount_cmp (&rh->details.bank->amount,
    179                                    &expect),
    180             TALER_EXCHANGEDB_free_reserve_history (rh));
    181   FAILIF_C (0 != strcmp (rh->details.bank->sender_account_details.full_payto,
    182                          account.payto.full_payto),
    183             TALER_EXCHANGEDB_free_reserve_history (rh));
    184   TALER_EXCHANGEDB_free_reserve_history (rh);
    185   return 0;
    186 }
    187 
    188 
    189 /**
    190  * A close request adds a second entry, and the offset filter hides the
    191  * older one.
    192  *
    193  * @param pg the database context
    194  * @return 0 on success
    195  */
    196 static int
    197 check_two_entries (struct TALER_EXCHANGEDB_PostgresContext *pg)
    198 {
    199   struct TALER_ReservePublicKeyP reserve_pub;
    200   struct TALER_ReserveSignatureP reserve_sig;
    201   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
    202   struct TALER_Amount balance;
    203   struct TALER_Amount close_balance = TDB_amount ("10");
    204   struct TALER_Amount close_fee = TDB_amount ("0.5");
    205   unsigned int matched;
    206   uint64_t etag_first;
    207   uint64_t etag_out = 0;
    208   uint64_t first_off;
    209 
    210   TDB_fill (&reserve_pub,
    211             sizeof (reserve_pub),
    212             10);
    213   TDB_FILL (reserve_sig,
    214             10);
    215   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    216           TALER_EXCHANGEDB_get_reserve_history (pg,
    217                                                 &reserve_pub,
    218                                                 0,
    219                                                 0,
    220                                                 &etag_first,
    221                                                 &balance,
    222                                                 &rh));
    223   first_off = rh->history_offset;
    224   TALER_EXCHANGEDB_free_reserve_history (rh);
    225   rh = NULL;
    226 
    227   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    228           TALER_EXCHANGEDB_insert_close_request (pg,
    229                                                  &reserve_pub,
    230                                                  account.payto,
    231                                                  &reserve_sig,
    232                                                  GNUNET_TIME_timestamp_get (),
    233                                                  &close_balance,
    234                                                  &close_fee));
    235   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    236           TALER_EXCHANGEDB_get_reserve_history (pg,
    237                                                 &reserve_pub,
    238                                                 0,
    239                                                 0,
    240                                                 &etag_out,
    241                                                 &balance,
    242                                                 &rh));
    243   FAILIF_C (2 != count_history (rh,
    244                                 TALER_EXCHANGEDB_RO_CLOSE_REQUEST,
    245                                 &matched),
    246             TALER_EXCHANGEDB_free_reserve_history (rh));
    247   FAILIF_C (1 != matched,
    248             TALER_EXCHANGEDB_free_reserve_history (rh));
    249   /* The query runs newest-first but the rows are walked back to front, so
    250      the list that comes out is oldest-first. */
    251   FAILIF_C (TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE != rh->type,
    252             TALER_EXCHANGEDB_free_reserve_history (rh));
    253   FAILIF_C (TALER_EXCHANGEDB_RO_CLOSE_REQUEST != rh->next->type,
    254             TALER_EXCHANGEDB_free_reserve_history (rh));
    255   FAILIF_C (rh->history_offset >= rh->next->history_offset,
    256             TALER_EXCHANGEDB_free_reserve_history (rh));
    257   FAILIF_C (etag_out <= etag_first,
    258             TALER_EXCHANGEDB_free_reserve_history (rh));
    259   TALER_EXCHANGEDB_free_reserve_history (rh);
    260   rh = NULL;
    261 
    262   /* starting past the bank transfer leaves only the close request */
    263   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    264           TALER_EXCHANGEDB_get_reserve_history (pg,
    265                                                 &reserve_pub,
    266                                                 first_off,
    267                                                 0,
    268                                                 &etag_out,
    269                                                 &balance,
    270                                                 &rh));
    271   FAILIF_C (1 != count_history (rh,
    272                                 TALER_EXCHANGEDB_RO_CLOSE_REQUEST,
    273                                 &matched),
    274             TALER_EXCHANGEDB_free_reserve_history (rh));
    275   FAILIF_C (1 != matched,
    276             TALER_EXCHANGEDB_free_reserve_history (rh));
    277   TALER_EXCHANGEDB_free_reserve_history (rh);
    278   rh = NULL;
    279 
    280   /* starting past everything leaves an empty history, but the reserve is
    281      still known and the ETag is still reported */
    282   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    283           TALER_EXCHANGEDB_get_reserve_history (pg,
    284                                                 &reserve_pub,
    285                                                 etag_out,
    286                                                 0,
    287                                                 &etag_out,
    288                                                 &balance,
    289                                                 &rh));
    290   FAILIF_C (NULL != rh,
    291             TALER_EXCHANGEDB_free_reserve_history (rh));
    292   return 0;
    293 }
    294 
    295 
    296 /**
    297  * Passing the current ETag short-circuits the lookup -- and must not leave
    298  * the transaction it opened behind.
    299  *
    300  * @param pg the database context
    301  * @return 0 on success
    302  */
    303 static int
    304 check_etag (struct TALER_EXCHANGEDB_PostgresContext *pg)
    305 {
    306   struct TALER_ReservePublicKeyP reserve_pub;
    307   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
    308   struct TALER_Amount balance;
    309   uint64_t etag_out = 0;
    310   uint64_t etag_again = 0;
    311 
    312   TDB_fill (&reserve_pub,
    313             sizeof (reserve_pub),
    314             10);
    315   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    316           TALER_EXCHANGEDB_get_reserve_history (pg,
    317                                                 &reserve_pub,
    318                                                 0,
    319                                                 0,
    320                                                 &etag_out,
    321                                                 &balance,
    322                                                 &rh));
    323   TALER_EXCHANGEDB_free_reserve_history (rh);
    324   rh = NULL;
    325 
    326   /* the client already has this version */
    327   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    328           TALER_EXCHANGEDB_get_reserve_history (pg,
    329                                                 &reserve_pub,
    330                                                 0,
    331                                                 etag_out,
    332                                                 &etag_again,
    333                                                 &balance,
    334                                                 &rh));
    335   FAILIF (etag_again != etag_out);
    336   FAILIF (NULL != rh);
    337 
    338   /* The short-circuit above returns early.  If it forgets to close the
    339      read-committed transaction it opened, the connection is parked in
    340      "idle in transaction" -- which the next TALER_EXCHANGEDB_start() would
    341      paper over, since its preflight rolls the stale transaction back and
    342      only logs it.  So check the bookkeeping preflight itself looks at. */
    343   FAILIF (NULL != pg->transaction_name);
    344   FAILIF (GNUNET_OK !=
    345           TALER_EXCHANGEDB_start (pg,
    346                                   "test-reserve-history-etag"));
    347   FAILIF (NULL == pg->transaction_name);
    348   TALER_EXCHANGEDB_rollback (pg);
    349   FAILIF (NULL != pg->transaction_name);
    350   return 0;
    351 }
    352 
    353 
    354 /**
    355  * The checks to run, in order.
    356  */
    357 static const struct TDB_Test tests[] = {
    358   { "reserve-history-unknown-reserve",
    359     &check_unknown_reserve },
    360   { "reserve-history-untouched-reserve",
    361     &check_untouched_reserve },
    362   { "reserve-history-bank-transfer",
    363     &check_bank_transfer },
    364   { "reserve-history-two-entries",
    365     &check_two_entries },
    366   { "reserve-history-etag",
    367     &check_etag },
    368   { NULL, NULL }
    369 };
    370 
    371 
    372 int
    373 main (int argc,
    374       char *const *argv)
    375 {
    376   int ret;
    377 
    378   ret = TDB_main (argc,
    379                   argv,
    380                   "test-reserve-history",
    381                   "Tests for the exchangedb `reserve_history' table",
    382                   tests);
    383   TDB_account_free (&account);
    384   return ret;
    385 }
    386 
    387 
    388 /* end of test_reserve_history.c */