test_reserves_in.c (22567B)
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_reserves_in.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `reserves_in` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_do_import_credits() (for its reserve half; the 23 * KYC-auth half is in test_kycauths_in.c), 24 * #TALER_EXCHANGEDB_get_reserve_origin(), 25 * #TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id(), 26 * #TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id_by_account() and 27 * #TALER_EXCHANGEDB_iterate_exchange_credit_transfers(). 28 * 29 * do_import_credits() creates the `wire_targets` and `reserves` rows it 30 * needs on its own, so the only fixture the checks have to set up is a work 31 * shard for the batch to advance. 32 */ 33 #include "test_common.h" 34 #include "exchange-database/begin_shard.h" 35 #include "exchange-database/do_import_credits.h" 36 #include "exchange-database/get_reserve_origin.h" 37 #include "exchange-database/iterate_exchange_credit_transfers.h" 38 #include "exchange-database/iterate_reserves_in_above_serial_id.h" 39 #include "exchange-database/iterate_reserves_in_above_serial_id_by_account.h" 40 41 42 /** 43 * Account the first two transfers come from. 44 */ 45 #define PAYTO_A "payto://x-taler-bank/localhost/ri-a?receiver-name=A" 46 47 /** 48 * Account the third transfer comes from. 49 */ 50 #define PAYTO_B "payto://x-taler-bank/localhost/ri-b?receiver-name=B" 51 52 53 /** 54 * Closure for #reserve_in_cb(). 55 */ 56 struct InContext 57 { 58 /** 59 * How many rows did the callback see? 60 */ 61 unsigned int total; 62 63 /** 64 * Stop after this many rows; 0 for no limit. 65 */ 66 unsigned int stop_after; 67 68 /** 69 * Reserve we are looking for, NULL to match nothing. 70 */ 71 const struct TALER_ReservePublicKeyP *reserve_pub; 72 73 /** 74 * How many times did we see @e reserve_pub? 75 */ 76 unsigned int matched; 77 78 /** 79 * Credit reported for @e reserve_pub. 80 */ 81 struct TALER_Amount credit; 82 83 /** 84 * Row reported for @e reserve_pub. 85 */ 86 uint64_t rowid; 87 88 /** 89 * Wire reference reported for @e reserve_pub. 90 */ 91 uint64_t wire_reference; 92 93 /** 94 * Sender account reported for @e reserve_pub, owned by this struct. 95 */ 96 char *sender; 97 }; 98 99 100 /** 101 * Callback for the `reserves_in` iterators. 102 * 103 * @param cls a `struct InContext *` 104 * @param rowid row of the transfer 105 * @param reserve_pub reserve that was credited 106 * @param credit amount that was credited 107 * @param sender_account_details account the money came from 108 * @param wire_reference bank's identifier for the transfer 109 * @param execution_date when the transfer was made 110 * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop 111 */ 112 static enum GNUNET_GenericReturnValue 113 reserve_in_cb (void *cls, 114 uint64_t rowid, 115 const struct TALER_ReservePublicKeyP *reserve_pub, 116 const struct TALER_Amount *credit, 117 const struct TALER_FullPayto sender_account_details, 118 uint64_t wire_reference, 119 struct GNUNET_TIME_Timestamp execution_date) 120 { 121 struct InContext *ctx = cls; 122 123 (void) execution_date; 124 ctx->total++; 125 if ( (NULL != ctx->reserve_pub) && 126 (0 == GNUNET_memcmp (reserve_pub, 127 ctx->reserve_pub)) ) 128 { 129 ctx->matched++; 130 ctx->credit = *credit; 131 ctx->rowid = rowid; 132 ctx->wire_reference = wire_reference; 133 GNUNET_free (ctx->sender); 134 ctx->sender = (NULL == sender_account_details.full_payto) 135 ? NULL 136 : GNUNET_strdup (sender_account_details.full_payto); 137 } 138 if ( (0 != ctx->stop_after) && 139 (ctx->total >= ctx->stop_after) ) 140 return GNUNET_SYSERR; 141 return GNUNET_OK; 142 } 143 144 145 /** 146 * Closure for #transfer_cb(). 147 */ 148 struct TransferContext 149 { 150 /** 151 * How many transfers did the callback see? 152 */ 153 unsigned int total; 154 155 /** 156 * Row of the first transfer seen. 157 */ 158 uint64_t first_row; 159 160 /** 161 * Sum of the whole-unit parts of the amounts seen. 162 */ 163 uint64_t value_sum; 164 }; 165 166 167 /** 168 * Callback for #TALER_EXCHANGEDB_iterate_exchange_credit_transfers(). 169 * 170 * @param cls a `struct TransferContext *` 171 * @param row_id row of the transfer 172 * @param payto_uri account the money came from 173 * @param execution_time when the transfer was made 174 * @param amount amount that was transferred 175 */ 176 static void 177 transfer_cb (void *cls, 178 uint64_t row_id, 179 const char *payto_uri, 180 struct GNUNET_TIME_Absolute execution_time, 181 const struct TALER_Amount *amount) 182 { 183 struct TransferContext *ctx = cls; 184 185 (void) payto_uri; 186 (void) execution_time; 187 if (0 == ctx->total++) 188 ctx->first_row = row_id; 189 ctx->value_sum += amount->value; 190 } 191 192 193 /** 194 * Import one wire transfer into a reserve, advancing a work shard of its 195 * own along with it. 196 * 197 * @param pg the database context 198 * @param job_name job whose shard to open and advance 199 * @param account_name exchange bank account the transfer arrived at 200 * @param payto account the money came from 201 * @param seed seed for the reserve's public key 202 * @param amount amount that was transferred, e.g. "10" 203 * @param[out] reserve_pub set to the credited reserve 204 * @param[out] status set to the per-transfer status 205 * @return transaction status of the import 206 */ 207 static enum GNUNET_DB_QueryStatus 208 import_one (struct TALER_EXCHANGEDB_PostgresContext *pg, 209 const char *job_name, 210 const char *account_name, 211 const char *payto, 212 uint32_t seed, 213 const char *amount, 214 struct TALER_ReservePublicKeyP *reserve_pub, 215 enum GNUNET_DB_QueryStatus *status) 216 { 217 struct TALER_Amount balance = TDB_amount (amount); 218 struct TALER_EXCHANGEDB_ReserveInInfo info; 219 struct TALER_EXCHANGEDB_CreditBatch batch; 220 uint64_t start; 221 uint64_t end; 222 uint64_t progress; 223 224 GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == 225 TALER_EXCHANGEDB_begin_shard (pg, 226 job_name, 227 GNUNET_TIME_UNIT_HOURS, 228 1024, 229 &start, 230 &end, 231 &progress)); 232 TDB_fill (reserve_pub, 233 sizeof (*reserve_pub), 234 seed); 235 memset (&info, 236 0, 237 sizeof (info)); 238 info.reserve_pub = reserve_pub; 239 info.balance = &balance; 240 info.execution_time = GNUNET_TIME_timestamp_get (); 241 info.sender_account_details.full_payto = (char *) payto; 242 info.wire_reference = start + 1; 243 memset (&batch, 244 0, 245 sizeof (batch)); 246 batch.exchange_account_name = account_name; 247 batch.reserves = &info; 248 batch.reserves_length = 1; 249 batch.job_name = job_name; 250 batch.shard_start = start; 251 batch.shard_end = end; 252 batch.progress_row = end; 253 batch.lease = GNUNET_TIME_UNIT_HOURS; 254 return TALER_EXCHANGEDB_do_import_credits (pg, 255 &batch, 256 status); 257 } 258 259 260 /** 261 * Nothing is known while the table is empty. 262 * 263 * @param pg the database context 264 * @return 0 on success 265 */ 266 static int 267 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 268 { 269 struct TALER_ReservePublicKeyP reserve_pub; 270 struct TALER_FullPaytoHashP h_payto; 271 struct TALER_FullPayto payto = { NULL }; 272 struct InContext ctx = { 0 }; 273 struct TransferContext tctx = { 0 }; 274 struct TALER_Amount zero = TDB_amount ("0"); 275 276 TDB_FILL (reserve_pub, 277 1); 278 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 279 TALER_EXCHANGEDB_get_reserve_origin (pg, 280 &reserve_pub, 281 &h_payto, 282 &payto)); 283 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 284 TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (pg, 285 0, 286 &reserve_in_cb, 287 &ctx)); 288 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 289 TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id_by_account ( 290 pg, 291 "exchange-account-1", 292 0, 293 &reserve_in_cb, 294 &ctx)); 295 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 296 TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg, 297 &zero, 298 0, 299 10, 300 NULL, 301 &transfer_cb, 302 &tctx)); 303 FAILIF (0 != ctx.total); 304 FAILIF (0 != tctx.total); 305 return 0; 306 } 307 308 309 /** 310 * Importing a transfer creates the reserve, the account and the 311 * `reserves_in` row, and doing it again is a no-op. 312 * 313 * @param pg the database context 314 * @return 0 on success 315 */ 316 static int 317 check_import (struct TALER_EXCHANGEDB_PostgresContext *pg) 318 { 319 struct TALER_ReservePublicKeyP reserve_pub; 320 struct TALER_FullPaytoHashP h_payto; 321 struct TALER_FullPayto payto = { NULL }; 322 struct TALER_FullPayto expect = { 323 .full_payto = (char *) PAYTO_A 324 }; 325 struct TALER_FullPaytoHashP expect_h; 326 struct TALER_Amount expect_amount = TDB_amount ("10"); 327 enum GNUNET_DB_QueryStatus status; 328 329 FAILIF (0 > 330 import_one (pg, 331 "ri-job-a", 332 "exchange-account-1", 333 PAYTO_A, 334 10, 335 "10", 336 &reserve_pub, 337 &status)); 338 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != status); 339 FAILIF (1 != TDB_count (pg, 340 "FROM reserves_in")); 341 FAILIF (1 != TDB_count (pg, 342 "FROM reserves")); 343 344 TALER_full_payto_hash (expect, 345 &expect_h); 346 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 347 TALER_EXCHANGEDB_get_reserve_origin (pg, 348 &reserve_pub, 349 &h_payto, 350 &payto)); 351 FAILIF (0 != GNUNET_memcmp (&h_payto, 352 &expect_h)); 353 FAILIF (0 != strcmp (payto.full_payto, 354 PAYTO_A)); 355 GNUNET_free (payto.full_payto); 356 357 /* the same transfer again: the row is already there */ 358 { 359 struct TALER_ReservePublicKeyP again; 360 361 FAILIF (0 > 362 import_one (pg, 363 "ri-job-a", 364 "exchange-account-1", 365 PAYTO_A, 366 10, 367 "10", 368 &again, 369 &status)); 370 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != status); 371 FAILIF (1 != TDB_count (pg, 372 "FROM reserves_in")); 373 } 374 375 { 376 struct InContext ctx; 377 378 memset (&ctx, 379 0, 380 sizeof (ctx)); 381 ctx.reserve_pub = &reserve_pub; 382 FAILIF (0 >= 383 TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id ( 384 pg, 385 0, 386 &reserve_in_cb, 387 &ctx)); 388 FAILIF_C (1 != ctx.matched, 389 GNUNET_free (ctx.sender)); 390 FAILIF_C (0 != TALER_amount_cmp (&ctx.credit, 391 &expect_amount), 392 GNUNET_free (ctx.sender)); 393 FAILIF_C (0 != strcmp (ctx.sender, 394 PAYTO_A), 395 GNUNET_free (ctx.sender)); 396 GNUNET_free (ctx.sender); 397 } 398 return 0; 399 } 400 401 402 /** 403 * With several transfers on file the iterators filter by serial and by 404 * exchange bank account, and honour an aborting callback. 405 * 406 * @param pg the database context 407 * @return 0 on success 408 */ 409 static int 410 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg) 411 { 412 struct TALER_ReservePublicKeyP r2; 413 struct TALER_ReservePublicKeyP r3; 414 struct InContext ctx; 415 enum GNUNET_DB_QueryStatus status; 416 417 /* a second transfer from the same account to the same exchange account */ 418 FAILIF (0 > 419 import_one (pg, 420 "ri-job-a", 421 "exchange-account-1", 422 PAYTO_A, 423 11, 424 "2", 425 &r2, 426 &status)); 427 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != status); 428 /* and one from a different account to a different exchange account */ 429 FAILIF (0 > 430 import_one (pg, 431 "ri-job-b", 432 "exchange-account-2", 433 PAYTO_B, 434 12, 435 "5", 436 &r3, 437 &status)); 438 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != status); 439 FAILIF (3 != TDB_count (pg, 440 "FROM reserves_in")); 441 442 memset (&ctx, 443 0, 444 sizeof (ctx)); 445 ctx.reserve_pub = &r3; 446 FAILIF (3 != 447 TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (pg, 448 0, 449 &reserve_in_cb, 450 &ctx)); 451 FAILIF_C (1 != ctx.matched, 452 GNUNET_free (ctx.sender)); 453 FAILIF_C (0 != strcmp (ctx.sender, 454 PAYTO_B), 455 GNUNET_free (ctx.sender)); 456 GNUNET_free (ctx.sender); 457 458 /* by exchange bank account */ 459 memset (&ctx, 460 0, 461 sizeof (ctx)); 462 FAILIF (2 != 463 TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id_by_account ( 464 pg, 465 "exchange-account-1", 466 0, 467 &reserve_in_cb, 468 &ctx)); 469 GNUNET_free (ctx.sender); 470 memset (&ctx, 471 0, 472 sizeof (ctx)); 473 FAILIF (1 != 474 TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id_by_account ( 475 pg, 476 "exchange-account-2", 477 0, 478 &reserve_in_cb, 479 &ctx)); 480 GNUNET_free (ctx.sender); 481 memset (&ctx, 482 0, 483 sizeof (ctx)); 484 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 485 TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id_by_account ( 486 pg, 487 "exchange-account-does-not-exist", 488 0, 489 &reserve_in_cb, 490 &ctx)); 491 FAILIF (0 != ctx.total); 492 493 /* A callback that gives up stops the iteration but is not an error: the 494 status stays the number of rows the query matched, not the number the 495 callback was shown. */ 496 memset (&ctx, 497 0, 498 sizeof (ctx)); 499 ctx.stop_after = 1; 500 FAILIF (3 != 501 TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (pg, 502 0, 503 &reserve_in_cb, 504 &ctx)); 505 FAILIF (1 != ctx.total); 506 GNUNET_free (ctx.sender); 507 return 0; 508 } 509 510 511 /** 512 * The serial number bound of the plain iterator excludes earlier rows. 513 * The bound is inclusive, so passing the row of the second transfer 514 * leaves the first one out and nothing else. 515 * 516 * @param pg the database context 517 * @return 0 on success 518 */ 519 static int 520 check_serial_bound (struct TALER_EXCHANGEDB_PostgresContext *pg) 521 { 522 struct InContext ctx; 523 524 FAILIF (3 != TDB_count (pg, 525 "FROM reserves_in")); 526 memset (&ctx, 527 0, 528 sizeof (ctx)); 529 FAILIF (2 != 530 TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (pg, 531 2, 532 &reserve_in_cb, 533 &ctx)); 534 GNUNET_free (ctx.sender); 535 memset (&ctx, 536 0, 537 sizeof (ctx)); 538 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 539 TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (pg, 540 1000, 541 &reserve_in_cb, 542 &ctx)); 543 GNUNET_free (ctx.sender); 544 return 0; 545 } 546 547 548 /** 549 * The AML view of the same rows filters by amount and by account, and can 550 * walk the table backwards. 551 * 552 * @param pg the database context 553 * @return 0 on success 554 */ 555 static int 556 check_credit_transfers (struct TALER_EXCHANGEDB_PostgresContext *pg) 557 { 558 struct TALER_NormalizedPayto npayto; 559 struct TALER_NormalizedPaytoHashP h_payto; 560 struct TALER_FullPayto payto_a = { 561 .full_payto = (char *) PAYTO_A 562 }; 563 struct TALER_Amount zero = TDB_amount ("0"); 564 struct TALER_Amount five = TDB_amount ("5"); 565 struct TALER_Amount huge = TDB_amount ("1000"); 566 struct TransferContext ctx; 567 uint64_t lowest; 568 569 /* everything, ascending */ 570 memset (&ctx, 571 0, 572 sizeof (ctx)); 573 FAILIF (3 != 574 TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg, 575 &zero, 576 0, 577 10, 578 NULL, 579 &transfer_cb, 580 &ctx)); 581 FAILIF (3 != ctx.total); 582 FAILIF (10 + 2 + 5 != ctx.value_sum); 583 lowest = ctx.first_row; 584 585 /* the amount threshold drops the EUR:2 transfer */ 586 memset (&ctx, 587 0, 588 sizeof (ctx)); 589 FAILIF (2 != 590 TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg, 591 &five, 592 0, 593 10, 594 NULL, 595 &transfer_cb, 596 &ctx)); 597 FAILIF (10 + 5 != ctx.value_sum); 598 599 /* a threshold nobody meets */ 600 memset (&ctx, 601 0, 602 sizeof (ctx)); 603 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 604 TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg, 605 &huge, 606 0, 607 10, 608 NULL, 609 &transfer_cb, 610 &ctx)); 611 FAILIF (0 != ctx.total); 612 613 /* filtering by account keeps the two transfers from PAYTO_A */ 614 npayto = TALER_payto_normalize (payto_a); 615 GNUNET_assert (NULL != npayto.normalized_payto); 616 TALER_normalized_payto_hash (npayto, 617 &h_payto); 618 GNUNET_free (npayto.normalized_payto); 619 memset (&ctx, 620 0, 621 sizeof (ctx)); 622 FAILIF (2 != 623 TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg, 624 &zero, 625 0, 626 10, 627 &h_payto, 628 &transfer_cb, 629 &ctx)); 630 FAILIF (10 + 2 != ctx.value_sum); 631 632 /* the limit caps the result set... */ 633 memset (&ctx, 634 0, 635 sizeof (ctx)); 636 FAILIF (1 != 637 TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg, 638 &zero, 639 0, 640 1, 641 NULL, 642 &transfer_cb, 643 &ctx)); 644 FAILIF (lowest != ctx.first_row); 645 646 /* ...and a negative limit walks the table the other way, starting from 647 the offset rather than from the beginning */ 648 memset (&ctx, 649 0, 650 sizeof (ctx)); 651 FAILIF (3 != 652 TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg, 653 &zero, 654 1000, 655 -10, 656 NULL, 657 &transfer_cb, 658 &ctx)); 659 FAILIF (lowest == ctx.first_row); 660 FAILIF (10 + 2 + 5 != ctx.value_sum); 661 return 0; 662 } 663 664 665 /** 666 * The checks to run, in order. 667 */ 668 static const struct TDB_Test tests[] = { 669 { "reserves-in-empty", 670 &check_empty }, 671 { "reserves-in-import", 672 &check_import }, 673 { "reserves-in-iterate", 674 &check_iterate }, 675 { "reserves-in-serial-bound", 676 &check_serial_bound }, 677 { "reserves-in-credit-transfers", 678 &check_credit_transfers }, 679 { NULL, NULL } 680 }; 681 682 683 int 684 main (int argc, 685 char *const *argv) 686 { 687 return TDB_main (argc, 688 argv, 689 "test-reserves-in", 690 "Tests for the exchangedb `reserves_in' table", 691 tests); 692 } 693 694 695 /* end of test_reserves_in.c */