LCOV - code coverage report
Current view: top level - exchangedb - test_replication.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 95.7 % 115 110
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_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            8 : record_cb (void *cls,
     109              :            const struct TALER_EXCHANGEDB_TableData *td)
     110              : {
     111            8 :   struct RecordContext *ctx = cls;
     112              : 
     113            8 :   ctx->total++;
     114            8 :   ctx->serial = td->serial;
     115            8 :   if (td->table != ctx->table)
     116            0 :     ctx->wrong_table = true;
     117            8 :   switch (td->table)
     118              :   {
     119            1 :   case TALER_EXCHANGEDB_RT_DENOMINATIONS:
     120            1 :     ctx->coin = td->details.denominations.coin;
     121            1 :     break;
     122            7 :   case TALER_EXCHANGEDB_RT_WIRE_TARGETS:
     123            7 :     if ( (NULL != ctx->payto) &&
     124            6 :          (0 == strcmp (ctx->payto,
     125            6 :                        td->details.wire_targets.full_payto_uri.full_payto)) )
     126            3 :       ctx->payto_serial = td->serial;
     127            7 :     break;
     128            0 :   default:
     129            0 :     break;
     130              :   }
     131            8 :   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            1 : check_all_serials (struct TALER_EXCHANGEDB_PostgresContext *pg)
     144              : {
     145            1 :   for (unsigned int t = 0;
     146           48 :        t <= TALER_EXCHANGEDB_RT_KYCAUTHS_IN;
     147           47 :        t++)
     148              :   {
     149           47 :     uint64_t serial = UINT64_MAX;
     150              :     enum GNUNET_DB_QueryStatus qs;
     151              : 
     152           47 :     qs = TALER_EXCHANGEDB_get_serial_by_table (
     153              :       pg,
     154              :       (enum TALER_EXCHANGEDB_ReplicatedTable) t,
     155              :       &serial);
     156           47 :     if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs)
     157            0 :       fprintf (stderr,
     158              :                "get_serial_by_table(%u) returned %d\n",
     159              :                t,
     160              :                (int) qs);
     161           47 :     FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs);
     162              :   }
     163            1 :   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            1 : check_all_iterable (struct TALER_EXCHANGEDB_PostgresContext *pg)
     176              : {
     177            1 :   for (unsigned int t = 0;
     178           48 :        t <= TALER_EXCHANGEDB_RT_KYCAUTHS_IN;
     179           47 :        t++)
     180              :   {
     181           47 :     struct RecordContext ctx = {
     182              :       .table = (enum TALER_EXCHANGEDB_ReplicatedTable) t
     183              :     };
     184              :     enum GNUNET_DB_QueryStatus qs;
     185              : 
     186           47 :     qs = TALER_EXCHANGEDB_iterate_records_by_table (
     187              :       pg,
     188              :       (enum TALER_EXCHANGEDB_ReplicatedTable) t,
     189              :       0,
     190              :       &record_cb,
     191              :       &ctx);
     192           47 :     if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs)
     193            0 :       fprintf (stderr,
     194              :                "iterate_records_by_table(%u) returned %d\n",
     195              :                t,
     196              :                (int) qs);
     197           47 :     FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs);
     198           47 :     FAILIF (0 != ctx.total);
     199              :   }
     200            1 :   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            1 : check_unknown_table (struct TALER_EXCHANGEDB_PostgresContext *pg)
     212              : {
     213            1 :   enum TALER_EXCHANGEDB_ReplicatedTable bogus
     214              :     = (enum TALER_EXCHANGEDB_ReplicatedTable) (TALER_EXCHANGEDB_RT_KYCAUTHS_IN
     215              :                                                + 1);
     216            1 :   struct TALER_EXCHANGEDB_TableData td = {
     217              :     .table = bogus
     218              :   };
     219            1 :   struct RecordContext ctx = {
     220              :     .table = bogus
     221              :   };
     222              :   uint64_t serial;
     223              : 
     224            1 :   FAILIF (GNUNET_DB_STATUS_HARD_ERROR !=
     225              :           TALER_EXCHANGEDB_get_serial_by_table (pg,
     226              :                                                 bogus,
     227              :                                                 &serial));
     228            1 :   FAILIF (GNUNET_DB_STATUS_HARD_ERROR !=
     229              :           TALER_EXCHANGEDB_iterate_records_by_table (pg,
     230              :                                                      bogus,
     231              :                                                      0,
     232              :                                                      &record_cb,
     233              :                                                      &ctx));
     234            1 :   FAILIF (0 != ctx.total);
     235            1 :   FAILIF (GNUNET_DB_STATUS_HARD_ERROR !=
     236              :           TALER_EXCHANGEDB_insert_records_by_table (pg,
     237              :                                                     &td));
     238            1 :   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            1 : check_serial (struct TALER_EXCHANGEDB_PostgresContext *pg)
     251              : {
     252            1 :   uint64_t serial = 0;
     253              : 
     254            3 :   for (unsigned int i = 0; i < 2; i++)
     255            2 :     TDB_account (pg,
     256              :                  10 + i,
     257              :                  &account[i]);
     258            1 :   TDB_denom (pg,
     259              :              10,
     260              :              DENOM_VALUE,
     261              :              "0.1",
     262              :              &denom);
     263              : 
     264            1 :   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            1 :   FAILIF (2 != serial);
     270            1 :   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            1 :   FAILIF (2 != serial);
     276            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     277              :           TALER_EXCHANGEDB_get_serial_by_table (
     278              :             pg,
     279              :             TALER_EXCHANGEDB_RT_DENOMINATIONS,
     280              :             &serial));
     281            1 :   FAILIF (serial != denom.serial);
     282              : 
     283              :   /* a table nobody wrote to still has no serial */
     284            1 :   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            1 :   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            1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
     302              : {
     303              :   struct RecordContext ctx;
     304            1 :   struct TALER_Amount expect = TDB_amount (DENOM_VALUE);
     305              : 
     306            1 :   memset (&ctx,
     307              :           0,
     308              :           sizeof (ctx));
     309            1 :   ctx.table = TALER_EXCHANGEDB_RT_DENOMINATIONS;
     310            1 :   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            1 :   FAILIF (1 != ctx.total);
     318            1 :   FAILIF (ctx.wrong_table);
     319            1 :   FAILIF (ctx.serial != denom.serial);
     320            1 :   FAILIF (0 != TALER_amount_cmp (&ctx.coin,
     321              :                                  &expect));
     322              : 
     323              :   /* nothing above the last serial */
     324            1 :   memset (&ctx,
     325              :           0,
     326              :           sizeof (ctx));
     327            1 :   ctx.table = TALER_EXCHANGEDB_RT_DENOMINATIONS;
     328            1 :   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            1 :   FAILIF (0 != ctx.total);
     336              : 
     337              :   /* both accounts, and the second one only above the first serial */
     338            1 :   memset (&ctx,
     339              :           0,
     340              :           sizeof (ctx));
     341            1 :   ctx.table = TALER_EXCHANGEDB_RT_WIRE_TARGETS;
     342            1 :   ctx.payto = account[1].payto.full_payto;
     343            1 :   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            1 :   FAILIF (2 != ctx.total);
     351            1 :   FAILIF (0 == ctx.payto_serial);
     352              : 
     353            1 :   memset (&ctx,
     354              :           0,
     355              :           sizeof (ctx));
     356            1 :   ctx.table = TALER_EXCHANGEDB_RT_WIRE_TARGETS;
     357            1 :   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            1 :   FAILIF (1 != ctx.total);
     365            1 :   FAILIF (2 != ctx.serial);
     366            1 :   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            1 : check_round_trip (struct TALER_EXCHANGEDB_PostgresContext *pg)
     379              : {
     380            1 :   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            1 :   memset (&ctx,
     388              :           0,
     389              :           sizeof (ctx));
     390            1 :   ctx.table = TALER_EXCHANGEDB_RT_WIRE_TARGETS;
     391            1 :   ctx.payto = account[1].payto.full_payto;
     392            1 :   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            1 :   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            1 :   hex = TDB_hex (&account[1].h_full,
     404              :                  sizeof (account[1].h_full));
     405            1 :   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            1 :   GNUNET_free (hex);
     412            1 :   FAILIF (1 != TDB_count (pg,
     413              :                           "FROM wire_targets"));
     414              : 
     415            1 :   td.serial = ctx.payto_serial;
     416            1 :   td.details.wire_targets.full_payto_uri = account[1].payto;
     417            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     418              :           TALER_EXCHANGEDB_insert_records_by_table (pg,
     419              :                                                     &td));
     420            1 :   FAILIF (2 != TDB_count (pg,
     421              :                           "FROM wire_targets"));
     422              : 
     423              :   /* and it is back under its original serial, with its payto intact */
     424            1 :   memset (&ctx,
     425              :           0,
     426              :           sizeof (ctx));
     427            1 :   ctx.table = TALER_EXCHANGEDB_RT_WIRE_TARGETS;
     428            1 :   ctx.payto = account[1].payto.full_payto;
     429            1 :   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            1 :   FAILIF (2 != ctx.total);
     437            1 :   FAILIF (ctx.payto_serial != td.serial);
     438            1 :   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            1 : main (int argc,
     464              :       char *const *argv)
     465              : {
     466              :   int ret;
     467              : 
     468            1 :   ret = TDB_main (argc,
     469              :                   argv,
     470              :                   "test-replication",
     471              :                   "Tests for the exchangedb replication functions",
     472              :                   tests);
     473            1 :   TDB_denom_free (&denom);
     474            3 :   for (unsigned int i = 0; i < 2; i++)
     475            2 :     TDB_account_free (&account[i]);
     476            1 :   return ret;
     477              : }
     478              : 
     479              : 
     480              : /* end of test_replication.c */
        

Generated by: LCOV version 2.0-1