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

            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_reserves_open_deposits.c
      18              :  * @brief tests for the exchangedb functions whose primary table is
      19              :  *        `reserves_open_deposits`
      20              :  * @author Christian Grothoff
      21              :  *
      22              :  * Covers #TALER_EXCHANGEDB_insert_reserve_open_deposit(), the only
      23              :  * exported function that touches the table.  The rows are read back by the
      24              :  * coin history, which test_coin_history.c covers.
      25              :  *
      26              :  * The table's primary key is (coin_pub, coin_sig), which is what makes a
      27              :  * replayed request idempotent.  On insufficient coin balance the row is
      28              :  * inserted first and the balance check fails afterwards, so the caller has
      29              :  * to roll back -- the check below does exactly that, since that is how the
      30              :  * /reserves/$RP/open handler uses the function.
      31              :  */
      32              : #include "test_common.h"
      33              : #include "exchange-database/commit.h"
      34              : #include "exchange-database/get_known_coin.h"
      35              : #include "exchange-database/insert_reserve_open_deposit.h"
      36              : #include "exchange-database/rollback.h"
      37              : #include "exchange-database/start.h"
      38              : 
      39              : 
      40              : /**
      41              :  * Read the remaining balance of a coin.
      42              :  *
      43              :  * @param pg the database context
      44              :  * @param coin_pub coin to look up
      45              :  * @param[out] remaining set to the coin's remaining value
      46              :  */
      47              : static void
      48            4 : coin_remaining (struct TALER_EXCHANGEDB_PostgresContext *pg,
      49              :                 const struct TALER_CoinSpendPublicKeyP *coin_pub,
      50              :                 struct TALER_Amount *remaining)
      51              : {
      52            4 :   struct GNUNET_PQ_QueryParam params[] = {
      53            4 :     GNUNET_PQ_query_param_auto_from_type (coin_pub),
      54              :     GNUNET_PQ_query_param_end
      55              :   };
      56            4 :   struct GNUNET_PQ_ResultSpec rs[] = {
      57            4 :     TALER_PQ_result_spec_amount ("remaining",
      58            4 :                                  pg->currency,
      59              :                                  remaining),
      60              :     GNUNET_PQ_result_spec_end
      61              :   };
      62              : 
      63            4 :   GNUNET_assert (GNUNET_OK ==
      64              :                  GNUNET_PQ_prepare_anon (pg->conn,
      65              :                                          "SELECT remaining"
      66              :                                          " FROM known_coins"
      67              :                                          " WHERE coin_pub=$1;"));
      68            4 :   GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
      69              :                  GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
      70              :                                                            "",
      71              :                                                            params,
      72              :                                                            rs));
      73            4 : }
      74              : 
      75              : 
      76              : /**
      77              :  * Spend part of a coin on keeping a reserve open.
      78              :  *
      79              :  * @param pg the database context
      80              :  * @param coin the coin to spend from
      81              :  * @param known_coin_id row of @a coin
      82              :  * @param reserve_pub reserve being kept open
      83              :  * @param seed seed for the coin and reserve signatures
      84              :  * @param amount how much of the coin to spend, e.g. "1"
      85              :  * @param[out] insufficient_funds set if the coin does not have that much
      86              :  * @return transaction status
      87              :  */
      88              : static enum GNUNET_DB_QueryStatus
      89            5 : open_deposit (struct TALER_EXCHANGEDB_PostgresContext *pg,
      90              :               const struct TALER_CoinPublicInfo *coin,
      91              :               uint64_t known_coin_id,
      92              :               const struct TALER_ReservePublicKeyP *reserve_pub,
      93              :               uint32_t seed,
      94              :               const char *amount,
      95              :               bool *insufficient_funds)
      96              : {
      97              :   struct TALER_CoinSpendSignatureP coin_sig;
      98              :   struct TALER_ReserveSignatureP reserve_sig;
      99            5 :   struct TALER_Amount a = TDB_amount (amount);
     100              : 
     101            5 :   TDB_fill (&coin_sig,
     102              :             sizeof (coin_sig),
     103              :             seed);
     104            5 :   TDB_fill (&reserve_sig,
     105              :             sizeof (reserve_sig),
     106              :             seed);
     107            5 :   return TALER_EXCHANGEDB_insert_reserve_open_deposit (pg,
     108              :                                                        coin,
     109              :                                                        &coin_sig,
     110              :                                                        known_coin_id,
     111              :                                                        &a,
     112              :                                                        &reserve_sig,
     113              :                                                        reserve_pub,
     114              :                                                        insufficient_funds);
     115              : }
     116              : 
     117              : 
     118              : /**
     119              :  * Spending part of a coin records the deposit and debits the coin.
     120              :  *
     121              :  * @param pg the database context
     122              :  * @return 0 on success
     123              :  */
     124              : static int
     125            1 : check_deposit (struct TALER_EXCHANGEDB_PostgresContext *pg)
     126              : {
     127              :   struct TDB_Denom denom;
     128              :   struct TALER_CoinPublicInfo coin;
     129              :   struct TALER_ReservePublicKeyP reserve_pub;
     130              :   struct TALER_Amount remaining;
     131            1 :   struct TALER_Amount expect = TDB_amount ("4");
     132              :   uint64_t known_coin_id;
     133            1 :   bool insufficient = true;
     134              : 
     135            1 :   FAILIF (0 != TDB_count (pg,
     136              :                           "FROM reserves_open_deposits"));
     137            1 :   TDB_denom (pg,
     138              :              10,
     139              :              "5",
     140              :              "0.1",
     141              :              &denom);
     142            1 :   TDB_coin (pg,
     143              :             &denom,
     144              :             10,
     145              :             &coin,
     146              :             &known_coin_id);
     147            1 :   TDB_reserve (pg,
     148              :                10,
     149              :                "0",
     150              :                &reserve_pub);
     151              : 
     152            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     153              :             open_deposit (pg,
     154              :                           &coin,
     155              :                           known_coin_id,
     156              :                           &reserve_pub,
     157              :                           10,
     158              :                           "1",
     159              :                           &insufficient),
     160              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     161            1 :   FAILIF_C (insufficient,
     162              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     163            1 :   FAILIF_C (1 != TDB_count (pg,
     164              :                             "FROM reserves_open_deposits"),
     165              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     166            1 :   coin_remaining (pg,
     167              :                   &coin.coin_pub,
     168              :                   &remaining);
     169            1 :   FAILIF_C (0 != TALER_amount_cmp (&remaining,
     170              :                                    &expect),
     171              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     172              : 
     173              :   /* a replay of the very same request is absorbed and does not debit the
     174              :      coin a second time */
     175            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     176              :             open_deposit (pg,
     177              :                           &coin,
     178              :                           known_coin_id,
     179              :                           &reserve_pub,
     180              :                           10,
     181              :                           "1",
     182              :                           &insufficient),
     183              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     184            1 :   FAILIF_C (insufficient,
     185              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     186            1 :   FAILIF_C (1 != TDB_count (pg,
     187              :                             "FROM reserves_open_deposits"),
     188              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     189            1 :   coin_remaining (pg,
     190              :                   &coin.coin_pub,
     191              :                   &remaining);
     192            1 :   FAILIF_C (0 != TALER_amount_cmp (&remaining,
     193              :                                    &expect),
     194              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     195            1 :   TDB_coin_free (&coin);
     196            1 :   TDB_denom_free (&denom);
     197            1 :   return 0;
     198              : }
     199              : 
     200              : 
     201              : /**
     202              :  * A second, larger deposit from the same coin is refused, and the caller
     203              :  * has to roll back to undo the row the function inserted first.
     204              :  *
     205              :  * @param pg the database context
     206              :  * @return 0 on success
     207              :  */
     208              : static int
     209            1 : check_insufficient_funds (struct TALER_EXCHANGEDB_PostgresContext *pg)
     210              : {
     211              :   struct TDB_Denom denom;
     212              :   struct TALER_CoinPublicInfo coin;
     213              :   struct TALER_ReservePublicKeyP reserve_pub;
     214              :   struct TALER_Amount remaining;
     215            1 :   struct TALER_Amount expect = TDB_amount ("4");
     216              :   uint64_t known_coin_id;
     217            1 :   bool insufficient = false;
     218              : 
     219            1 :   TDB_denom (pg,
     220              :              10,
     221              :              "5",
     222              :              "0.1",
     223              :              &denom);
     224            1 :   TDB_coin (pg,
     225              :             &denom,
     226              :             10,
     227              :             &coin,
     228              :             &known_coin_id);
     229            1 :   TDB_reserve (pg,
     230              :                10,
     231              :                "0",
     232              :                &reserve_pub);
     233              : 
     234            1 :   FAILIF_C (GNUNET_OK !=
     235              :             TALER_EXCHANGEDB_start (pg,
     236              :                                     "test-open-deposit-insufficient"),
     237              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     238              :   /* EUR:4 is left, so EUR:5 cannot be taken */
     239            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     240              :             open_deposit (pg,
     241              :                           &coin,
     242              :                           known_coin_id,
     243              :                           &reserve_pub,
     244              :                           11,
     245              :                           "5",
     246              :                           &insufficient),
     247              :             TALER_EXCHANGEDB_rollback (pg);
     248              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     249            1 :   FAILIF_C (! insufficient,
     250              :             TALER_EXCHANGEDB_rollback (pg);
     251              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     252            1 :   TALER_EXCHANGEDB_rollback (pg);
     253              :   /* the rollback took the row with it */
     254            1 :   FAILIF_C (1 != TDB_count (pg,
     255              :                             "FROM reserves_open_deposits"),
     256              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     257            1 :   coin_remaining (pg,
     258              :                   &coin.coin_pub,
     259              :                   &remaining);
     260            1 :   FAILIF_C (0 != TALER_amount_cmp (&remaining,
     261              :                                    &expect),
     262              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     263            1 :   TDB_coin_free (&coin);
     264            1 :   TDB_denom_free (&denom);
     265            1 :   return 0;
     266              : }
     267              : 
     268              : 
     269              : /**
     270              :  * Spending exactly what is left works and leaves the coin at zero.
     271              :  *
     272              :  * @param pg the database context
     273              :  * @return 0 on success
     274              :  */
     275              : static int
     276            1 : check_exact_balance (struct TALER_EXCHANGEDB_PostgresContext *pg)
     277              : {
     278              :   struct TDB_Denom denom;
     279              :   struct TALER_CoinPublicInfo coin;
     280              :   struct TALER_ReservePublicKeyP reserve_pub;
     281              :   struct TALER_Amount remaining;
     282            1 :   struct TALER_Amount zero = TDB_amount ("0");
     283              :   uint64_t known_coin_id;
     284            1 :   bool insufficient = true;
     285              : 
     286            1 :   TDB_denom (pg,
     287              :              10,
     288              :              "5",
     289              :              "0.1",
     290              :              &denom);
     291            1 :   TDB_coin (pg,
     292              :             &denom,
     293              :             10,
     294              :             &coin,
     295              :             &known_coin_id);
     296            1 :   TDB_reserve (pg,
     297              :                10,
     298              :                "0",
     299              :                &reserve_pub);
     300            1 :   FAILIF_C (GNUNET_OK !=
     301              :             TALER_EXCHANGEDB_start (pg,
     302              :                                     "test-open-deposit-exact"),
     303              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     304            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     305              :             open_deposit (pg,
     306              :                           &coin,
     307              :                           known_coin_id,
     308              :                           &reserve_pub,
     309              :                           12,
     310              :                           "4",
     311              :                           &insufficient),
     312              :             TALER_EXCHANGEDB_rollback (pg);
     313              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     314            1 :   FAILIF_C (insufficient,
     315              :             TALER_EXCHANGEDB_rollback (pg);
     316              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     317            1 :   FAILIF_C (0 >
     318              :             TALER_EXCHANGEDB_commit (pg),
     319              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     320            1 :   coin_remaining (pg,
     321              :                   &coin.coin_pub,
     322              :                   &remaining);
     323            1 :   FAILIF_C (0 != TALER_amount_cmp (&remaining,
     324              :                                    &zero),
     325              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     326            1 :   FAILIF_C (2 != TDB_count (pg,
     327              :                             "FROM reserves_open_deposits"),
     328              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     329              :   /* and now even the smallest further deposit is refused */
     330            1 :   insufficient = false;
     331            1 :   FAILIF_C (GNUNET_OK !=
     332              :             TALER_EXCHANGEDB_start (pg,
     333              :                                     "test-open-deposit-drained"),
     334              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     335            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     336              :             open_deposit (pg,
     337              :                           &coin,
     338              :                           known_coin_id,
     339              :                           &reserve_pub,
     340              :                           13,
     341              :                           "0.01",
     342              :                           &insufficient),
     343              :             TALER_EXCHANGEDB_rollback (pg);
     344              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     345            1 :   FAILIF_C (! insufficient,
     346              :             TALER_EXCHANGEDB_rollback (pg);
     347              :             TDB_coin_free (&coin); TDB_denom_free (&denom));
     348            1 :   TALER_EXCHANGEDB_rollback (pg);
     349            1 :   TDB_coin_free (&coin);
     350            1 :   TDB_denom_free (&denom);
     351            1 :   return 0;
     352              : }
     353              : 
     354              : 
     355              : /**
     356              :  * The checks to run, in order.
     357              :  */
     358              : static const struct TDB_Test tests[] = {
     359              :   { "reserves-open-deposits-deposit",
     360              :     &check_deposit },
     361              :   { "reserves-open-deposits-insufficient-funds",
     362              :     &check_insufficient_funds },
     363              :   { "reserves-open-deposits-exact-balance",
     364              :     &check_exact_balance },
     365              :   { NULL, NULL }
     366              : };
     367              : 
     368              : 
     369              : int
     370            1 : main (int argc,
     371              :       char *const *argv)
     372              : {
     373            1 :   return TDB_main (argc,
     374              :                    argv,
     375              :                    "test-reserves-open-deposits",
     376              :                    "Tests for the exchangedb `reserves_open_deposits' table",
     377              :                    tests);
     378              : }
     379              : 
     380              : 
     381              : /* end of test_reserves_open_deposits.c */
        

Generated by: LCOV version 2.0-1