LCOV - code coverage report
Current view: top level - exchangedb - test_reserve_history.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 90 90
Test Date: 2026-09-11 18:55:36 Functions: 100.0 % 7 7

            Line data    Source code
       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            3 : count_history (const struct TALER_EXCHANGEDB_ReserveHistory *rh,
      54              :                enum TALER_EXCHANGEDB_ReserveOperation type,
      55              :                unsigned int *matched)
      56              : {
      57            3 :   unsigned int total = 0;
      58              : 
      59            3 :   *matched = 0;
      60            3 :   for (const struct TALER_EXCHANGEDB_ReserveHistory *p = rh;
      61            7 :        NULL != p;
      62            4 :        p = p->next)
      63              :   {
      64            4 :     total++;
      65            4 :     if (type == p->type)
      66            3 :       (*matched)++;
      67              :   }
      68            3 :   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            1 : check_unknown_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg)
      80              : {
      81              :   struct TALER_ReservePublicKeyP reserve_pub;
      82            1 :   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
      83              :   struct TALER_Amount balance;
      84            1 :   uint64_t etag_out = 42;
      85              : 
      86            1 :   TDB_FILL (reserve_pub,
      87              :             1);
      88            1 :   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            1 :   FAILIF (NULL != rh);
      97            1 :   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            1 : check_untouched_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg)
     110              : {
     111              :   struct TALER_ReservePublicKeyP reserve_pub;
     112            1 :   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
     113              :   struct TALER_Amount balance;
     114            1 :   uint64_t etag_out = 42;
     115              : 
     116            1 :   TDB_reserve (pg,
     117              :                2,
     118              :                "5",
     119              :                &reserve_pub);
     120            1 :   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            1 :   FAILIF (NULL != rh);
     129            1 :   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            1 : check_bank_transfer (struct TALER_EXCHANGEDB_PostgresContext *pg)
     141              : {
     142              :   struct TALER_ReservePublicKeyP reserve_pub;
     143            1 :   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
     144              :   struct TALER_Amount balance;
     145            1 :   struct TALER_Amount expect = TDB_amount ("10");
     146              :   unsigned int matched;
     147            1 :   uint64_t etag_out = 0;
     148              : 
     149            1 :   TDB_account (pg,
     150              :                10,
     151              :                &account);
     152            1 :   TDB_reserve_in (pg,
     153              :                   &account,
     154              :                   10,
     155              :                   "10",
     156              :                   &reserve_pub);
     157            1 :   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            1 :   FAILIF_C (NULL == rh,
     166              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     167            1 :   FAILIF_C (0 == etag_out,
     168              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     169            1 :   FAILIF_C (0 != TALER_amount_cmp (&balance,
     170              :                                    &expect),
     171              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     172            1 :   FAILIF_C (1 != count_history (rh,
     173              :                                 TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE,
     174              :                                 &matched),
     175              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     176            1 :   FAILIF_C (1 != matched,
     177              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     178            1 :   FAILIF_C (0 != TALER_amount_cmp (&rh->details.bank->amount,
     179              :                                    &expect),
     180              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     181            1 :   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            1 :   TALER_EXCHANGEDB_free_reserve_history (rh);
     185            1 :   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            1 : check_two_entries (struct TALER_EXCHANGEDB_PostgresContext *pg)
     198              : {
     199              :   struct TALER_ReservePublicKeyP reserve_pub;
     200              :   struct TALER_ReserveSignatureP reserve_sig;
     201            1 :   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
     202              :   struct TALER_Amount balance;
     203            1 :   struct TALER_Amount close_balance = TDB_amount ("10");
     204            1 :   struct TALER_Amount close_fee = TDB_amount ("0.5");
     205              :   unsigned int matched;
     206              :   uint64_t etag_first;
     207            1 :   uint64_t etag_out = 0;
     208              :   uint64_t first_off;
     209              : 
     210            1 :   TDB_fill (&reserve_pub,
     211              :             sizeof (reserve_pub),
     212              :             10);
     213            1 :   TDB_FILL (reserve_sig,
     214              :             10);
     215            1 :   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            1 :   first_off = rh->history_offset;
     224            1 :   TALER_EXCHANGEDB_free_reserve_history (rh);
     225            1 :   rh = NULL;
     226              : 
     227            1 :   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            1 :   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            1 :   FAILIF_C (2 != count_history (rh,
     244              :                                 TALER_EXCHANGEDB_RO_CLOSE_REQUEST,
     245              :                                 &matched),
     246              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     247            1 :   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            1 :   FAILIF_C (TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE != rh->type,
     252              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     253            1 :   FAILIF_C (TALER_EXCHANGEDB_RO_CLOSE_REQUEST != rh->next->type,
     254              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     255            1 :   FAILIF_C (rh->history_offset >= rh->next->history_offset,
     256              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     257            1 :   FAILIF_C (etag_out <= etag_first,
     258              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     259            1 :   TALER_EXCHANGEDB_free_reserve_history (rh);
     260            1 :   rh = NULL;
     261              : 
     262              :   /* starting past the bank transfer leaves only the close request */
     263            1 :   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            1 :   FAILIF_C (1 != count_history (rh,
     272              :                                 TALER_EXCHANGEDB_RO_CLOSE_REQUEST,
     273              :                                 &matched),
     274              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     275            1 :   FAILIF_C (1 != matched,
     276              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     277            1 :   TALER_EXCHANGEDB_free_reserve_history (rh);
     278            1 :   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            1 :   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            1 :   FAILIF_C (NULL != rh,
     291              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     292            1 :   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            1 : check_etag (struct TALER_EXCHANGEDB_PostgresContext *pg)
     305              : {
     306              :   struct TALER_ReservePublicKeyP reserve_pub;
     307            1 :   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
     308              :   struct TALER_Amount balance;
     309            1 :   uint64_t etag_out = 0;
     310            1 :   uint64_t etag_again = 0;
     311              : 
     312            1 :   TDB_fill (&reserve_pub,
     313              :             sizeof (reserve_pub),
     314              :             10);
     315            1 :   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            1 :   TALER_EXCHANGEDB_free_reserve_history (rh);
     324            1 :   rh = NULL;
     325              : 
     326              :   /* the client already has this version */
     327            1 :   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            1 :   FAILIF (etag_again != etag_out);
     336            1 :   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            1 :   FAILIF (NULL != pg->transaction_name);
     344            1 :   FAILIF (GNUNET_OK !=
     345              :           TALER_EXCHANGEDB_start (pg,
     346              :                                   "test-reserve-history-etag"));
     347            1 :   FAILIF (NULL == pg->transaction_name);
     348            1 :   TALER_EXCHANGEDB_rollback (pg);
     349            1 :   FAILIF (NULL != pg->transaction_name);
     350            1 :   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            1 : main (int argc,
     374              :       char *const *argv)
     375              : {
     376              :   int ret;
     377              : 
     378            1 :   ret = TDB_main (argc,
     379              :                   argv,
     380              :                   "test-reserve-history",
     381              :                   "Tests for the exchangedb `reserve_history' table",
     382              :                   tests);
     383            1 :   TDB_account_free (&account);
     384            1 :   return ret;
     385              : }
     386              : 
     387              : 
     388              : /* end of test_reserve_history.c */
        

Generated by: LCOV version 2.0-1