LCOV - code coverage report
Current view: top level - exchangedb - test_purse_decision.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 97.6 % 84 82
Test Date: 2026-09-11 18:55:36 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_purse_decision.c
      18              :  * @brief tests for the exchangedb functions whose primary table is
      19              :  *        `purse_decision`
      20              :  * @author Christian Grothoff
      21              :  *
      22              :  * Covers #TALER_EXCHANGEDB_do_expire_purse(),
      23              :  * #TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id() and
      24              :  * #TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id().
      25              :  *
      26              :  * A purse is decided either because it was merged or because it expired.
      27              :  * do_expire_purse() handles the second case: it decides the oldest expired
      28              :  * purse in the window it is given, marks the decision as a refund and
      29              :  * gives the coins their money back -- which is what the checks look at.
      30              :  */
      31              : #include "test_common.h"
      32              : #include "exchange-database/do_expire_purse.h"
      33              : #include "exchange-database/do_purse_deposit.h"
      34              : #include "exchange-database/iterate_all_purse_decisions_above_serial_id.h"
      35              : #include "exchange-database/iterate_purse_decisions_above_serial_id.h"
      36              : 
      37              : 
      38              : /**
      39              :  * Denomination the checks deposit into purses.
      40              :  */
      41              : static struct TDB_Denom denom;
      42              : 
      43              : 
      44              : /**
      45              :  * Build a timestamp from a number of seconds since the epoch.
      46              :  *
      47              :  * @param secs seconds since the epoch
      48              :  * @return the timestamp
      49              :  */
      50              : static struct GNUNET_TIME_Timestamp
      51            5 : ts (uint64_t secs)
      52              : {
      53            5 :   struct GNUNET_TIME_Absolute abs = {
      54            5 :     .abs_value_us = secs * 1000LLU * 1000LLU
      55              :   };
      56              : 
      57            5 :   return GNUNET_TIME_absolute_to_timestamp (abs);
      58              : }
      59              : 
      60              : 
      61              : /**
      62              :  * Closure for #decision_cb() and #all_decision_cb().
      63              :  */
      64              : struct DecisionContext
      65              : {
      66              :   /**
      67              :    * How many rows did the callback see?
      68              :    */
      69              :   unsigned int total;
      70              : 
      71              :   /**
      72              :    * Stop after this many rows; 0 for no limit.
      73              :    */
      74              :   unsigned int stop_after;
      75              : 
      76              :   /**
      77              :    * Purse we are looking for, NULL to match nothing.
      78              :    */
      79              :   const struct TALER_PurseContractPublicKeyP *purse_pub;
      80              : 
      81              :   /**
      82              :    * How many times did we see it?
      83              :    */
      84              :   unsigned int matched;
      85              : 
      86              :   /**
      87              :    * Was it reported as refunded?
      88              :    */
      89              :   bool refunded;
      90              : 
      91              :   /**
      92              :    * Was a reserve reported for it?
      93              :    */
      94              :   bool have_reserve;
      95              : };
      96              : 
      97              : 
      98              : /**
      99              :  * Callback for
     100              :  * #TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id().
     101              :  *
     102              :  * @param cls a `struct DecisionContext *`
     103              :  * @param rowid row of the decision
     104              :  * @param purse_pub the purse
     105              :  * @param reserve_pub reserve it went to, NULL if refunded
     106              :  * @param purse_value target value of the purse
     107              :  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
     108              :  */
     109              : static enum GNUNET_GenericReturnValue
     110            1 : decision_cb (void *cls,
     111              :              uint64_t rowid,
     112              :              const struct TALER_PurseContractPublicKeyP *purse_pub,
     113              :              const struct TALER_ReservePublicKeyP *reserve_pub,
     114              :              const struct TALER_Amount *purse_value)
     115              : {
     116            1 :   struct DecisionContext *ctx = cls;
     117              : 
     118              :   (void) rowid;
     119              :   (void) purse_value;
     120            1 :   ctx->total++;
     121            1 :   if ( (NULL != ctx->purse_pub) &&
     122            1 :        (0 == GNUNET_memcmp (purse_pub,
     123              :                             ctx->purse_pub)) )
     124              :   {
     125            1 :     ctx->matched++;
     126            1 :     ctx->have_reserve = (NULL != reserve_pub);
     127              :   }
     128            1 :   if ( (0 != ctx->stop_after) &&
     129            0 :        (ctx->total >= ctx->stop_after) )
     130            0 :     return GNUNET_SYSERR;
     131            1 :   return GNUNET_OK;
     132              : }
     133              : 
     134              : 
     135              : /**
     136              :  * Callback for
     137              :  * #TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id().
     138              :  *
     139              :  * @param cls a `struct DecisionContext *`
     140              :  * @param rowid row of the decision
     141              :  * @param purse_pub the purse
     142              :  * @param refunded whether the decision was to refund
     143              :  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
     144              :  */
     145              : static enum GNUNET_GenericReturnValue
     146            2 : all_decision_cb (void *cls,
     147              :                  uint64_t rowid,
     148              :                  const struct TALER_PurseContractPublicKeyP *purse_pub,
     149              :                  bool refunded)
     150              : {
     151            2 :   struct DecisionContext *ctx = cls;
     152              : 
     153              :   (void) rowid;
     154            2 :   ctx->total++;
     155            2 :   if ( (NULL != ctx->purse_pub) &&
     156            1 :        (0 == GNUNET_memcmp (purse_pub,
     157              :                             ctx->purse_pub)) )
     158              :   {
     159            1 :     ctx->matched++;
     160            1 :     ctx->refunded = refunded;
     161              :   }
     162            2 :   if ( (0 != ctx->stop_after) &&
     163            1 :        (ctx->total >= ctx->stop_after) )
     164            1 :     return GNUNET_SYSERR;
     165            1 :   return GNUNET_OK;
     166              : }
     167              : 
     168              : 
     169              : /**
     170              :  * With no purse to expire, nothing is decided and nothing is reported.
     171              :  *
     172              :  * @param pg the database context
     173              :  * @return 0 on success
     174              :  */
     175              : static int
     176            1 : check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
     177              : {
     178            1 :   struct DecisionContext ctx = { 0 };
     179              :   struct GNUNET_TIME_Absolute expiration;
     180              : 
     181            1 :   TDB_denom (pg,
     182              :              10,
     183              :              "5",
     184              :              "0.1",
     185              :              &denom);
     186            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     187              :           TALER_EXCHANGEDB_do_expire_purse (pg,
     188              :                                             GNUNET_TIME_UNIT_ZERO_ABS,
     189              :                                             GNUNET_TIME_absolute_get (),
     190              :                                             &expiration));
     191            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     192              :           TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id (
     193              :             pg,
     194              :             0,
     195              :             false,
     196              :             &decision_cb,
     197              :             &ctx));
     198            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     199              :           TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id (
     200              :             pg,
     201              :             0,
     202              :             &all_decision_cb,
     203              :             &ctx));
     204            1 :   FAILIF (0 != ctx.total);
     205            1 :   return 0;
     206              : }
     207              : 
     208              : 
     209              : /**
     210              :  * A purse that has not expired yet is left alone.
     211              :  *
     212              :  * @param pg the database context
     213              :  * @return 0 on success
     214              :  */
     215              : static int
     216            1 : check_not_expired (struct TALER_EXCHANGEDB_PostgresContext *pg)
     217              : {
     218              :   struct TDB_Purse purse;
     219              :   struct GNUNET_TIME_Absolute expiration;
     220              : 
     221              :   /* expires in 2023 */
     222            1 :   TDB_purse (pg,
     223              :              10,
     224              :              "5",
     225              :              ts (1700000000),
     226              :              &purse);
     227              :   /* ...but we only look at purses that expired before 2020 */
     228            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     229              :           TALER_EXCHANGEDB_do_expire_purse (pg,
     230              :                                             GNUNET_TIME_UNIT_ZERO_ABS,
     231              :                                             ts (1600000000).abs_time,
     232              :                                             &expiration));
     233            1 :   FAILIF (0 != TDB_count (pg,
     234              :                           "FROM purse_decision"));
     235            1 :   return 0;
     236              : }
     237              : 
     238              : 
     239              : /**
     240              :  * Expiring a purse decides it as a refund and gives the coins their
     241              :  * money back.
     242              :  *
     243              :  * @param pg the database context
     244              :  * @return 0 on success
     245              :  */
     246              : static int
     247            1 : check_expire (struct TALER_EXCHANGEDB_PostgresContext *pg)
     248              : {
     249              :   struct TDB_Purse purse;
     250              :   struct TALER_CoinPublicInfo coin;
     251              :   struct GNUNET_TIME_Absolute expiration;
     252              :   struct DecisionContext ctx;
     253              :   struct TALER_CoinSpendSignatureP coin_sig;
     254            1 :   struct TALER_Amount amount = TDB_amount ("1");
     255              :   bool balance_ok;
     256              :   bool too_late;
     257              :   bool conflict;
     258              :   char *hex;
     259              : 
     260              :   /* a purse that expired in 2020, with a coin paid into it */
     261            1 :   TDB_purse (pg,
     262              :              11,
     263              :              "5",
     264              :              ts (1600000000),
     265              :              &purse);
     266            1 :   TDB_coin (pg,
     267              :             &denom,
     268              :             20,
     269              :             &coin,
     270              :             NULL);
     271            1 :   TDB_FILL (coin_sig,
     272              :             20);
     273            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     274              :             TALER_EXCHANGEDB_do_purse_deposit (pg,
     275              :                                                &purse.purse_pub,
     276              :                                                &coin.coin_pub,
     277              :                                                &amount,
     278              :                                                &coin_sig,
     279              :                                                &amount,
     280              :                                                &balance_ok,
     281              :                                                &too_late,
     282              :                                                &conflict),
     283              :             TDB_coin_free (&coin));
     284            1 :   FAILIF_C (! balance_ok,
     285              :             TDB_coin_free (&coin));
     286            1 :   hex = TDB_hex (&coin.coin_pub,
     287              :                  sizeof (coin.coin_pub));
     288            1 :   FAILIF_C (1 != TDB_count (pg,
     289              :                             "FROM known_coins"
     290              :                             " WHERE coin_pub=decode('%s','hex')"
     291              :                             "   AND remaining=ROW(4,0)::taler_amount",
     292              :                             hex),
     293              :             GNUNET_free (hex); TDB_coin_free (&coin));
     294              : 
     295            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     296              :             TALER_EXCHANGEDB_do_expire_purse (pg,
     297              :                                               GNUNET_TIME_UNIT_ZERO_ABS,
     298              :                                               GNUNET_TIME_absolute_get (),
     299              :                                               &expiration),
     300              :             GNUNET_free (hex); TDB_coin_free (&coin));
     301            1 :   FAILIF_C (expiration.abs_value_us !=
     302              :             ts (1600000000).abs_time.abs_value_us,
     303              :             GNUNET_free (hex); TDB_coin_free (&coin));
     304            1 :   FAILIF_C (1 != TDB_count (pg,
     305              :                             "FROM purse_decision"),
     306              :             GNUNET_free (hex); TDB_coin_free (&coin));
     307              :   /* the coin got its money back */
     308            1 :   FAILIF_C (1 != TDB_count (pg,
     309              :                             "FROM known_coins"
     310              :                             " WHERE coin_pub=decode('%s','hex')"
     311              :                             "   AND remaining=ROW(5,0)::taler_amount",
     312              :                             hex),
     313              :             GNUNET_free (hex); TDB_coin_free (&coin));
     314            1 :   GNUNET_free (hex);
     315            1 :   TDB_coin_free (&coin);
     316              : 
     317              :   /* the decision is a refund, so the by-refund iterator sees it... */
     318            1 :   memset (&ctx,
     319              :           0,
     320              :           sizeof (ctx));
     321            1 :   ctx.purse_pub = &purse.purse_pub;
     322            1 :   FAILIF (0 >=
     323              :           TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id (
     324              :             pg,
     325              :             0,
     326              :             true,
     327              :             &decision_cb,
     328              :             &ctx));
     329            1 :   FAILIF (1 != ctx.matched);
     330              :   /* ...a refunded purse has no reserve to credit */
     331            1 :   FAILIF (ctx.have_reserve);
     332              : 
     333              :   /* ...and the non-refunded view does not */
     334            1 :   memset (&ctx,
     335              :           0,
     336              :           sizeof (ctx));
     337            1 :   ctx.purse_pub = &purse.purse_pub;
     338            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     339              :           TALER_EXCHANGEDB_iterate_purse_decisions_above_serial_id (
     340              :             pg,
     341              :             0,
     342              :             false,
     343              :             &decision_cb,
     344              :             &ctx));
     345            1 :   FAILIF (0 != ctx.matched);
     346              : 
     347              :   /* the all-decisions view reports it with its refund flag */
     348            1 :   memset (&ctx,
     349              :           0,
     350              :           sizeof (ctx));
     351            1 :   ctx.purse_pub = &purse.purse_pub;
     352            1 :   FAILIF (0 >=
     353              :           TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id (
     354              :             pg,
     355              :             0,
     356              :             &all_decision_cb,
     357              :             &ctx));
     358            1 :   FAILIF (1 != ctx.matched);
     359            1 :   FAILIF (! ctx.refunded);
     360              : 
     361              :   /* a second sweep finds nothing left to expire */
     362            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     363              :           TALER_EXCHANGEDB_do_expire_purse (pg,
     364              :                                             GNUNET_TIME_UNIT_ZERO_ABS,
     365              :                                             ts (1650000000).abs_time,
     366              :                                             &expiration));
     367            1 :   FAILIF (1 != TDB_count (pg,
     368              :                           "FROM purse_decision"));
     369            1 :   return 0;
     370              : }
     371              : 
     372              : 
     373              : /**
     374              :  * The iterators' serial bounds and abort returns behave as documented.
     375              :  *
     376              :  * @param pg the database context
     377              :  * @return 0 on success
     378              :  */
     379              : static int
     380            1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
     381              : {
     382              :   struct DecisionContext ctx;
     383              : 
     384            1 :   memset (&ctx,
     385              :           0,
     386              :           sizeof (ctx));
     387            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     388              :           TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id (
     389              :             pg,
     390              :             1000,
     391              :             &all_decision_cb,
     392              :             &ctx));
     393            1 :   FAILIF (0 != ctx.total);
     394            1 :   memset (&ctx,
     395              :           0,
     396              :           sizeof (ctx));
     397            1 :   ctx.stop_after = 1;
     398            1 :   FAILIF (1 !=
     399              :           TALER_EXCHANGEDB_iterate_all_purse_decisions_above_serial_id (
     400              :             pg,
     401              :             0,
     402              :             &all_decision_cb,
     403              :             &ctx));
     404            1 :   FAILIF (1 != ctx.total);
     405            1 :   return 0;
     406              : }
     407              : 
     408              : 
     409              : /**
     410              :  * The checks to run, in order.
     411              :  */
     412              : static const struct TDB_Test tests[] = {
     413              :   { "purse-decision-empty",
     414              :     &check_empty },
     415              :   { "purse-decision-not-expired",
     416              :     &check_not_expired },
     417              :   { "purse-decision-expire",
     418              :     &check_expire },
     419              :   { "purse-decision-iterate",
     420              :     &check_iterate },
     421              :   { NULL, NULL }
     422              : };
     423              : 
     424              : 
     425              : int
     426            1 : main (int argc,
     427              :       char *const *argv)
     428              : {
     429              :   int ret;
     430              : 
     431            1 :   ret = TDB_main (argc,
     432              :                   argv,
     433              :                   "test-purse-decision",
     434              :                   "Tests for the exchangedb `purse_decision' table",
     435              :                   tests);
     436            1 :   TDB_denom_free (&denom);
     437            1 :   return ret;
     438              : }
     439              : 
     440              : 
     441              : /* end of test_purse_decision.c */
        

Generated by: LCOV version 2.0-1