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 */