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

            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_close.c
      18              :  * @brief tests for the exchangedb functions whose primary table is
      19              :  *        `reserves_close`
      20              :  * @author Christian Grothoff
      21              :  *
      22              :  * Covers #TALER_EXCHANGEDB_insert_reserve_closed(),
      23              :  * #TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id() and
      24              :  * #TALER_EXCHANGEDB_iterate_reserve_close_info().
      25              :  *
      26              :  * The table references `reserves` and `wire_targets`, both of which the
      27              :  * checks create with TDB_reserve() and TDB_account().  The KYC-facing
      28              :  * iterator groups by the *normalized* account, so the checks close two
      29              :  * reserves to the same account and one to another.
      30              :  */
      31              : #include "test_common.h"
      32              : #include "exchange-database/insert_reserve_closed.h"
      33              : #include "exchange-database/iterate_reserve_closed_above_serial_id.h"
      34              : #include "exchange-database/iterate_reserve_close_info.h"
      35              : 
      36              : 
      37              : /**
      38              :  * Build a timestamp from a number of seconds since the epoch.
      39              :  *
      40              :  * @param secs seconds since the epoch
      41              :  * @return the timestamp
      42              :  */
      43              : static struct GNUNET_TIME_Timestamp
      44            6 : ts (uint64_t secs)
      45              : {
      46            6 :   struct GNUNET_TIME_Absolute abs = {
      47            6 :     .abs_value_us = secs * 1000LLU * 1000LLU
      48              :   };
      49              : 
      50            6 :   return GNUNET_TIME_absolute_to_timestamp (abs);
      51              : }
      52              : 
      53              : 
      54              : /**
      55              :  * Closure for #closed_cb().
      56              :  */
      57              : struct ClosedContext
      58              : {
      59              :   /**
      60              :    * How many rows did the callback see?
      61              :    */
      62              :   unsigned int total;
      63              : 
      64              :   /**
      65              :    * Stop after this many rows; 0 for no limit.
      66              :    */
      67              :   unsigned int stop_after;
      68              : 
      69              :   /**
      70              :    * Reserve we are looking for, NULL to match nothing.
      71              :    */
      72              :   const struct TALER_ReservePublicKeyP *reserve_pub;
      73              : 
      74              :   /**
      75              :    * How many times did we see @e reserve_pub?
      76              :    */
      77              :   unsigned int matched;
      78              : 
      79              :   /**
      80              :    * Amount reported for @e reserve_pub.
      81              :    */
      82              :   struct TALER_Amount amount;
      83              : 
      84              :   /**
      85              :    * Closing fee reported for @e reserve_pub.
      86              :    */
      87              :   struct TALER_Amount closing_fee;
      88              : 
      89              :   /**
      90              :    * Wire transfer identifier reported for @e reserve_pub.
      91              :    */
      92              :   struct TALER_WireTransferIdentifierRawP wtid;
      93              : 
      94              :   /**
      95              :    * Close request row reported for @e reserve_pub.
      96              :    */
      97              :   uint64_t close_request_row;
      98              : 
      99              :   /**
     100              :    * Receiver account reported for @e reserve_pub, owned by this struct.
     101              :    */
     102              :   char *receiver;
     103              : };
     104              : 
     105              : 
     106              : /**
     107              :  * Callback for #TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id().
     108              :  *
     109              :  * @param cls a `struct ClosedContext *`
     110              :  * @param rowid row of the close operation
     111              :  * @param execution_date when the transfer was made
     112              :  * @param amount_with_fee how much was debited
     113              :  * @param closing_fee how much was charged for closing
     114              :  * @param reserve_pub the reserve that was closed
     115              :  * @param receiver_account where the money went
     116              :  * @param wtid identifier of the wire transfer
     117              :  * @param close_request_row close request that triggered this, 0 if none
     118              :  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
     119              :  */
     120              : static enum GNUNET_GenericReturnValue
     121            6 : closed_cb (void *cls,
     122              :            uint64_t rowid,
     123              :            struct GNUNET_TIME_Timestamp execution_date,
     124              :            const struct TALER_Amount *amount_with_fee,
     125              :            const struct TALER_Amount *closing_fee,
     126              :            const struct TALER_ReservePublicKeyP *reserve_pub,
     127              :            const struct TALER_FullPayto receiver_account,
     128              :            const struct TALER_WireTransferIdentifierRawP *wtid,
     129              :            uint64_t close_request_row)
     130              : {
     131            6 :   struct ClosedContext *ctx = cls;
     132              : 
     133              :   (void) rowid;
     134              :   (void) execution_date;
     135            6 :   ctx->total++;
     136            6 :   if ( (NULL != ctx->reserve_pub) &&
     137            1 :        (0 == GNUNET_memcmp (reserve_pub,
     138              :                             ctx->reserve_pub)) )
     139              :   {
     140            1 :     ctx->matched++;
     141            1 :     ctx->amount = *amount_with_fee;
     142            1 :     ctx->closing_fee = *closing_fee;
     143            1 :     ctx->wtid = *wtid;
     144            1 :     ctx->close_request_row = close_request_row;
     145            1 :     GNUNET_free (ctx->receiver);
     146            1 :     ctx->receiver = GNUNET_strdup (receiver_account.full_payto);
     147              :   }
     148            6 :   if ( (0 != ctx->stop_after) &&
     149            2 :        (ctx->total >= ctx->stop_after) )
     150            1 :     return GNUNET_SYSERR;
     151            5 :   return GNUNET_OK;
     152              : }
     153              : 
     154              : 
     155              : /**
     156              :  * Closure for #amount_cb().
     157              :  */
     158              : struct AmountContext
     159              : {
     160              :   /**
     161              :    * How many amounts did the callback see?
     162              :    */
     163              :   unsigned int total;
     164              : 
     165              :   /**
     166              :    * Sum of the whole-unit parts of the amounts seen.
     167              :    */
     168              :   uint64_t value_sum;
     169              : 
     170              :   /**
     171              :    * Date of the first amount seen.
     172              :    */
     173              :   struct GNUNET_TIME_Absolute first_date;
     174              : 
     175              :   /**
     176              :    * Return this from the callback.
     177              :    */
     178              :   enum GNUNET_GenericReturnValue ret;
     179              : };
     180              : 
     181              : 
     182              : /**
     183              :  * Callback for #TALER_EXCHANGEDB_iterate_reserve_close_info().
     184              :  *
     185              :  * @param cls a `struct AmountContext *`
     186              :  * @param amount the closed amount
     187              :  * @param date when the reserve was closed
     188              :  * @return what @e ret of the closure says
     189              :  */
     190              : static enum GNUNET_GenericReturnValue
     191            5 : amount_cb (void *cls,
     192              :            const struct TALER_Amount *amount,
     193              :            struct GNUNET_TIME_Absolute date)
     194              : {
     195            5 :   struct AmountContext *ctx = cls;
     196              : 
     197            5 :   if (0 == ctx->total++)
     198            4 :     ctx->first_date = date;
     199            5 :   ctx->value_sum += amount->value;
     200            5 :   return ctx->ret;
     201              : }
     202              : 
     203              : 
     204              : /**
     205              :  * Close a reserve.
     206              :  *
     207              :  * @param pg the database context
     208              :  * @param reserve_pub reserve to close
     209              :  * @param account where the money goes
     210              :  * @param seed seed for the wire transfer identifier
     211              :  * @param when when the transfer was made, in seconds since the epoch
     212              :  * @param amount amount debited from the reserve, e.g. "10"
     213              :  * @param fee closing fee, e.g. "0.5"
     214              :  * @param close_request_row close request that triggered this, 0 if none
     215              :  * @param[out] wtid set to the wire transfer identifier used
     216              :  * @return transaction status
     217              :  */
     218              : static enum GNUNET_DB_QueryStatus
     219            3 : close_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg,
     220              :                const struct TALER_ReservePublicKeyP *reserve_pub,
     221              :                const struct TDB_Account *account,
     222              :                uint32_t seed,
     223              :                uint64_t when,
     224              :                const char *amount,
     225              :                const char *fee,
     226              :                uint64_t close_request_row,
     227              :                struct TALER_WireTransferIdentifierRawP *wtid)
     228              : {
     229            3 :   struct TALER_Amount a = TDB_amount (amount);
     230            3 :   struct TALER_Amount f = TDB_amount (fee);
     231              : 
     232            3 :   TDB_fill (wtid,
     233              :             sizeof (*wtid),
     234              :             seed);
     235            3 :   return TALER_EXCHANGEDB_insert_reserve_closed (pg,
     236              :                                                  reserve_pub,
     237              :                                                  ts (when),
     238              :                                                  account->payto,
     239              :                                                  wtid,
     240              :                                                  &a,
     241              :                                                  &f,
     242              :                                                  close_request_row);
     243              : }
     244              : 
     245              : 
     246              : /**
     247              :  * Nothing is reported while the table is empty.
     248              :  *
     249              :  * @param pg the database context
     250              :  * @return 0 on success
     251              :  */
     252              : static int
     253            1 : check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
     254              : {
     255            1 :   struct ClosedContext ctx = { 0 };
     256            1 :   struct AmountContext actx = { 0 };
     257              :   struct TALER_NormalizedPaytoHashP h_payto;
     258              : 
     259            1 :   TDB_FILL (h_payto,
     260              :             1);
     261            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     262              :           TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id (
     263              :             pg,
     264              :             0,
     265              :             &closed_cb,
     266              :             &ctx));
     267            1 :   actx.ret = GNUNET_OK;
     268            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     269              :           TALER_EXCHANGEDB_iterate_reserve_close_info (
     270              :             pg,
     271              :             &h_payto,
     272              :             GNUNET_TIME_UNIT_ZERO_ABS,
     273              :             &amount_cb,
     274              :             &actx));
     275            1 :   FAILIF (0 != ctx.total);
     276            1 :   FAILIF (0 != actx.total);
     277            1 :   return 0;
     278              : }
     279              : 
     280              : 
     281              : /**
     282              :  * A closed reserve is reported with everything it was closed with.
     283              :  *
     284              :  * @param pg the database context
     285              :  * @return 0 on success
     286              :  */
     287              : static int
     288            1 : check_insert_and_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
     289              : {
     290              :   struct TDB_Account account;
     291              :   struct TALER_ReservePublicKeyP reserve_pub;
     292              :   struct TALER_WireTransferIdentifierRawP wtid;
     293            1 :   struct TALER_Amount expect = TDB_amount ("10");
     294            1 :   struct TALER_Amount expect_fee = TDB_amount ("0.5");
     295              :   struct ClosedContext ctx;
     296              : 
     297            1 :   TDB_account (pg,
     298              :                10,
     299              :                &account);
     300              :   /* insert_reserve_closed() debits the reserve, so it has to hold exactly
     301              :      what is being closed */
     302            1 :   TDB_reserve (pg,
     303              :                10,
     304              :                "10",
     305              :                &reserve_pub);
     306            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     307              :             close_reserve (pg,
     308              :                            &reserve_pub,
     309              :                            &account,
     310              :                            10,
     311              :                            1600000000,
     312              :                            "10",
     313              :                            "0.5",
     314              :                            0,
     315              :                            &wtid),
     316              :             TDB_account_free (&account));
     317            1 :   memset (&ctx,
     318              :           0,
     319              :           sizeof (ctx));
     320            1 :   ctx.reserve_pub = &reserve_pub;
     321            1 :   FAILIF_C (0 >=
     322              :             TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id (
     323              :               pg,
     324              :               0,
     325              :               &closed_cb,
     326              :               &ctx),
     327              :             TDB_account_free (&account));
     328            1 :   FAILIF_C (1 != ctx.matched,
     329              :             GNUNET_free (ctx.receiver); TDB_account_free (&account));
     330            1 :   FAILIF_C (0 != TALER_amount_cmp (&ctx.amount,
     331              :                                    &expect),
     332              :             GNUNET_free (ctx.receiver); TDB_account_free (&account));
     333            1 :   FAILIF_C (0 != TALER_amount_cmp (&ctx.closing_fee,
     334              :                                    &expect_fee),
     335              :             GNUNET_free (ctx.receiver); TDB_account_free (&account));
     336            1 :   FAILIF_C (0 != GNUNET_memcmp (&ctx.wtid,
     337              :                                 &wtid),
     338              :             GNUNET_free (ctx.receiver); TDB_account_free (&account));
     339            1 :   FAILIF_C (0 != ctx.close_request_row,
     340              :             GNUNET_free (ctx.receiver); TDB_account_free (&account));
     341            1 :   FAILIF_C (0 != strcmp (ctx.receiver,
     342              :                          account.payto.full_payto),
     343              :             GNUNET_free (ctx.receiver); TDB_account_free (&account));
     344            1 :   GNUNET_free (ctx.receiver);
     345            1 :   TDB_account_free (&account);
     346            1 :   return 0;
     347              : }
     348              : 
     349              : 
     350              : /**
     351              :  * The serial bound, the abort return of the callback and a second account
     352              :  * all behave as documented.
     353              :  *
     354              :  * @param pg the database context
     355              :  * @return 0 on success
     356              :  */
     357              : static int
     358            1 : check_iterate_filters (struct TALER_EXCHANGEDB_PostgresContext *pg)
     359              : {
     360              :   struct TDB_Account account;
     361              :   struct TDB_Account other;
     362              :   struct TALER_ReservePublicKeyP r2;
     363              :   struct TALER_ReservePublicKeyP r3;
     364              :   struct TALER_WireTransferIdentifierRawP wtid;
     365              :   struct ClosedContext ctx;
     366              : 
     367            1 :   TDB_account (pg,
     368              :                10,
     369              :                &account);
     370            1 :   TDB_account (pg,
     371              :                11,
     372              :                &other);
     373            1 :   TDB_reserve (pg,
     374              :                11,
     375              :                "4",
     376              :                &r2);
     377            1 :   TDB_reserve (pg,
     378              :                12,
     379              :                "9",
     380              :                &r3);
     381              :   /* a second close to the same account, later */
     382            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     383              :             close_reserve (pg,
     384              :                            &r2,
     385              :                            &account,
     386              :                            11,
     387              :                            1600003600,
     388              :                            "4",
     389              :                            "0.5",
     390              :                            0,
     391              :                            &wtid),
     392              :             TDB_account_free (&account); TDB_account_free (&other));
     393              :   /* and one to a different account */
     394            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     395              :             close_reserve (pg,
     396              :                            &r3,
     397              :                            &other,
     398              :                            12,
     399              :                            1600007200,
     400              :                            "9",
     401              :                            "0.5",
     402              :                            0,
     403              :                            &wtid),
     404              :             TDB_account_free (&account); TDB_account_free (&other));
     405            1 :   FAILIF_C (3 != TDB_count (pg,
     406              :                             "FROM reserves_close"),
     407              :             TDB_account_free (&account); TDB_account_free (&other));
     408              : 
     409            1 :   memset (&ctx,
     410              :           0,
     411              :           sizeof (ctx));
     412            1 :   FAILIF_C (3 !=
     413              :             TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id (
     414              :               pg,
     415              :               0,
     416              :               &closed_cb,
     417              :               &ctx),
     418              :             TDB_account_free (&account); TDB_account_free (&other));
     419            1 :   memset (&ctx,
     420              :           0,
     421              :           sizeof (ctx));
     422            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     423              :             TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id (
     424              :               pg,
     425              :               1000,
     426              :               &closed_cb,
     427              :               &ctx),
     428              :             TDB_account_free (&account); TDB_account_free (&other));
     429            1 :   FAILIF_C (0 != ctx.total,
     430              :             TDB_account_free (&account); TDB_account_free (&other));
     431              : 
     432              :   /* an aborting callback stops the iteration */
     433            1 :   memset (&ctx,
     434              :           0,
     435              :           sizeof (ctx));
     436            1 :   ctx.stop_after = 2;
     437            1 :   FAILIF_C (3 !=
     438              :             TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id (
     439              :               pg,
     440              :               0,
     441              :               &closed_cb,
     442              :               &ctx),
     443              :             TDB_account_free (&account); TDB_account_free (&other));
     444            1 :   FAILIF_C (2 != ctx.total,
     445              :             TDB_account_free (&account); TDB_account_free (&other));
     446            1 :   TDB_account_free (&account);
     447            1 :   TDB_account_free (&other);
     448            1 :   return 0;
     449              : }
     450              : 
     451              : 
     452              : /**
     453              :  * The KYC view sums the closures per account, newest first, and honours
     454              :  * the time limit and the callback's abort.
     455              :  *
     456              :  * @param pg the database context
     457              :  * @return 0 on success
     458              :  */
     459              : static int
     460            1 : check_close_info (struct TALER_EXCHANGEDB_PostgresContext *pg)
     461              : {
     462              :   struct TDB_Account account;
     463              :   struct TDB_Account other;
     464              :   struct AmountContext ctx;
     465              : 
     466            1 :   TDB_account (pg,
     467              :                10,
     468              :                &account);
     469            1 :   TDB_account (pg,
     470              :                11,
     471              :                &other);
     472              : 
     473              :   /* both closures to the first account */
     474            1 :   memset (&ctx,
     475              :           0,
     476              :           sizeof (ctx));
     477            1 :   ctx.ret = GNUNET_OK;
     478            1 :   FAILIF_C (2 !=
     479              :             TALER_EXCHANGEDB_iterate_reserve_close_info (
     480              :               pg,
     481              :               &account.h_normalized,
     482              :               GNUNET_TIME_UNIT_ZERO_ABS,
     483              :               &amount_cb,
     484              :               &ctx),
     485              :             TDB_account_free (&account); TDB_account_free (&other));
     486            1 :   FAILIF_C (10 + 4 != ctx.value_sum,
     487              :             TDB_account_free (&account); TDB_account_free (&other));
     488              :   /* newest first */
     489            1 :   FAILIF_C (ctx.first_date.abs_value_us !=
     490              :             ts (1600003600).abs_time.abs_value_us,
     491              :             TDB_account_free (&account); TDB_account_free (&other));
     492              : 
     493              :   /* the other account has just the one */
     494            1 :   memset (&ctx,
     495              :           0,
     496              :           sizeof (ctx));
     497            1 :   ctx.ret = GNUNET_OK;
     498            1 :   FAILIF_C (1 !=
     499              :             TALER_EXCHANGEDB_iterate_reserve_close_info (
     500              :               pg,
     501              :               &other.h_normalized,
     502              :               GNUNET_TIME_UNIT_ZERO_ABS,
     503              :               &amount_cb,
     504              :               &ctx),
     505              :             TDB_account_free (&account); TDB_account_free (&other));
     506            1 :   FAILIF_C (9 != ctx.value_sum,
     507              :             TDB_account_free (&account); TDB_account_free (&other));
     508              : 
     509              :   /* the time limit drops the older closure */
     510            1 :   memset (&ctx,
     511              :           0,
     512              :           sizeof (ctx));
     513            1 :   ctx.ret = GNUNET_OK;
     514            1 :   FAILIF_C (1 !=
     515              :             TALER_EXCHANGEDB_iterate_reserve_close_info (
     516              :               pg,
     517              :               &account.h_normalized,
     518              :               ts (1600001800).abs_time,
     519              :               &amount_cb,
     520              :               &ctx),
     521              :             TDB_account_free (&account); TDB_account_free (&other));
     522            1 :   FAILIF_C (4 != ctx.value_sum,
     523              :             TDB_account_free (&account); TDB_account_free (&other));
     524              : 
     525              :   /* a limit past every closure leaves nothing */
     526            1 :   memset (&ctx,
     527              :           0,
     528              :           sizeof (ctx));
     529            1 :   ctx.ret = GNUNET_OK;
     530            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     531              :             TALER_EXCHANGEDB_iterate_reserve_close_info (
     532              :               pg,
     533              :               &account.h_normalized,
     534              :               ts (1700000000).abs_time,
     535              :               &amount_cb,
     536              :               &ctx),
     537              :             TDB_account_free (&account); TDB_account_free (&other));
     538            1 :   FAILIF_C (0 != ctx.total,
     539              :             TDB_account_free (&account); TDB_account_free (&other));
     540              : 
     541              :   /* a callback that aborts with GNUNET_NO is not an error */
     542            1 :   memset (&ctx,
     543              :           0,
     544              :           sizeof (ctx));
     545            1 :   ctx.ret = GNUNET_NO;
     546            1 :   FAILIF_C (0 >
     547              :             TALER_EXCHANGEDB_iterate_reserve_close_info (
     548              :               pg,
     549              :               &account.h_normalized,
     550              :               GNUNET_TIME_UNIT_ZERO_ABS,
     551              :               &amount_cb,
     552              :               &ctx),
     553              :             TDB_account_free (&account); TDB_account_free (&other));
     554            1 :   FAILIF_C (1 != ctx.total,
     555              :             TDB_account_free (&account); TDB_account_free (&other));
     556            1 :   TDB_account_free (&account);
     557            1 :   TDB_account_free (&other);
     558            1 :   return 0;
     559              : }
     560              : 
     561              : 
     562              : /**
     563              :  * The checks to run, in order.
     564              :  */
     565              : static const struct TDB_Test tests[] = {
     566              :   { "reserves-close-empty",
     567              :     &check_empty },
     568              :   { "reserves-close-insert-and-iterate",
     569              :     &check_insert_and_iterate },
     570              :   { "reserves-close-iterate-filters",
     571              :     &check_iterate_filters },
     572              :   { "reserves-close-close-info",
     573              :     &check_close_info },
     574              :   { NULL, NULL }
     575              : };
     576              : 
     577              : 
     578              : int
     579            1 : main (int argc,
     580              :       char *const *argv)
     581              : {
     582            1 :   return TDB_main (argc,
     583              :                    argv,
     584              :                    "test-reserves-close",
     585              :                    "Tests for the exchangedb `reserves_close' table",
     586              :                    tests);
     587              : }
     588              : 
     589              : 
     590              : /* end of test_reserves_close.c */
        

Generated by: LCOV version 2.0-1