exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

test_reserves_close.c (17303B)


      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 ts (uint64_t secs)
     45 {
     46   struct GNUNET_TIME_Absolute abs = {
     47     .abs_value_us = secs * 1000LLU * 1000LLU
     48   };
     49 
     50   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 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   struct ClosedContext *ctx = cls;
    132 
    133   (void) rowid;
    134   (void) execution_date;
    135   ctx->total++;
    136   if ( (NULL != ctx->reserve_pub) &&
    137        (0 == GNUNET_memcmp (reserve_pub,
    138                             ctx->reserve_pub)) )
    139   {
    140     ctx->matched++;
    141     ctx->amount = *amount_with_fee;
    142     ctx->closing_fee = *closing_fee;
    143     ctx->wtid = *wtid;
    144     ctx->close_request_row = close_request_row;
    145     GNUNET_free (ctx->receiver);
    146     ctx->receiver = GNUNET_strdup (receiver_account.full_payto);
    147   }
    148   if ( (0 != ctx->stop_after) &&
    149        (ctx->total >= ctx->stop_after) )
    150     return GNUNET_SYSERR;
    151   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 amount_cb (void *cls,
    192            const struct TALER_Amount *amount,
    193            struct GNUNET_TIME_Absolute date)
    194 {
    195   struct AmountContext *ctx = cls;
    196 
    197   if (0 == ctx->total++)
    198     ctx->first_date = date;
    199   ctx->value_sum += amount->value;
    200   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 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   struct TALER_Amount a = TDB_amount (amount);
    230   struct TALER_Amount f = TDB_amount (fee);
    231 
    232   TDB_fill (wtid,
    233             sizeof (*wtid),
    234             seed);
    235   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 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
    254 {
    255   struct ClosedContext ctx = { 0 };
    256   struct AmountContext actx = { 0 };
    257   struct TALER_NormalizedPaytoHashP h_payto;
    258 
    259   TDB_FILL (h_payto,
    260             1);
    261   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   actx.ret = GNUNET_OK;
    268   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   FAILIF (0 != ctx.total);
    276   FAILIF (0 != actx.total);
    277   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 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   struct TALER_Amount expect = TDB_amount ("10");
    294   struct TALER_Amount expect_fee = TDB_amount ("0.5");
    295   struct ClosedContext ctx;
    296 
    297   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   TDB_reserve (pg,
    303                10,
    304                "10",
    305                &reserve_pub);
    306   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   memset (&ctx,
    318           0,
    319           sizeof (ctx));
    320   ctx.reserve_pub = &reserve_pub;
    321   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   FAILIF_C (1 != ctx.matched,
    329             GNUNET_free (ctx.receiver); TDB_account_free (&account));
    330   FAILIF_C (0 != TALER_amount_cmp (&ctx.amount,
    331                                    &expect),
    332             GNUNET_free (ctx.receiver); TDB_account_free (&account));
    333   FAILIF_C (0 != TALER_amount_cmp (&ctx.closing_fee,
    334                                    &expect_fee),
    335             GNUNET_free (ctx.receiver); TDB_account_free (&account));
    336   FAILIF_C (0 != GNUNET_memcmp (&ctx.wtid,
    337                                 &wtid),
    338             GNUNET_free (ctx.receiver); TDB_account_free (&account));
    339   FAILIF_C (0 != ctx.close_request_row,
    340             GNUNET_free (ctx.receiver); TDB_account_free (&account));
    341   FAILIF_C (0 != strcmp (ctx.receiver,
    342                          account.payto.full_payto),
    343             GNUNET_free (ctx.receiver); TDB_account_free (&account));
    344   GNUNET_free (ctx.receiver);
    345   TDB_account_free (&account);
    346   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 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   TDB_account (pg,
    368                10,
    369                &account);
    370   TDB_account (pg,
    371                11,
    372                &other);
    373   TDB_reserve (pg,
    374                11,
    375                "4",
    376                &r2);
    377   TDB_reserve (pg,
    378                12,
    379                "9",
    380                &r3);
    381   /* a second close to the same account, later */
    382   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   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   FAILIF_C (3 != TDB_count (pg,
    406                             "FROM reserves_close"),
    407             TDB_account_free (&account); TDB_account_free (&other));
    408 
    409   memset (&ctx,
    410           0,
    411           sizeof (ctx));
    412   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   memset (&ctx,
    420           0,
    421           sizeof (ctx));
    422   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   FAILIF_C (0 != ctx.total,
    430             TDB_account_free (&account); TDB_account_free (&other));
    431 
    432   /* an aborting callback stops the iteration */
    433   memset (&ctx,
    434           0,
    435           sizeof (ctx));
    436   ctx.stop_after = 2;
    437   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   FAILIF_C (2 != ctx.total,
    445             TDB_account_free (&account); TDB_account_free (&other));
    446   TDB_account_free (&account);
    447   TDB_account_free (&other);
    448   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 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   TDB_account (pg,
    467                10,
    468                &account);
    469   TDB_account (pg,
    470                11,
    471                &other);
    472 
    473   /* both closures to the first account */
    474   memset (&ctx,
    475           0,
    476           sizeof (ctx));
    477   ctx.ret = GNUNET_OK;
    478   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   FAILIF_C (10 + 4 != ctx.value_sum,
    487             TDB_account_free (&account); TDB_account_free (&other));
    488   /* newest first */
    489   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   memset (&ctx,
    495           0,
    496           sizeof (ctx));
    497   ctx.ret = GNUNET_OK;
    498   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   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   memset (&ctx,
    511           0,
    512           sizeof (ctx));
    513   ctx.ret = GNUNET_OK;
    514   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   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   memset (&ctx,
    527           0,
    528           sizeof (ctx));
    529   ctx.ret = GNUNET_OK;
    530   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   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   memset (&ctx,
    543           0,
    544           sizeof (ctx));
    545   ctx.ret = GNUNET_NO;
    546   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   FAILIF_C (1 != ctx.total,
    555             TDB_account_free (&account); TDB_account_free (&other));
    556   TDB_account_free (&account);
    557   TDB_account_free (&other);
    558   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 main (int argc,
    580       char *const *argv)
    581 {
    582   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 */