test_misc.c (19653B)
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 TDB_account_free (&account); 209 210 FAILIF (GNUNET_OK != 211 TALER_EXCHANGEDB_start (pg, 212 "misc-isolation")); 213 TDB_account (pg, 214 10, 215 &account); 216 FAILIF (0 > TALER_EXCHANGEDB_commit (pg)); 217 FAILIF (1 != TDB_count (pg, 218 "FROM wire_targets")); 219 return 0; 220 } 221 222 223 /** 224 * Preflight is happy on an idle connection and cleans up after a caller 225 * that forgot to end its transaction. 226 * 227 * @param pg the database context 228 * @return 0 on success 229 */ 230 static int 231 check_preflight (struct TALER_EXCHANGEDB_PostgresContext *pg) 232 { 233 FAILIF (GNUNET_OK != 234 TALER_EXCHANGEDB_preflight (pg)); 235 236 /* a forgotten transaction is rolled back, and preflight says so */ 237 FAILIF (GNUNET_OK != 238 TALER_EXCHANGEDB_start (pg, 239 "misc-leak")); 240 FAILIF (GNUNET_NO != 241 TALER_EXCHANGEDB_preflight (pg)); 242 FAILIF (NULL != pg->transaction_name); 243 FAILIF (GNUNET_OK != 244 TALER_EXCHANGEDB_preflight (pg)); 245 246 /* which is also what start() does for us */ 247 FAILIF (GNUNET_OK != 248 TALER_EXCHANGEDB_start (pg, 249 "misc-leak")); 250 FAILIF (GNUNET_OK != 251 TALER_EXCHANGEDB_start (pg, 252 "misc-after-leak")); 253 TALER_EXCHANGEDB_rollback (pg); 254 return 0; 255 } 256 257 258 /** 259 * The shard of a merchant key is deterministic and stays in range. 260 * 261 * @param pg the database context 262 * @return 0 on success 263 */ 264 static int 265 check_compute_shard (struct TALER_EXCHANGEDB_PostgresContext *pg) 266 { 267 struct TALER_MerchantPublicKeyP m1; 268 struct TALER_MerchantPublicKeyP m2; 269 uint64_t s1; 270 uint64_t s2; 271 272 (void) pg; 273 TDB_FILL (m1, 274 1); 275 TDB_FILL (m2, 276 2); 277 s1 = TALER_EXCHANGEDB_compute_shard (&m1); 278 s2 = TALER_EXCHANGEDB_compute_shard (&m2); 279 FAILIF (s1 > INT32_MAX); 280 FAILIF (s2 > INT32_MAX); 281 FAILIF (s1 == s2); 282 /* the same key always maps to the same shard, otherwise the shard 283 locks of the aggregator would not partition the work */ 284 FAILIF (s1 != TALER_EXCHANGEDB_compute_shard (&m1)); 285 FAILIF (s2 != TALER_EXCHANGEDB_compute_shard (&m2)); 286 return 0; 287 } 288 289 290 /** 291 * The coin transaction list adds up and can be released. 292 * 293 * @param pg the database context 294 * @return 0 on success 295 */ 296 static int 297 check_coin_list (struct TALER_EXCHANGEDB_PostgresContext *pg) 298 { 299 struct TALER_EXCHANGEDB_TransactionList *tl = NULL; 300 struct TALER_CoinPublicInfo coin; 301 struct TDB_Deposit dep; 302 struct TALER_Amount balance; 303 struct TALER_Amount total; 304 struct TALER_Amount zero = TDB_amount ("0"); 305 struct TALER_Amount one = TDB_amount ("1"); 306 struct TALER_Amount two = TDB_amount ("2"); 307 struct TALER_DenominationHashP h_denom_pub; 308 uint64_t etag = 0; 309 310 /* an empty list is worth the offset it started from */ 311 FAILIF (GNUNET_OK != 312 TALER_EXCHANGEDB_calculate_transaction_list_totals (NULL, 313 &one, 314 &total)); 315 FAILIF (0 != TALER_amount_cmp (&total, 316 &one)); 317 /* and freeing it is a no-op */ 318 TALER_EXCHANGEDB_free_coin_transaction_list (NULL); 319 320 TDB_denom (pg, 321 10, 322 "5", 323 "0.1", 324 &denom); 325 TDB_coin (pg, 326 &denom, 327 20, 328 &coin, 329 NULL); 330 TDB_deposit (pg, 331 &account, 332 &coin, 333 30, 334 "1", 335 "0.1", 336 GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS), 337 GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS), 338 &dep); 339 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 340 TALER_EXCHANGEDB_get_coin_transactions (pg, 341 true, 342 &coin.coin_pub, 343 0, 344 0, 345 &etag, 346 &balance, 347 &h_denom_pub, 348 &tl), 349 TDB_coin_free (&coin)); 350 TDB_coin_free (&coin); 351 FAILIF (NULL == tl); 352 FAILIF_C (GNUNET_OK != 353 TALER_EXCHANGEDB_calculate_transaction_list_totals (tl, 354 &zero, 355 &total), 356 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 357 FAILIF_C (0 != TALER_amount_cmp (&total, 358 &one), 359 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 360 /* the offset is added on top */ 361 FAILIF_C (GNUNET_OK != 362 TALER_EXCHANGEDB_calculate_transaction_list_totals (tl, 363 &one, 364 &total), 365 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 366 FAILIF_C (0 != TALER_amount_cmp (&total, 367 &two), 368 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 369 TALER_EXCHANGEDB_free_coin_transaction_list (tl); 370 return 0; 371 } 372 373 374 /** 375 * The reserve history can be released. 376 * 377 * @param pg the database context 378 * @return 0 on success 379 */ 380 static int 381 check_reserve_history (struct TALER_EXCHANGEDB_PostgresContext *pg) 382 { 383 struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; 384 struct TALER_ReservePublicKeyP reserve_pub; 385 struct TALER_Amount balance; 386 uint64_t etag = 0; 387 unsigned int cnt = 0; 388 389 /* freeing nothing is a no-op */ 390 TALER_EXCHANGEDB_free_reserve_history (NULL); 391 392 TDB_reserve_in (pg, 393 &account, 394 11, 395 "10", 396 &reserve_pub); 397 (void) TDB_withdraw (pg, 398 &denom, 399 &reserve_pub, 400 11, 401 "5"); 402 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 403 TALER_EXCHANGEDB_get_reserve_history (pg, 404 &reserve_pub, 405 0, 406 0, 407 &etag, 408 &balance, 409 &rh)); 410 FAILIF (NULL == rh); 411 for (const struct TALER_EXCHANGEDB_ReserveHistory *pos = rh; 412 NULL != pos; 413 pos = pos->next) 414 cnt++; 415 FAILIF_C (2 != cnt, 416 TALER_EXCHANGEDB_free_reserve_history (rh)); 417 TALER_EXCHANGEDB_free_reserve_history (rh); 418 return 0; 419 } 420 421 422 /** 423 * State of the event check. 424 */ 425 struct EventState 426 { 427 /** 428 * How often did the callback fire with a payload? 429 */ 430 unsigned int fired; 431 432 /** 433 * Payload of the last notification. 434 */ 435 char payload[16]; 436 437 /** 438 * Number of bytes in @e payload. 439 */ 440 size_t payload_len; 441 }; 442 443 444 /** 445 * Payload of the notification. 446 */ 447 #define PAYLOAD "hello" 448 449 450 /** 451 * Called when the notification arrives (or the listener times out). 452 * 453 * @param cls a `struct EventState *` 454 * @param extra the payload, NULL on timeout 455 * @param extra_size number of bytes in @a extra 456 */ 457 static void 458 event_cb (void *cls, 459 const void *extra, 460 size_t extra_size) 461 { 462 struct EventState *st = cls; 463 464 if (NULL == extra) 465 return; /* timeout event, not what we are after */ 466 st->fired++; 467 st->payload_len = GNUNET_MIN (extra_size, 468 sizeof (st->payload)); 469 memcpy (st->payload, 470 extra, 471 st->payload_len); 472 } 473 474 475 /** 476 * A notification sent on the connection reaches a listener registered on 477 * it, and stops reaching it once the listener is cancelled. 478 * 479 * @param pg the database context 480 * @return 0 on success 481 */ 482 static int 483 check_events (struct TALER_EXCHANGEDB_PostgresContext *pg) 484 { 485 struct GNUNET_DB_EventHeaderP es = { 486 .size = htons (sizeof (es)), 487 .type = htons (TALER_DBEVENT_EXCHANGE_KYC_COMPLETED) 488 }; 489 struct GNUNET_DB_EventHandler *eh; 490 struct EventState st = { 0 }; 491 492 eh = TALER_EXCHANGEDB_event_listen (pg, 493 GNUNET_TIME_UNIT_MINUTES, 494 &es, 495 &event_cb, 496 &st); 497 FAILIF (NULL == eh); 498 TALER_EXCHANGEDB_event_notify (pg, 499 &es, 500 PAYLOAD, 501 strlen (PAYLOAD)); 502 /* the scheduler task that would pick this up does not get to run while 503 the checks are executing, so poll for it ourselves -- which is also 504 what the exchange does after every notifying transaction */ 505 GNUNET_PQ_event_do_poll (pg->conn); 506 FAILIF_C (1 != st.fired, 507 TALER_EXCHANGEDB_event_listen_cancel (pg, eh)); 508 FAILIF_C (strlen (PAYLOAD) != st.payload_len, 509 TALER_EXCHANGEDB_event_listen_cancel (pg, eh)); 510 FAILIF_C (0 != memcmp (st.payload, 511 PAYLOAD, 512 st.payload_len), 513 TALER_EXCHANGEDB_event_listen_cancel (pg, eh)); 514 515 /* an unrelated event does not wake us */ 516 { 517 struct GNUNET_DB_EventHeaderP other = { 518 .size = htons (sizeof (other)), 519 .type = htons (TALER_DBEVENT_EXCHANGE_NEW_KYC_ATTRIBUTES) 520 }; 521 522 TALER_EXCHANGEDB_event_notify (pg, 523 &other, 524 PAYLOAD, 525 strlen (PAYLOAD)); 526 GNUNET_PQ_event_do_poll (pg->conn); 527 FAILIF_C (1 != st.fired, 528 TALER_EXCHANGEDB_event_listen_cancel (pg, eh)); 529 } 530 531 /* and after cancelling, neither does the one we listened for */ 532 TALER_EXCHANGEDB_event_listen_cancel (pg, 533 eh); 534 TALER_EXCHANGEDB_event_notify (pg, 535 &es, 536 PAYLOAD, 537 strlen (PAYLOAD)); 538 GNUNET_PQ_event_do_poll (pg->conn); 539 FAILIF (1 != st.fired); 540 return 0; 541 } 542 543 544 /** 545 * Customization rules that were never deployed cannot be dropped. 546 * 547 * @param pg the database context 548 * @return 0 on success 549 */ 550 static int 551 check_rules (struct TALER_EXCHANGEDB_PostgresContext *pg) 552 { 553 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 554 TALER_EXCHANGEDB_disable_rules (pg, 555 "no-such-customization")); 556 /* and a schema without SQL files on disk cannot be deployed; the 557 search_path must be back to `exchange' afterwards either way */ 558 FAILIF (GNUNET_SYSERR == 559 TALER_EXCHANGEDB_enable_rules (pg, 560 "no-such-customization")); 561 /* the search_path is back to `exchange', so an unqualified query 562 still finds the exchange's own tables */ 563 FAILIF (1 != TDB_count (pg, 564 "FROM wire_targets")); 565 return 0; 566 } 567 568 569 /** 570 * The auditor triggers can be injected into an existing schema. 571 * 572 * @param pg the database context 573 * @return 0 on success 574 */ 575 static int 576 check_auditor_triggers (struct TALER_EXCHANGEDB_PostgresContext *pg) 577 { 578 FAILIF (GNUNET_OK != 579 TALER_EXCHANGEDB_inject_auditor_triggers (pg)); 580 return 0; 581 } 582 583 584 /** 585 * Dropping and re-creating the schema leaves an empty but usable 586 * database. Nothing may run after this check. 587 * 588 * @param pg the database context 589 * @return 0 on success 590 */ 591 static int 592 check_schema (struct TALER_EXCHANGEDB_PostgresContext *pg) 593 { 594 FAILIF (0 == TDB_count (pg, 595 "FROM wire_targets")); 596 FAILIF (GNUNET_OK != 597 TALER_EXCHANGEDB_drop_tables (pg)); 598 /* dropping twice fails: drop.sql says `DROP SCHEMA exchange CASCADE' 599 without IF EXISTS, so there is nothing left to drop the second time */ 600 FAILIF (GNUNET_OK == 601 TALER_EXCHANGEDB_drop_tables (pg)); 602 FAILIF (GNUNET_OK != 603 TALER_EXCHANGEDB_create_tables (pg, 604 false, 605 0)); 606 FAILIF (0 != TDB_count (pg, 607 "FROM wire_targets")); 608 return 0; 609 } 610 611 612 /** 613 * The checks to run, in order. 614 */ 615 static const struct TDB_Test tests[] = { 616 { "misc-transactions", 617 &check_transactions }, 618 { "misc-isolation", 619 &check_isolation }, 620 { "misc-preflight", 621 &check_preflight }, 622 { "misc-compute-shard", 623 &check_compute_shard }, 624 { "misc-coin-list", 625 &check_coin_list }, 626 { "misc-reserve-history", 627 &check_reserve_history }, 628 { "misc-events", 629 &check_events }, 630 { "misc-rules", 631 &check_rules }, 632 { "misc-auditor-triggers", 633 &check_auditor_triggers }, 634 { "misc-schema", 635 &check_schema }, 636 { NULL, NULL } 637 }; 638 639 640 int 641 main (int argc, 642 char *const *argv) 643 { 644 int ret; 645 646 ret = TDB_main (argc, 647 argv, 648 "test-misc", 649 "Tests for the exchangedb functions without a table", 650 tests); 651 TDB_denom_free (&denom); 652 TDB_account_free (&account); 653 return ret; 654 } 655 656 657 /* end of test_misc.c */