test_misc.c (19622B)
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_misc.c 18 * @brief tests for the exchangedb functions that have no table of their own 19 * @author Christian Grothoff 20 * 21 * Covers #TALER_EXCHANGEDB_start(), #TALER_EXCHANGEDB_start_read_only(), 22 * #TALER_EXCHANGEDB_start_read_committed(), 23 * #TALER_EXCHANGEDB_start_deferred_wire_out(), #TALER_EXCHANGEDB_commit(), 24 * #TALER_EXCHANGEDB_rollback(), #TALER_EXCHANGEDB_preflight(), 25 * #TALER_EXCHANGEDB_compute_shard(), 26 * #TALER_EXCHANGEDB_calculate_transaction_list_totals(), 27 * #TALER_EXCHANGEDB_free_coin_transaction_list(), 28 * #TALER_EXCHANGEDB_free_reserve_history(), 29 * #TALER_EXCHANGEDB_event_listen(), 30 * #TALER_EXCHANGEDB_event_listen_cancel(), 31 * #TALER_EXCHANGEDB_event_notify(), #TALER_EXCHANGEDB_enable_rules(), 32 * #TALER_EXCHANGEDB_disable_rules(), 33 * #TALER_EXCHANGEDB_inject_auditor_triggers(), 34 * #TALER_EXCHANGEDB_drop_tables() and #TALER_EXCHANGEDB_create_tables(). 35 * 36 * These are the connection-level functions: transactions, notifications, 37 * schema management and the two list destructors. The schema checks come 38 * last on purpose -- they take the tables away and put them back, so 39 * nothing may run after them. 40 */ 41 #include "test_common.h" 42 #include "taler/taler_dbevents.h" 43 #include "exchange-database/commit.h" 44 #include "exchange-database/compute_shard.h" 45 #include "exchange-database/create_tables.h" 46 #include "exchange-database/disable_rules.h" 47 #include "exchange-database/drop_tables.h" 48 #include "exchange-database/enable_rules.h" 49 #include "exchange-database/event_listen.h" 50 #include "exchange-database/event_listen_cancel.h" 51 #include "exchange-database/event_notify.h" 52 #include "exchange-database/free_coin_transaction_list.h" 53 #include "exchange-database/free_reserve_history.h" 54 #include "exchange-database/get_coin_transactions.h" 55 #include "exchange-database/get_reserve_history.h" 56 #include "exchange-database/inject_auditor_triggers.h" 57 #include "exchange-database/preflight.h" 58 #include "exchange-database/rollback.h" 59 #include "exchange-database/start.h" 60 #include "exchange-database/start_deferred_wire_out.h" 61 #include "exchange-database/start_read_committed.h" 62 #include "exchange-database/start_read_only.h" 63 64 65 /** 66 * Account the checks fund their reserves from. 67 */ 68 static struct TDB_Account account; 69 70 71 /** 72 * Denomination the checks use. 73 */ 74 static struct TDB_Denom denom; 75 76 77 /** 78 * The four ways to start a transaction. 79 * 80 * @param pg the database context 81 * @param name name of the transaction 82 * @return #GNUNET_OK on success 83 */ 84 static enum GNUNET_GenericReturnValue 85 start_serializable (struct TALER_EXCHANGEDB_PostgresContext *pg, 86 const char *name) 87 { 88 return TALER_EXCHANGEDB_start (pg, 89 name); 90 } 91 92 93 /** 94 * Start a read-only transaction. 95 * 96 * @param pg the database context 97 * @param name name of the transaction 98 * @return #GNUNET_OK on success 99 */ 100 static enum GNUNET_GenericReturnValue 101 start_read_only (struct TALER_EXCHANGEDB_PostgresContext *pg, 102 const char *name) 103 { 104 return TALER_EXCHANGEDB_start_read_only (pg, 105 name); 106 } 107 108 109 /** 110 * Start a read-committed transaction. 111 * 112 * @param pg the database context 113 * @param name name of the transaction 114 * @return #GNUNET_OK on success 115 */ 116 static enum GNUNET_GenericReturnValue 117 start_read_committed (struct TALER_EXCHANGEDB_PostgresContext *pg, 118 const char *name) 119 { 120 return TALER_EXCHANGEDB_start_read_committed (pg, 121 name); 122 } 123 124 125 /** 126 * Start a transaction with deferred wire-out constraints. 127 * 128 * @param pg the database context 129 * @param name name of the transaction 130 * @return #GNUNET_OK on success 131 */ 132 static enum GNUNET_GenericReturnValue 133 start_deferred (struct TALER_EXCHANGEDB_PostgresContext *pg, 134 const char *name) 135 { 136 (void) name; 137 return TALER_EXCHANGEDB_start_deferred_wire_out (pg); 138 } 139 140 141 /** 142 * All four transaction starters behave the same way as far as the 143 * connection's bookkeeping is concerned. 144 * 145 * @param pg the database context 146 * @return 0 on success 147 */ 148 static int 149 check_transactions (struct TALER_EXCHANGEDB_PostgresContext *pg) 150 { 151 static enum GNUNET_GenericReturnValue (*const starters[])( 152 struct TALER_EXCHANGEDB_PostgresContext *, 153 const char *) = { 154 &start_serializable, 155 &start_read_only, 156 &start_read_committed, 157 &start_deferred 158 }; 159 160 FAILIF (NULL != pg->transaction_name); 161 for (unsigned int i = 0; i < 4; i++) 162 { 163 /* commit ends the transaction ... */ 164 FAILIF (GNUNET_OK != 165 starters[i](pg, 166 "misc-commit")); 167 FAILIF (NULL == pg->transaction_name); 168 FAILIF (0 > TALER_EXCHANGEDB_commit (pg)); 169 FAILIF (NULL != pg->transaction_name); 170 171 /* ... and so does a rollback */ 172 FAILIF (GNUNET_OK != 173 starters[i](pg, 174 "misc-rollback")); 175 FAILIF (NULL == pg->transaction_name); 176 TALER_EXCHANGEDB_rollback (pg); 177 FAILIF (NULL != pg->transaction_name); 178 } 179 180 /* rolling back when nothing is open is a no-op */ 181 TALER_EXCHANGEDB_rollback (pg); 182 FAILIF (NULL != pg->transaction_name); 183 return 0; 184 } 185 186 187 /** 188 * A transaction really isolates: what it wrote is gone after a rollback 189 * and there after a commit. 190 * 191 * @param pg the database context 192 * @return 0 on success 193 */ 194 static int 195 check_isolation (struct TALER_EXCHANGEDB_PostgresContext *pg) 196 { 197 FAILIF (GNUNET_OK != 198 TALER_EXCHANGEDB_start (pg, 199 "misc-isolation")); 200 TDB_account (pg, 201 10, 202 &account); 203 FAILIF (1 != TDB_count (pg, 204 "FROM wire_targets")); 205 TALER_EXCHANGEDB_rollback (pg); 206 FAILIF (0 != TDB_count (pg, 207 "FROM wire_targets")); 208 209 FAILIF (GNUNET_OK != 210 TALER_EXCHANGEDB_start (pg, 211 "misc-isolation")); 212 TDB_account (pg, 213 10, 214 &account); 215 FAILIF (0 > TALER_EXCHANGEDB_commit (pg)); 216 FAILIF (1 != TDB_count (pg, 217 "FROM wire_targets")); 218 return 0; 219 } 220 221 222 /** 223 * Preflight is happy on an idle connection and cleans up after a caller 224 * that forgot to end its transaction. 225 * 226 * @param pg the database context 227 * @return 0 on success 228 */ 229 static int 230 check_preflight (struct TALER_EXCHANGEDB_PostgresContext *pg) 231 { 232 FAILIF (GNUNET_OK != 233 TALER_EXCHANGEDB_preflight (pg)); 234 235 /* a forgotten transaction is rolled back, and preflight says so */ 236 FAILIF (GNUNET_OK != 237 TALER_EXCHANGEDB_start (pg, 238 "misc-leak")); 239 FAILIF (GNUNET_NO != 240 TALER_EXCHANGEDB_preflight (pg)); 241 FAILIF (NULL != pg->transaction_name); 242 FAILIF (GNUNET_OK != 243 TALER_EXCHANGEDB_preflight (pg)); 244 245 /* which is also what start() does for us */ 246 FAILIF (GNUNET_OK != 247 TALER_EXCHANGEDB_start (pg, 248 "misc-leak")); 249 FAILIF (GNUNET_OK != 250 TALER_EXCHANGEDB_start (pg, 251 "misc-after-leak")); 252 TALER_EXCHANGEDB_rollback (pg); 253 return 0; 254 } 255 256 257 /** 258 * The shard of a merchant key is deterministic and stays in range. 259 * 260 * @param pg the database context 261 * @return 0 on success 262 */ 263 static int 264 check_compute_shard (struct TALER_EXCHANGEDB_PostgresContext *pg) 265 { 266 struct TALER_MerchantPublicKeyP m1; 267 struct TALER_MerchantPublicKeyP m2; 268 uint64_t s1; 269 uint64_t s2; 270 271 (void) pg; 272 TDB_FILL (m1, 273 1); 274 TDB_FILL (m2, 275 2); 276 s1 = TALER_EXCHANGEDB_compute_shard (&m1); 277 s2 = TALER_EXCHANGEDB_compute_shard (&m2); 278 FAILIF (s1 > INT32_MAX); 279 FAILIF (s2 > INT32_MAX); 280 FAILIF (s1 == s2); 281 /* the same key always maps to the same shard, otherwise the shard 282 locks of the aggregator would not partition the work */ 283 FAILIF (s1 != TALER_EXCHANGEDB_compute_shard (&m1)); 284 FAILIF (s2 != TALER_EXCHANGEDB_compute_shard (&m2)); 285 return 0; 286 } 287 288 289 /** 290 * The coin transaction list adds up and can be released. 291 * 292 * @param pg the database context 293 * @return 0 on success 294 */ 295 static int 296 check_coin_list (struct TALER_EXCHANGEDB_PostgresContext *pg) 297 { 298 struct TALER_EXCHANGEDB_TransactionList *tl = NULL; 299 struct TALER_CoinPublicInfo coin; 300 struct TDB_Deposit dep; 301 struct TALER_Amount balance; 302 struct TALER_Amount total; 303 struct TALER_Amount zero = TDB_amount ("0"); 304 struct TALER_Amount one = TDB_amount ("1"); 305 struct TALER_Amount two = TDB_amount ("2"); 306 struct TALER_DenominationHashP h_denom_pub; 307 uint64_t etag = 0; 308 309 /* an empty list is worth the offset it started from */ 310 FAILIF (GNUNET_OK != 311 TALER_EXCHANGEDB_calculate_transaction_list_totals (NULL, 312 &one, 313 &total)); 314 FAILIF (0 != TALER_amount_cmp (&total, 315 &one)); 316 /* and freeing it is a no-op */ 317 TALER_EXCHANGEDB_free_coin_transaction_list (NULL); 318 319 TDB_denom (pg, 320 10, 321 "5", 322 "0.1", 323 &denom); 324 TDB_coin (pg, 325 &denom, 326 20, 327 &coin, 328 NULL); 329 TDB_deposit (pg, 330 &account, 331 &coin, 332 30, 333 "1", 334 "0.1", 335 GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS), 336 GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS), 337 &dep); 338 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 339 TALER_EXCHANGEDB_get_coin_transactions (pg, 340 true, 341 &coin.coin_pub, 342 0, 343 0, 344 &etag, 345 &balance, 346 &h_denom_pub, 347 &tl), 348 TDB_coin_free (&coin)); 349 TDB_coin_free (&coin); 350 FAILIF (NULL == tl); 351 FAILIF_C (GNUNET_OK != 352 TALER_EXCHANGEDB_calculate_transaction_list_totals (tl, 353 &zero, 354 &total), 355 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 356 FAILIF_C (0 != TALER_amount_cmp (&total, 357 &one), 358 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 359 /* the offset is added on top */ 360 FAILIF_C (GNUNET_OK != 361 TALER_EXCHANGEDB_calculate_transaction_list_totals (tl, 362 &one, 363 &total), 364 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 365 FAILIF_C (0 != TALER_amount_cmp (&total, 366 &two), 367 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 368 TALER_EXCHANGEDB_free_coin_transaction_list (tl); 369 return 0; 370 } 371 372 373 /** 374 * The reserve history can be released. 375 * 376 * @param pg the database context 377 * @return 0 on success 378 */ 379 static int 380 check_reserve_history (struct TALER_EXCHANGEDB_PostgresContext *pg) 381 { 382 struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; 383 struct TALER_ReservePublicKeyP reserve_pub; 384 struct TALER_Amount balance; 385 uint64_t etag = 0; 386 unsigned int cnt = 0; 387 388 /* freeing nothing is a no-op */ 389 TALER_EXCHANGEDB_free_reserve_history (NULL); 390 391 TDB_reserve_in (pg, 392 &account, 393 11, 394 "10", 395 &reserve_pub); 396 (void) TDB_withdraw (pg, 397 &denom, 398 &reserve_pub, 399 11, 400 "5"); 401 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 402 TALER_EXCHANGEDB_get_reserve_history (pg, 403 &reserve_pub, 404 0, 405 0, 406 &etag, 407 &balance, 408 &rh)); 409 FAILIF (NULL == rh); 410 for (const struct TALER_EXCHANGEDB_ReserveHistory *pos = rh; 411 NULL != pos; 412 pos = pos->next) 413 cnt++; 414 FAILIF_C (2 != cnt, 415 TALER_EXCHANGEDB_free_reserve_history (rh)); 416 TALER_EXCHANGEDB_free_reserve_history (rh); 417 return 0; 418 } 419 420 421 /** 422 * State of the event check. 423 */ 424 struct EventState 425 { 426 /** 427 * How often did the callback fire with a payload? 428 */ 429 unsigned int fired; 430 431 /** 432 * Payload of the last notification. 433 */ 434 char payload[16]; 435 436 /** 437 * Number of bytes in @e payload. 438 */ 439 size_t payload_len; 440 }; 441 442 443 /** 444 * Payload of the notification. 445 */ 446 #define PAYLOAD "hello" 447 448 449 /** 450 * Called when the notification arrives (or the listener times out). 451 * 452 * @param cls a `struct EventState *` 453 * @param extra the payload, NULL on timeout 454 * @param extra_size number of bytes in @a extra 455 */ 456 static void 457 event_cb (void *cls, 458 const void *extra, 459 size_t extra_size) 460 { 461 struct EventState *st = cls; 462 463 if (NULL == extra) 464 return; /* timeout event, not what we are after */ 465 st->fired++; 466 st->payload_len = GNUNET_MIN (extra_size, 467 sizeof (st->payload)); 468 memcpy (st->payload, 469 extra, 470 st->payload_len); 471 } 472 473 474 /** 475 * A notification sent on the connection reaches a listener registered on 476 * it, and stops reaching it once the listener is cancelled. 477 * 478 * @param pg the database context 479 * @return 0 on success 480 */ 481 static int 482 check_events (struct TALER_EXCHANGEDB_PostgresContext *pg) 483 { 484 struct GNUNET_DB_EventHeaderP es = { 485 .size = htons (sizeof (es)), 486 .type = htons (TALER_DBEVENT_EXCHANGE_KYC_COMPLETED) 487 }; 488 struct GNUNET_DB_EventHandler *eh; 489 struct EventState st = { 0 }; 490 491 eh = TALER_EXCHANGEDB_event_listen (pg, 492 GNUNET_TIME_UNIT_MINUTES, 493 &es, 494 &event_cb, 495 &st); 496 FAILIF (NULL == eh); 497 TALER_EXCHANGEDB_event_notify (pg, 498 &es, 499 PAYLOAD, 500 strlen (PAYLOAD)); 501 /* the scheduler task that would pick this up does not get to run while 502 the checks are executing, so poll for it ourselves -- which is also 503 what the exchange does after every notifying transaction */ 504 GNUNET_PQ_event_do_poll (pg->conn); 505 FAILIF_C (1 != st.fired, 506 TALER_EXCHANGEDB_event_listen_cancel (pg, eh)); 507 FAILIF_C (strlen (PAYLOAD) != st.payload_len, 508 TALER_EXCHANGEDB_event_listen_cancel (pg, eh)); 509 FAILIF_C (0 != memcmp (st.payload, 510 PAYLOAD, 511 st.payload_len), 512 TALER_EXCHANGEDB_event_listen_cancel (pg, eh)); 513 514 /* an unrelated event does not wake us */ 515 { 516 struct GNUNET_DB_EventHeaderP other = { 517 .size = htons (sizeof (other)), 518 .type = htons (TALER_DBEVENT_EXCHANGE_NEW_KYC_ATTRIBUTES) 519 }; 520 521 TALER_EXCHANGEDB_event_notify (pg, 522 &other, 523 PAYLOAD, 524 strlen (PAYLOAD)); 525 GNUNET_PQ_event_do_poll (pg->conn); 526 FAILIF_C (1 != st.fired, 527 TALER_EXCHANGEDB_event_listen_cancel (pg, eh)); 528 } 529 530 /* and after cancelling, neither does the one we listened for */ 531 TALER_EXCHANGEDB_event_listen_cancel (pg, 532 eh); 533 TALER_EXCHANGEDB_event_notify (pg, 534 &es, 535 PAYLOAD, 536 strlen (PAYLOAD)); 537 GNUNET_PQ_event_do_poll (pg->conn); 538 FAILIF (1 != st.fired); 539 return 0; 540 } 541 542 543 /** 544 * Customization rules that were never deployed cannot be dropped. 545 * 546 * @param pg the database context 547 * @return 0 on success 548 */ 549 static int 550 check_rules (struct TALER_EXCHANGEDB_PostgresContext *pg) 551 { 552 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 553 TALER_EXCHANGEDB_disable_rules (pg, 554 "no-such-customization")); 555 /* and a schema without SQL files on disk cannot be deployed; the 556 search_path must be back to `exchange' afterwards either way */ 557 FAILIF (GNUNET_SYSERR == 558 TALER_EXCHANGEDB_enable_rules (pg, 559 "no-such-customization")); 560 /* the search_path is back to `exchange', so an unqualified query 561 still finds the exchange's own tables */ 562 FAILIF (1 != TDB_count (pg, 563 "FROM wire_targets")); 564 return 0; 565 } 566 567 568 /** 569 * The auditor triggers can be injected into an existing schema. 570 * 571 * @param pg the database context 572 * @return 0 on success 573 */ 574 static int 575 check_auditor_triggers (struct TALER_EXCHANGEDB_PostgresContext *pg) 576 { 577 FAILIF (GNUNET_OK != 578 TALER_EXCHANGEDB_inject_auditor_triggers (pg)); 579 return 0; 580 } 581 582 583 /** 584 * Dropping and re-creating the schema leaves an empty but usable 585 * database. Nothing may run after this check. 586 * 587 * @param pg the database context 588 * @return 0 on success 589 */ 590 static int 591 check_schema (struct TALER_EXCHANGEDB_PostgresContext *pg) 592 { 593 FAILIF (0 == TDB_count (pg, 594 "FROM wire_targets")); 595 FAILIF (GNUNET_OK != 596 TALER_EXCHANGEDB_drop_tables (pg)); 597 /* dropping twice fails: drop.sql says `DROP SCHEMA exchange CASCADE' 598 without IF EXISTS, so there is nothing left to drop the second time */ 599 FAILIF (GNUNET_OK == 600 TALER_EXCHANGEDB_drop_tables (pg)); 601 FAILIF (GNUNET_OK != 602 TALER_EXCHANGEDB_create_tables (pg, 603 false, 604 0)); 605 FAILIF (0 != TDB_count (pg, 606 "FROM wire_targets")); 607 return 0; 608 } 609 610 611 /** 612 * The checks to run, in order. 613 */ 614 static const struct TDB_Test tests[] = { 615 { "misc-transactions", 616 &check_transactions }, 617 { "misc-isolation", 618 &check_isolation }, 619 { "misc-preflight", 620 &check_preflight }, 621 { "misc-compute-shard", 622 &check_compute_shard }, 623 { "misc-coin-list", 624 &check_coin_list }, 625 { "misc-reserve-history", 626 &check_reserve_history }, 627 { "misc-events", 628 &check_events }, 629 { "misc-rules", 630 &check_rules }, 631 { "misc-auditor-triggers", 632 &check_auditor_triggers }, 633 { "misc-schema", 634 &check_schema }, 635 { NULL, NULL } 636 }; 637 638 639 int 640 main (int argc, 641 char *const *argv) 642 { 643 int ret; 644 645 ret = TDB_main (argc, 646 argv, 647 "test-misc", 648 "Tests for the exchangedb functions without a table", 649 tests); 650 TDB_denom_free (&denom); 651 TDB_account_free (&account); 652 return ret; 653 } 654 655 656 /* end of test_misc.c */