LCOV - code coverage report
Current view: top level - exchangedb - test_reserve_history.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 125 125
Test Date: 2026-10-10 21:00:14 Functions: 100.0 % 8 8

            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/do_recoup.h"
      33              : #include "exchange-database/get_reserve_history.h"
      34              : #include "exchange-database/insert_close_request.h"
      35              : #include "exchange-database/start.h"
      36              : #include "exchange-database/rollback.h"
      37              : 
      38              : 
      39              : /**
      40              :  * Account the checks use.
      41              :  */
      42              : static struct TDB_Account account;
      43              : 
      44              : 
      45              : /**
      46              :  * Count the entries of @a rh, and how many of them have type @a type.
      47              :  *
      48              :  * @param rh history to walk
      49              :  * @param type operation to count
      50              :  * @param[out] matched set to the number of entries of that type
      51              :  * @return total number of entries
      52              :  */
      53              : static unsigned int
      54            4 : count_history (const struct TALER_EXCHANGEDB_ReserveHistory *rh,
      55              :                enum TALER_EXCHANGEDB_ReserveOperation type,
      56              :                unsigned int *matched)
      57              : {
      58            4 :   unsigned int total = 0;
      59              : 
      60            4 :   *matched = 0;
      61            4 :   for (const struct TALER_EXCHANGEDB_ReserveHistory *p = rh;
      62           11 :        NULL != p;
      63            7 :        p = p->next)
      64              :   {
      65            7 :     total++;
      66            7 :     if (type == p->type)
      67            4 :       (*matched)++;
      68              :   }
      69            4 :   return total;
      70              : }
      71              : 
      72              : 
      73              : /**
      74              :  * An unknown reserve has no history at all.
      75              :  *
      76              :  * @param pg the database context
      77              :  * @return 0 on success
      78              :  */
      79              : static int
      80            1 : check_unknown_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg)
      81              : {
      82              :   struct TALER_ReservePublicKeyP reserve_pub;
      83            1 :   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
      84              :   struct TALER_Amount balance;
      85            1 :   uint64_t etag_out = 42;
      86              : 
      87            1 :   TDB_FILL (reserve_pub,
      88              :             1);
      89            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
      90              :           TALER_EXCHANGEDB_get_reserve_history (pg,
      91              :                                                 &reserve_pub,
      92              :                                                 0,
      93              :                                                 0,
      94              :                                                 &etag_out,
      95              :                                                 &balance,
      96              :                                                 &rh));
      97            1 :   FAILIF (NULL != rh);
      98            1 :   return 0;
      99              : }
     100              : 
     101              : 
     102              : /**
     103              :  * A reserve that exists but was never touched has no history either: the
     104              :  * `reserves` row alone does not create an index entry.
     105              :  *
     106              :  * @param pg the database context
     107              :  * @return 0 on success
     108              :  */
     109              : static int
     110            1 : check_untouched_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg)
     111              : {
     112              :   struct TALER_ReservePublicKeyP reserve_pub;
     113            1 :   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
     114              :   struct TALER_Amount balance;
     115            1 :   uint64_t etag_out = 42;
     116              : 
     117            1 :   TDB_reserve (pg,
     118              :                2,
     119              :                "5",
     120              :                &reserve_pub);
     121            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     122              :           TALER_EXCHANGEDB_get_reserve_history (pg,
     123              :                                                 &reserve_pub,
     124              :                                                 0,
     125              :                                                 0,
     126              :                                                 &etag_out,
     127              :                                                 &balance,
     128              :                                                 &rh));
     129            1 :   FAILIF (NULL != rh);
     130            1 :   return 0;
     131              : }
     132              : 
     133              : 
     134              : /**
     135              :  * A bank transfer into the reserve shows up as one history entry.
     136              :  *
     137              :  * @param pg the database context
     138              :  * @return 0 on success
     139              :  */
     140              : static int
     141            1 : check_bank_transfer (struct TALER_EXCHANGEDB_PostgresContext *pg)
     142              : {
     143              :   struct TALER_ReservePublicKeyP reserve_pub;
     144            1 :   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
     145              :   struct TALER_Amount balance;
     146            1 :   struct TALER_Amount expect = TDB_amount ("10");
     147              :   unsigned int matched;
     148            1 :   uint64_t etag_out = 0;
     149              : 
     150            1 :   TDB_account (pg,
     151              :                10,
     152              :                &account);
     153            1 :   TDB_reserve_in (pg,
     154              :                   &account,
     155              :                   10,
     156              :                   "10",
     157              :                   &reserve_pub);
     158            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     159              :           TALER_EXCHANGEDB_get_reserve_history (pg,
     160              :                                                 &reserve_pub,
     161              :                                                 0,
     162              :                                                 0,
     163              :                                                 &etag_out,
     164              :                                                 &balance,
     165              :                                                 &rh));
     166            1 :   FAILIF_C (NULL == rh,
     167              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     168            1 :   FAILIF_C (0 == etag_out,
     169              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     170            1 :   FAILIF_C (0 != TALER_amount_cmp (&balance,
     171              :                                    &expect),
     172              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     173            1 :   FAILIF_C (1 != count_history (rh,
     174              :                                 TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE,
     175              :                                 &matched),
     176              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     177            1 :   FAILIF_C (1 != matched,
     178              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     179            1 :   FAILIF_C (0 != TALER_amount_cmp (&rh->details.bank->amount,
     180              :                                    &expect),
     181              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     182            1 :   FAILIF_C (0 != strcmp (rh->details.bank->sender_account_details.full_payto,
     183              :                          account.payto.full_payto),
     184              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     185            1 :   TALER_EXCHANGEDB_free_reserve_history (rh);
     186            1 :   return 0;
     187              : }
     188              : 
     189              : 
     190              : /**
     191              :  * A close request adds a second entry, and the offset filter hides the
     192              :  * older one.
     193              :  *
     194              :  * @param pg the database context
     195              :  * @return 0 on success
     196              :  */
     197              : static int
     198            1 : check_two_entries (struct TALER_EXCHANGEDB_PostgresContext *pg)
     199              : {
     200              :   struct TALER_ReservePublicKeyP reserve_pub;
     201              :   struct TALER_ReserveSignatureP reserve_sig;
     202            1 :   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
     203              :   struct TALER_Amount balance;
     204            1 :   struct TALER_Amount close_balance = TDB_amount ("10");
     205            1 :   struct TALER_Amount close_fee = TDB_amount ("0.5");
     206              :   unsigned int matched;
     207              :   uint64_t etag_first;
     208            1 :   uint64_t etag_out = 0;
     209              :   uint64_t first_off;
     210              : 
     211            1 :   TDB_fill (&reserve_pub,
     212              :             sizeof (reserve_pub),
     213              :             10);
     214            1 :   TDB_FILL (reserve_sig,
     215              :             10);
     216            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     217              :           TALER_EXCHANGEDB_get_reserve_history (pg,
     218              :                                                 &reserve_pub,
     219              :                                                 0,
     220              :                                                 0,
     221              :                                                 &etag_first,
     222              :                                                 &balance,
     223              :                                                 &rh));
     224            1 :   first_off = rh->history_offset;
     225            1 :   TALER_EXCHANGEDB_free_reserve_history (rh);
     226            1 :   rh = NULL;
     227              : 
     228            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     229              :           TALER_EXCHANGEDB_insert_close_request (pg,
     230              :                                                  &reserve_pub,
     231              :                                                  account.payto,
     232              :                                                  &reserve_sig,
     233              :                                                  GNUNET_TIME_timestamp_get (),
     234              :                                                  &close_balance,
     235              :                                                  &close_fee));
     236            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     237              :           TALER_EXCHANGEDB_get_reserve_history (pg,
     238              :                                                 &reserve_pub,
     239              :                                                 0,
     240              :                                                 0,
     241              :                                                 &etag_out,
     242              :                                                 &balance,
     243              :                                                 &rh));
     244            1 :   FAILIF_C (2 != count_history (rh,
     245              :                                 TALER_EXCHANGEDB_RO_CLOSE_REQUEST,
     246              :                                 &matched),
     247              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     248            1 :   FAILIF_C (1 != matched,
     249              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     250              :   /* The query runs newest-first but the rows are walked back to front, so
     251              :      the list that comes out is oldest-first. */
     252            1 :   FAILIF_C (TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE != rh->type,
     253              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     254            1 :   FAILIF_C (TALER_EXCHANGEDB_RO_CLOSE_REQUEST != rh->next->type,
     255              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     256            1 :   FAILIF_C (rh->history_offset >= rh->next->history_offset,
     257              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     258            1 :   FAILIF_C (etag_out <= etag_first,
     259              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     260            1 :   TALER_EXCHANGEDB_free_reserve_history (rh);
     261            1 :   rh = NULL;
     262              : 
     263              :   /* starting past the bank transfer leaves only the close request */
     264            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     265              :           TALER_EXCHANGEDB_get_reserve_history (pg,
     266              :                                                 &reserve_pub,
     267              :                                                 first_off,
     268              :                                                 0,
     269              :                                                 &etag_out,
     270              :                                                 &balance,
     271              :                                                 &rh));
     272            1 :   FAILIF_C (1 != count_history (rh,
     273              :                                 TALER_EXCHANGEDB_RO_CLOSE_REQUEST,
     274              :                                 &matched),
     275              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     276            1 :   FAILIF_C (1 != matched,
     277              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     278            1 :   TALER_EXCHANGEDB_free_reserve_history (rh);
     279            1 :   rh = NULL;
     280              : 
     281              :   /* starting past everything leaves an empty history, but the reserve is
     282              :      still known and the ETag is still reported */
     283            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     284              :           TALER_EXCHANGEDB_get_reserve_history (pg,
     285              :                                                 &reserve_pub,
     286              :                                                 etag_out,
     287              :                                                 0,
     288              :                                                 &etag_out,
     289              :                                                 &balance,
     290              :                                                 &rh));
     291            1 :   FAILIF_C (NULL != rh,
     292              :             TALER_EXCHANGEDB_free_reserve_history (rh));
     293            1 :   return 0;
     294              : }
     295              : 
     296              : 
     297              : /**
     298              :  * A recoup shows up as a RECOUP entry that names the coin, the amount
     299              :  * and the withdraw operation the coin came from.
     300              :  *
     301              :  * @param pg the database context
     302              :  * @return 0 on success
     303              :  */
     304              : static int
     305            1 : check_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg)
     306              : {
     307              :   struct TDB_Denom denom;
     308              :   struct TALER_CoinPublicInfo coin;
     309              :   struct TALER_ReservePublicKeyP reserve_pub;
     310              :   struct TALER_HashBlindedPlanchetsP planchets_h;
     311              :   struct TALER_CoinSpendSignatureP coin_sig;
     312              :   union GNUNET_CRYPTO_BlindingSecretP coin_bks;
     313              :   struct GNUNET_TIME_Timestamp recoup_timestamp;
     314              :   struct TALER_Amount recoup_amount;
     315            1 :   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
     316              :   struct TALER_Amount balance;
     317            1 :   struct TALER_Amount expect_value = TDB_amount ("5");
     318            1 :   struct TALER_Amount expect_balance = TDB_amount ("10");
     319            1 :   const struct TALER_EXCHANGEDB_Recoup *recoup = NULL;
     320            1 :   unsigned int matched = 0;
     321              :   uint64_t withdraw_id;
     322              :   uint64_t known_coin_id;
     323            1 :   uint64_t etag = 0;
     324              :   bool recoup_ok;
     325              :   bool internal_failure;
     326              : 
     327            1 :   TDB_denom (pg,
     328              :              30,
     329              :              "5",
     330              :              "0.1",
     331              :              &denom);
     332            1 :   TDB_reserve_in (pg,
     333              :                   &account,
     334              :                   30,
     335              :                   "10",
     336              :                   &reserve_pub);
     337            1 :   withdraw_id = TDB_withdraw (pg,
     338              :                               &denom,
     339              :                               &reserve_pub,
     340              :                               30,
     341              :                               "5");
     342              :   /* TDB_withdraw() seeds the commitment with its seed */
     343            1 :   TDB_FILL (planchets_h,
     344              :             30);
     345            1 :   TDB_coin (pg,
     346              :             &denom,
     347              :             31,
     348              :             &coin,
     349              :             &known_coin_id);
     350            1 :   TDB_FILL (coin_sig,
     351              :             31);
     352            1 :   TDB_FILL (coin_bks,
     353              :             31);
     354            1 :   recoup_timestamp = GNUNET_TIME_timestamp_get ();
     355            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     356              :             TALER_EXCHANGEDB_do_recoup (pg,
     357              :                                         &reserve_pub,
     358              :                                         withdraw_id,
     359              :                                         2,
     360              :                                         &coin_bks,
     361              :                                         &coin.coin_pub,
     362              :                                         known_coin_id,
     363              :                                         &coin_sig,
     364              :                                         &recoup_timestamp,
     365              :                                         &recoup_amount,
     366              :                                         &recoup_ok,
     367              :                                         &internal_failure),
     368              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     369            1 :   FAILIF_C (internal_failure || ! recoup_ok,
     370              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     371            1 :   FAILIF_C (0 != TALER_amount_cmp (&recoup_amount,
     372              :                                    &expect_value),
     373              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     374            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     375              :             TALER_EXCHANGEDB_get_reserve_history (pg,
     376              :                                                   &reserve_pub,
     377              :                                                   0,
     378              :                                                   0,
     379              :                                                   &etag,
     380              :                                                   &balance,
     381              :                                                   &rh),
     382              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     383              :   /* bank transfer in, withdraw, recoup */
     384            1 :   FAILIF_C (3 != count_history (rh,
     385              :                                 TALER_EXCHANGEDB_RO_RECOUP_COIN,
     386              :                                 &matched),
     387              :             TALER_EXCHANGEDB_free_reserve_history (rh);
     388              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     389            1 :   FAILIF_C (1 != matched,
     390              :             TALER_EXCHANGEDB_free_reserve_history (rh);
     391              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     392            1 :   for (const struct TALER_EXCHANGEDB_ReserveHistory *p = rh;
     393            4 :        NULL != p;
     394            3 :        p = p->next)
     395            3 :     if (TALER_EXCHANGEDB_RO_RECOUP_COIN == p->type)
     396            1 :       recoup = p->details.recoup;
     397            1 :   FAILIF_C (0 != GNUNET_memcmp (&recoup->coin.coin_pub,
     398              :                                 &coin.coin_pub),
     399              :             TALER_EXCHANGEDB_free_reserve_history (rh);
     400              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     401            1 :   FAILIF_C (0 != GNUNET_memcmp (&recoup->planchets_h,
     402              :                                 &planchets_h),
     403              :             TALER_EXCHANGEDB_free_reserve_history (rh);
     404              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     405            1 :   FAILIF_C (0 != GNUNET_memcmp (&recoup->reserve_pub,
     406              :                                 &reserve_pub),
     407              :             TALER_EXCHANGEDB_free_reserve_history (rh);
     408              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     409            1 :   FAILIF_C (0 != TALER_amount_cmp (&recoup->value,
     410              :                                    &expect_value),
     411              :             TALER_EXCHANGEDB_free_reserve_history (rh);
     412              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     413              :   /* 10 in, 5 withdrawn, 5 recouped */
     414            1 :   FAILIF_C (0 != TALER_amount_cmp (&balance,
     415              :                                    &expect_balance),
     416              :             TALER_EXCHANGEDB_free_reserve_history (rh);
     417              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     418            1 :   TALER_EXCHANGEDB_free_reserve_history (rh);
     419            1 :   TDB_coin_free (&coin);
     420            1 :   TDB_denom_free (&denom);
     421            1 :   return 0;
     422              : }
     423              : 
     424              : 
     425              : /**
     426              :  * Passing the current ETag short-circuits the lookup -- and must not leave
     427              :  * the transaction it opened behind.
     428              :  *
     429              :  * @param pg the database context
     430              :  * @return 0 on success
     431              :  */
     432              : static int
     433            1 : check_etag (struct TALER_EXCHANGEDB_PostgresContext *pg)
     434              : {
     435              :   struct TALER_ReservePublicKeyP reserve_pub;
     436            1 :   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
     437              :   struct TALER_Amount balance;
     438            1 :   uint64_t etag_out = 0;
     439            1 :   uint64_t etag_again = 0;
     440              : 
     441            1 :   TDB_fill (&reserve_pub,
     442              :             sizeof (reserve_pub),
     443              :             10);
     444            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     445              :           TALER_EXCHANGEDB_get_reserve_history (pg,
     446              :                                                 &reserve_pub,
     447              :                                                 0,
     448              :                                                 0,
     449              :                                                 &etag_out,
     450              :                                                 &balance,
     451              :                                                 &rh));
     452            1 :   TALER_EXCHANGEDB_free_reserve_history (rh);
     453            1 :   rh = NULL;
     454              : 
     455              :   /* the client already has this version */
     456            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     457              :           TALER_EXCHANGEDB_get_reserve_history (pg,
     458              :                                                 &reserve_pub,
     459              :                                                 0,
     460              :                                                 etag_out,
     461              :                                                 &etag_again,
     462              :                                                 &balance,
     463              :                                                 &rh));
     464            1 :   FAILIF (etag_again != etag_out);
     465            1 :   FAILIF (NULL != rh);
     466              : 
     467              :   /* The short-circuit above returns early.  If it forgets to close the
     468              :      read-committed transaction it opened, the connection is parked in
     469              :      "idle in transaction" -- which the next TALER_EXCHANGEDB_start() would
     470              :      paper over, since its preflight rolls the stale transaction back and
     471              :      only logs it.  So check the bookkeeping preflight itself looks at. */
     472            1 :   FAILIF (NULL != pg->transaction_name);
     473            1 :   FAILIF (GNUNET_OK !=
     474              :           TALER_EXCHANGEDB_start (pg,
     475              :                                   "test-reserve-history-etag"));
     476            1 :   FAILIF (NULL == pg->transaction_name);
     477            1 :   TALER_EXCHANGEDB_rollback (pg);
     478            1 :   FAILIF (NULL != pg->transaction_name);
     479            1 :   return 0;
     480              : }
     481              : 
     482              : 
     483              : /**
     484              :  * The checks to run, in order.
     485              :  */
     486              : static const struct TDB_Test tests[] = {
     487              :   { "reserve-history-unknown-reserve",
     488              :     &check_unknown_reserve },
     489              :   { "reserve-history-untouched-reserve",
     490              :     &check_untouched_reserve },
     491              :   { "reserve-history-bank-transfer",
     492              :     &check_bank_transfer },
     493              :   { "reserve-history-two-entries",
     494              :     &check_two_entries },
     495              :   { "reserve-history-recoup",
     496              :     &check_recoup },
     497              :   { "reserve-history-etag",
     498              :     &check_etag },
     499              :   { NULL, NULL }
     500              : };
     501              : 
     502              : 
     503              : int
     504            1 : main (int argc,
     505              :       char *const *argv)
     506              : {
     507              :   int ret;
     508              : 
     509            1 :   ret = TDB_main (argc,
     510              :                   argv,
     511              :                   "test-reserve-history",
     512              :                   "Tests for the exchangedb `reserve_history' table",
     513              :                   tests);
     514            1 :   TDB_account_free (&account);
     515            1 :   return ret;
     516              : }
     517              : 
     518              : 
     519              : /* end of test_reserve_history.c */
        

Generated by: LCOV version 2.0-1