exchange

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

test_replication.c (12799B)


      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_replication.c
     18  * @brief tests for the exchangedb replication functions
     19  * @author Christian Grothoff
     20  *
     21  * Covers #TALER_EXCHANGEDB_get_serial_by_table(),
     22  * #TALER_EXCHANGEDB_iterate_records_by_table() and
     23  * #TALER_EXCHANGEDB_insert_records_by_table().
     24  *
     25  * These three have no table of their own: each is one big switch over
     26  * #TALER_EXCHANGEDB_ReplicatedTable, and taler-auditor-sync drives them in
     27  * a loop over every value of that enum.  A table that is missing from one
     28  * of the switches, or whose SQL no longer matches the schema, breaks
     29  * replication for that table only -- and nothing else in the test suite
     30  * would notice.  So the checks here walk the whole enum, and then do one
     31  * full round trip (read a record out, delete it, put it back) to show that
     32  * the reader and the writer agree.
     33  */
     34 #include "test_common.h"
     35 #include "exchange-database/get_serial_by_table.h"
     36 #include "exchange-database/insert_records_by_table.h"
     37 #include "exchange-database/iterate_records_by_table.h"
     38 
     39 
     40 /**
     41  * The two accounts we replicate.
     42  */
     43 static struct TDB_Account account[2];
     44 
     45 
     46 /**
     47  * Denomination the checks use.
     48  */
     49 static struct TDB_Denom denom;
     50 
     51 
     52 /**
     53  * Value we gave @e denom.
     54  */
     55 #define DENOM_VALUE "5"
     56 
     57 
     58 /**
     59  * Closure for #record_cb().
     60  */
     61 struct RecordContext
     62 {
     63   /**
     64    * How many records did the callback see?
     65    */
     66   unsigned int total;
     67 
     68   /**
     69    * Table we expect the records to be from.
     70    */
     71   enum TALER_EXCHANGEDB_ReplicatedTable table;
     72 
     73   /**
     74    * Set to true if a record from another table arrived.
     75    */
     76   bool wrong_table;
     77 
     78   /**
     79    * Serial of the last record.
     80    */
     81   uint64_t serial;
     82 
     83   /**
     84    * Payto URI we are looking for, NULL to look for none.
     85    */
     86   const char *payto;
     87 
     88   /**
     89    * Serial the record with @e payto had, 0 if it did not show up.
     90    */
     91   uint64_t payto_serial;
     92 
     93   /**
     94    * Value of the last denomination record.
     95    */
     96   struct TALER_Amount coin;
     97 };
     98 
     99 
    100 /**
    101  * Callback for #TALER_EXCHANGEDB_iterate_records_by_table().
    102  *
    103  * @param cls a `struct RecordContext *`
    104  * @param td the record
    105  * @return #GNUNET_OK to continue iterating
    106  */
    107 static int
    108 record_cb (void *cls,
    109            const struct TALER_EXCHANGEDB_TableData *td)
    110 {
    111   struct RecordContext *ctx = cls;
    112 
    113   ctx->total++;
    114   ctx->serial = td->serial;
    115   if (td->table != ctx->table)
    116     ctx->wrong_table = true;
    117   switch (td->table)
    118   {
    119   case TALER_EXCHANGEDB_RT_DENOMINATIONS:
    120     ctx->coin = td->details.denominations.coin;
    121     break;
    122   case TALER_EXCHANGEDB_RT_WIRE_TARGETS:
    123     if ( (NULL != ctx->payto) &&
    124          (0 == strcmp (ctx->payto,
    125                        td->details.wire_targets.full_payto_uri.full_payto)) )
    126       ctx->payto_serial = td->serial;
    127     break;
    128   default:
    129     break;
    130   }
    131   return GNUNET_OK;
    132 }
    133 
    134 
    135 /**
    136  * Every replicated table must have a serial statement, and on an empty
    137  * database none of them has a row.
    138  *
    139  * @param pg the database context
    140  * @return 0 on success
    141  */
    142 static int
    143 check_all_serials (struct TALER_EXCHANGEDB_PostgresContext *pg)
    144 {
    145   for (unsigned int t = 0;
    146        t <= TALER_EXCHANGEDB_RT_KYCAUTHS_IN;
    147        t++)
    148   {
    149     uint64_t serial = UINT64_MAX;
    150     enum GNUNET_DB_QueryStatus qs;
    151 
    152     qs = TALER_EXCHANGEDB_get_serial_by_table (
    153       pg,
    154       (enum TALER_EXCHANGEDB_ReplicatedTable) t,
    155       &serial);
    156     if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs)
    157       fprintf (stderr,
    158                "get_serial_by_table(%u) returned %d\n",
    159                t,
    160                (int) qs);
    161     FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs);
    162   }
    163   return 0;
    164 }
    165 
    166 
    167 /**
    168  * Every replicated table must be readable, and on an empty database none
    169  * of them yields a record.
    170  *
    171  * @param pg the database context
    172  * @return 0 on success
    173  */
    174 static int
    175 check_all_iterable (struct TALER_EXCHANGEDB_PostgresContext *pg)
    176 {
    177   for (unsigned int t = 0;
    178        t <= TALER_EXCHANGEDB_RT_KYCAUTHS_IN;
    179        t++)
    180   {
    181     struct RecordContext ctx = {
    182       .table = (enum TALER_EXCHANGEDB_ReplicatedTable) t
    183     };
    184     enum GNUNET_DB_QueryStatus qs;
    185 
    186     qs = TALER_EXCHANGEDB_iterate_records_by_table (
    187       pg,
    188       (enum TALER_EXCHANGEDB_ReplicatedTable) t,
    189       0,
    190       &record_cb,
    191       &ctx);
    192     if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs)
    193       fprintf (stderr,
    194                "iterate_records_by_table(%u) returned %d\n",
    195                t,
    196                (int) qs);
    197     FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs);
    198     FAILIF (0 != ctx.total);
    199   }
    200   return 0;
    201 }
    202 
    203 
    204 /**
    205  * A table outside the enum is refused by all three functions.
    206  *
    207  * @param pg the database context
    208  * @return 0 on success
    209  */
    210 static int
    211 check_unknown_table (struct TALER_EXCHANGEDB_PostgresContext *pg)
    212 {
    213   enum TALER_EXCHANGEDB_ReplicatedTable bogus
    214     = (enum TALER_EXCHANGEDB_ReplicatedTable) (TALER_EXCHANGEDB_RT_KYCAUTHS_IN
    215                                                + 1);
    216   struct TALER_EXCHANGEDB_TableData td = {
    217     .table = bogus
    218   };
    219   struct RecordContext ctx = {
    220     .table = bogus
    221   };
    222   uint64_t serial;
    223 
    224   FAILIF (GNUNET_DB_STATUS_HARD_ERROR !=
    225           TALER_EXCHANGEDB_get_serial_by_table (pg,
    226                                                 bogus,
    227                                                 &serial));
    228   FAILIF (GNUNET_DB_STATUS_HARD_ERROR !=
    229           TALER_EXCHANGEDB_iterate_records_by_table (pg,
    230                                                      bogus,
    231                                                      0,
    232                                                      &record_cb,
    233                                                      &ctx));
    234   FAILIF (0 != ctx.total);
    235   FAILIF (GNUNET_DB_STATUS_HARD_ERROR !=
    236           TALER_EXCHANGEDB_insert_records_by_table (pg,
    237                                                     &td));
    238   return 0;
    239 }
    240 
    241 
    242 /**
    243  * With rows in place, the serial of a table is the row ID of its last
    244  * record.
    245  *
    246  * @param pg the database context
    247  * @return 0 on success
    248  */
    249 static int
    250 check_serial (struct TALER_EXCHANGEDB_PostgresContext *pg)
    251 {
    252   uint64_t serial = 0;
    253 
    254   for (unsigned int i = 0; i < 2; i++)
    255     TDB_account (pg,
    256                  10 + i,
    257                  &account[i]);
    258   TDB_denom (pg,
    259              10,
    260              DENOM_VALUE,
    261              "0.1",
    262              &denom);
    263 
    264   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    265           TALER_EXCHANGEDB_get_serial_by_table (
    266             pg,
    267             TALER_EXCHANGEDB_RT_WIRE_TARGETS,
    268             &serial));
    269   FAILIF (2 != serial);
    270   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    271           TALER_EXCHANGEDB_get_serial_by_table (
    272             pg,
    273             TALER_EXCHANGEDB_RT_KYC_TARGETS,
    274             &serial));
    275   FAILIF (2 != serial);
    276   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    277           TALER_EXCHANGEDB_get_serial_by_table (
    278             pg,
    279             TALER_EXCHANGEDB_RT_DENOMINATIONS,
    280             &serial));
    281   FAILIF (serial != denom.serial);
    282 
    283   /* a table nobody wrote to still has no serial */
    284   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    285           TALER_EXCHANGEDB_get_serial_by_table (
    286             pg,
    287             TALER_EXCHANGEDB_RT_WIRE_OUT,
    288             &serial));
    289   return 0;
    290 }
    291 
    292 
    293 /**
    294  * Reading records returns them in serial order and skips everything up
    295  * to and including the requested serial.
    296  *
    297  * @param pg the database context
    298  * @return 0 on success
    299  */
    300 static int
    301 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    302 {
    303   struct RecordContext ctx;
    304   struct TALER_Amount expect = TDB_amount (DENOM_VALUE);
    305 
    306   memset (&ctx,
    307           0,
    308           sizeof (ctx));
    309   ctx.table = TALER_EXCHANGEDB_RT_DENOMINATIONS;
    310   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    311           TALER_EXCHANGEDB_iterate_records_by_table (
    312             pg,
    313             TALER_EXCHANGEDB_RT_DENOMINATIONS,
    314             0,
    315             &record_cb,
    316             &ctx));
    317   FAILIF (1 != ctx.total);
    318   FAILIF (ctx.wrong_table);
    319   FAILIF (ctx.serial != denom.serial);
    320   FAILIF (0 != TALER_amount_cmp (&ctx.coin,
    321                                  &expect));
    322 
    323   /* nothing above the last serial */
    324   memset (&ctx,
    325           0,
    326           sizeof (ctx));
    327   ctx.table = TALER_EXCHANGEDB_RT_DENOMINATIONS;
    328   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    329           TALER_EXCHANGEDB_iterate_records_by_table (
    330             pg,
    331             TALER_EXCHANGEDB_RT_DENOMINATIONS,
    332             denom.serial,
    333             &record_cb,
    334             &ctx));
    335   FAILIF (0 != ctx.total);
    336 
    337   /* both accounts, and the second one only above the first serial */
    338   memset (&ctx,
    339           0,
    340           sizeof (ctx));
    341   ctx.table = TALER_EXCHANGEDB_RT_WIRE_TARGETS;
    342   ctx.payto = account[1].payto.full_payto;
    343   FAILIF (0 >=
    344           TALER_EXCHANGEDB_iterate_records_by_table (
    345             pg,
    346             TALER_EXCHANGEDB_RT_WIRE_TARGETS,
    347             0,
    348             &record_cb,
    349             &ctx));
    350   FAILIF (2 != ctx.total);
    351   FAILIF (0 == ctx.payto_serial);
    352 
    353   memset (&ctx,
    354           0,
    355           sizeof (ctx));
    356   ctx.table = TALER_EXCHANGEDB_RT_WIRE_TARGETS;
    357   FAILIF (0 >=
    358           TALER_EXCHANGEDB_iterate_records_by_table (
    359             pg,
    360             TALER_EXCHANGEDB_RT_WIRE_TARGETS,
    361             1,
    362             &record_cb,
    363             &ctx));
    364   FAILIF (1 != ctx.total);
    365   FAILIF (2 != ctx.serial);
    366   return 0;
    367 }
    368 
    369 
    370 /**
    371  * A record that is read out, dropped and written back lands in the same
    372  * row it came from.
    373  *
    374  * @param pg the database context
    375  * @return 0 on success
    376  */
    377 static int
    378 check_round_trip (struct TALER_EXCHANGEDB_PostgresContext *pg)
    379 {
    380   struct TALER_EXCHANGEDB_TableData td = {
    381     .table = TALER_EXCHANGEDB_RT_WIRE_TARGETS
    382   };
    383   struct RecordContext ctx;
    384   char *hex;
    385 
    386   /* find the row of the second account */
    387   memset (&ctx,
    388           0,
    389           sizeof (ctx));
    390   ctx.table = TALER_EXCHANGEDB_RT_WIRE_TARGETS;
    391   ctx.payto = account[1].payto.full_payto;
    392   FAILIF (0 >=
    393           TALER_EXCHANGEDB_iterate_records_by_table (
    394             pg,
    395             TALER_EXCHANGEDB_RT_WIRE_TARGETS,
    396             0,
    397             &record_cb,
    398             &ctx));
    399   FAILIF (0 == ctx.payto_serial);
    400 
    401   /* the account is not referenced by anything, so we can drop it and
    402      replicate it back in */
    403   hex = TDB_hex (&account[1].h_full,
    404                  sizeof (account[1].h_full));
    405   FAILIF_C (GNUNET_OK !=
    406             TDB_exec (pg,
    407                       "DELETE FROM wire_targets"
    408                       " WHERE wire_target_h_payto=decode('%s','hex');",
    409                       hex),
    410             GNUNET_free (hex));
    411   GNUNET_free (hex);
    412   FAILIF (1 != TDB_count (pg,
    413                           "FROM wire_targets"));
    414 
    415   td.serial = ctx.payto_serial;
    416   td.details.wire_targets.full_payto_uri = account[1].payto;
    417   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    418           TALER_EXCHANGEDB_insert_records_by_table (pg,
    419                                                     &td));
    420   FAILIF (2 != TDB_count (pg,
    421                           "FROM wire_targets"));
    422 
    423   /* and it is back under its original serial, with its payto intact */
    424   memset (&ctx,
    425           0,
    426           sizeof (ctx));
    427   ctx.table = TALER_EXCHANGEDB_RT_WIRE_TARGETS;
    428   ctx.payto = account[1].payto.full_payto;
    429   FAILIF (0 >=
    430           TALER_EXCHANGEDB_iterate_records_by_table (
    431             pg,
    432             TALER_EXCHANGEDB_RT_WIRE_TARGETS,
    433             0,
    434             &record_cb,
    435             &ctx));
    436   FAILIF (2 != ctx.total);
    437   FAILIF (ctx.payto_serial != td.serial);
    438   return 0;
    439 }
    440 
    441 
    442 /**
    443  * The checks to run, in order.
    444  */
    445 static const struct TDB_Test tests[] = {
    446   { "replication-all-serials",
    447     &check_all_serials },
    448   { "replication-all-iterable",
    449     &check_all_iterable },
    450   { "replication-unknown-table",
    451     &check_unknown_table },
    452   { "replication-serial",
    453     &check_serial },
    454   { "replication-iterate",
    455     &check_iterate },
    456   { "replication-round-trip",
    457     &check_round_trip },
    458   { NULL, NULL }
    459 };
    460 
    461 
    462 int
    463 main (int argc,
    464       char *const *argv)
    465 {
    466   int ret;
    467 
    468   ret = TDB_main (argc,
    469                   argv,
    470                   "test-replication",
    471                   "Tests for the exchangedb replication functions",
    472                   tests);
    473   TDB_denom_free (&denom);
    474   for (unsigned int i = 0; i < 2; i++)
    475     TDB_account_free (&account[i]);
    476   return ret;
    477 }
    478 
    479 
    480 /* end of test_replication.c */