test_reserves.c (20782B)
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.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `reserves` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_get_reserve(), 23 * #TALER_EXCHANGEDB_update_reserve(), 24 * #TALER_EXCHANGEDB_get_reserve_balance(), 25 * #TALER_EXCHANGEDB_get_reserve_close_info(), 26 * #TALER_EXCHANGEDB_iterate_expired_reserves() and 27 * #TALER_EXCHANGEDB_gc(). 28 * 29 * A reserve may or may not have a `reserves_in` row: one funded by a bank 30 * transfer has one, one created by a peer-to-peer merge does not. Several 31 * of these functions LEFT JOIN through it to get the originating account, 32 * so both shapes are exercised. The garbage collection check runs last, 33 * as it removes rows the earlier checks created. 34 */ 35 #include "test_common.h" 36 #include "exchange-database/gc.h" 37 #include "exchange-database/get_reserve.h" 38 #include "exchange-database/get_reserve_balance.h" 39 #include "exchange-database/get_reserve_close_info.h" 40 #include "exchange-database/iterate_expired_reserves.h" 41 #include "exchange-database/update_reserve.h" 42 43 44 /** 45 * One second in microseconds. Timestamps read back through 46 * GNUNET_PQ_result_spec_timestamp() have to be whole seconds, so the 47 * checks never write a raw microsecond value that is not a multiple of 48 * this. 49 */ 50 #define SECOND 1000000LLU 51 52 53 /** 54 * Create a reserve with an explicit expiration and garbage collection 55 * date, which TDB_reserve() does not offer. 56 * 57 * @param pg the database context 58 * @param seed seed for the reserve's public key 59 * @param balance balance of the reserve, e.g. "10" 60 * @param expiration_us expiration date, in microseconds since the epoch 61 * @param gc_us garbage collection date, in microseconds since the epoch 62 * @param[out] reserve_pub set to the public key of the reserve 63 */ 64 static void 65 make_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg, 66 uint32_t seed, 67 const char *balance, 68 uint64_t expiration_us, 69 uint64_t gc_us, 70 struct TALER_ReservePublicKeyP *reserve_pub) 71 { 72 struct TALER_Amount b = TDB_amount (balance); 73 char *hex; 74 75 TDB_fill (reserve_pub, 76 sizeof (*reserve_pub), 77 seed); 78 hex = TDB_hex (reserve_pub, 79 sizeof (*reserve_pub)); 80 GNUNET_assert (GNUNET_OK == 81 TDB_exec (pg, 82 "INSERT INTO reserves" 83 " (reserve_pub,current_balance" 84 " ,expiration_date,gc_date)" 85 " VALUES (decode('%s','hex')" 86 " ,ROW(%llu,%u)::taler_amount" 87 " ,%llu,%llu)" 88 " ON CONFLICT DO NOTHING;", 89 hex, 90 (unsigned long long) b.value, 91 (unsigned int) b.fraction, 92 (unsigned long long) expiration_us, 93 (unsigned long long) gc_us)); 94 GNUNET_free (hex); 95 } 96 97 98 /** 99 * Closure for #expired_cb(). 100 */ 101 struct ExpiredContext 102 { 103 /** 104 * How many reserves did the callback see? 105 */ 106 unsigned int total; 107 108 /** 109 * Public key of the last reserve seen. 110 */ 111 struct TALER_ReservePublicKeyP reserve_pub; 112 113 /** 114 * Balance of the last reserve seen. 115 */ 116 struct TALER_Amount left; 117 118 /** 119 * Set if the last reserve was reported with an originating account. 120 */ 121 bool have_account; 122 123 /** 124 * Close request row of the last reserve seen. 125 */ 126 uint64_t close_request_row; 127 }; 128 129 130 /** 131 * Callback for #TALER_EXCHANGEDB_iterate_expired_reserves(). 132 * 133 * @param cls a `struct ExpiredContext *` 134 * @param reserve_pub the expired reserve 135 * @param left balance left in the reserve 136 * @param account_details originating account 137 * @param expiration_date when the reserve expired 138 * @param close_request_row row of the close request, 0 if none 139 * @return #GNUNET_OK 140 */ 141 static enum GNUNET_GenericReturnValue 142 expired_cb (void *cls, 143 const struct TALER_ReservePublicKeyP *reserve_pub, 144 const struct TALER_Amount *left, 145 const struct TALER_FullPayto account_details, 146 struct GNUNET_TIME_Timestamp expiration_date, 147 uint64_t close_request_row) 148 { 149 struct ExpiredContext *ctx = cls; 150 151 (void) expiration_date; 152 ctx->total++; 153 ctx->reserve_pub = *reserve_pub; 154 ctx->left = *left; 155 ctx->have_account = (NULL != account_details.full_payto); 156 ctx->close_request_row = close_request_row; 157 return GNUNET_OK; 158 } 159 160 161 /** 162 * Nothing is known about a reserve that was never created. 163 * 164 * @param pg the database context 165 * @return 0 on success 166 */ 167 static int 168 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 169 { 170 struct TALER_EXCHANGEDB_Reserve reserve; 171 struct TALER_Amount balance; 172 struct TALER_FullPayto payto = { NULL }; 173 struct ExpiredContext ctx = { 0 }; 174 175 memset (&reserve, 176 0, 177 sizeof (reserve)); 178 TDB_FILL (reserve.pub, 179 1); 180 reserve.balance = TDB_amount ("1"); 181 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 182 TALER_EXCHANGEDB_get_reserve (pg, 183 &reserve)); 184 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 185 TALER_EXCHANGEDB_update_reserve (pg, 186 &reserve)); 187 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 188 TALER_EXCHANGEDB_get_reserve_balance (pg, 189 &reserve.pub, 190 &balance, 191 &payto)); 192 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 193 TALER_EXCHANGEDB_get_reserve_close_info (pg, 194 &reserve.pub, 195 &balance, 196 &payto)); 197 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 198 TALER_EXCHANGEDB_iterate_expired_reserves (pg, 199 GNUNET_TIME_timestamp_get ( 200 ), 201 &expired_cb, 202 &ctx)); 203 FAILIF (0 != ctx.total); 204 FAILIF (0 != TDB_count (pg, 205 "FROM reserves")); 206 return 0; 207 } 208 209 210 /** 211 * A reserve without a `reserves_in` row is found, with no origin account. 212 * 213 * @param pg the database context 214 * @return 0 on success 215 */ 216 static int 217 check_without_origin (struct TALER_EXCHANGEDB_PostgresContext *pg) 218 { 219 struct TALER_EXCHANGEDB_Reserve reserve; 220 struct TALER_Amount balance; 221 struct TALER_Amount expect = TDB_amount ("10"); 222 struct TALER_FullPayto payto = { 223 .full_payto = (char *) "sentinel" 224 }; 225 226 memset (&reserve, 227 0, 228 sizeof (reserve)); 229 TDB_reserve (pg, 230 2, 231 "10", 232 &reserve.pub); 233 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 234 TALER_EXCHANGEDB_get_reserve (pg, 235 &reserve)); 236 FAILIF (0 != TALER_amount_cmp (&reserve.balance, 237 &expect)); 238 FAILIF (GNUNET_TIME_absolute_is_zero (reserve.expiry.abs_time)); 239 FAILIF (GNUNET_TIME_absolute_is_zero (reserve.gc.abs_time)); 240 241 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 242 TALER_EXCHANGEDB_get_reserve_balance (pg, 243 &reserve.pub, 244 &balance, 245 &payto)); 246 FAILIF (0 != TALER_amount_cmp (&balance, 247 &expect)); 248 FAILIF (NULL != payto.full_payto); 249 250 payto.full_payto = (char *) "sentinel"; 251 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 252 TALER_EXCHANGEDB_get_reserve_close_info (pg, 253 &reserve.pub, 254 &balance, 255 &payto)); 256 FAILIF (0 != TALER_amount_cmp (&balance, 257 &expect)); 258 FAILIF (NULL != payto.full_payto); 259 return 0; 260 } 261 262 263 /** 264 * A reserve funded by a bank transfer reports its origin account. 265 * 266 * @param pg the database context 267 * @return 0 on success 268 */ 269 static int 270 check_with_origin (struct TALER_EXCHANGEDB_PostgresContext *pg) 271 { 272 struct TDB_Account account; 273 struct TALER_ReservePublicKeyP reserve_pub; 274 struct TALER_Amount balance; 275 struct TALER_Amount expect = TDB_amount ("7"); 276 struct TALER_FullPayto payto = { NULL }; 277 278 TDB_account (pg, 279 3, 280 &account); 281 TDB_reserve_in (pg, 282 &account, 283 3, 284 "7", 285 &reserve_pub); 286 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 287 TALER_EXCHANGEDB_get_reserve_balance (pg, 288 &reserve_pub, 289 &balance, 290 &payto), 291 TDB_account_free (&account)); 292 FAILIF_C (0 != TALER_amount_cmp (&balance, 293 &expect), 294 TDB_account_free (&account)); 295 FAILIF_C (NULL == payto.full_payto, 296 TDB_account_free (&account)); 297 FAILIF_C (0 != strcmp (payto.full_payto, 298 account.payto.full_payto), 299 GNUNET_free (payto.full_payto); TDB_account_free (&account)); 300 GNUNET_free (payto.full_payto); 301 302 payto.full_payto = NULL; 303 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 304 TALER_EXCHANGEDB_get_reserve_close_info (pg, 305 &reserve_pub, 306 &balance, 307 &payto), 308 TDB_account_free (&account)); 309 FAILIF_C (NULL == payto.full_payto, 310 TDB_account_free (&account)); 311 FAILIF_C (0 != strcmp (payto.full_payto, 312 account.payto.full_payto), 313 GNUNET_free (payto.full_payto); TDB_account_free (&account)); 314 GNUNET_free (payto.full_payto); 315 TDB_account_free (&account); 316 return 0; 317 } 318 319 320 /** 321 * Updating a reserve writes back the balance and both dates. 322 * 323 * @param pg the database context 324 * @return 0 on success 325 */ 326 static int 327 check_update (struct TALER_EXCHANGEDB_PostgresContext *pg) 328 { 329 struct TALER_EXCHANGEDB_Reserve reserve; 330 struct TALER_EXCHANGEDB_Reserve got; 331 struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); 332 333 memset (&reserve, 334 0, 335 sizeof (reserve)); 336 TDB_FILL (reserve.pub, 337 2); 338 reserve.balance = TDB_amount ("3.5"); 339 reserve.expiry = GNUNET_TIME_absolute_to_timestamp (now); 340 reserve.gc = GNUNET_TIME_absolute_to_timestamp ( 341 GNUNET_TIME_absolute_add (now, 342 GNUNET_TIME_UNIT_HOURS)); 343 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 344 TALER_EXCHANGEDB_update_reserve (pg, 345 &reserve)); 346 memset (&got, 347 0, 348 sizeof (got)); 349 got.pub = reserve.pub; 350 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 351 TALER_EXCHANGEDB_get_reserve (pg, 352 &got)); 353 FAILIF (0 != TALER_amount_cmp (&got.balance, 354 &reserve.balance)); 355 FAILIF (GNUNET_TIME_timestamp_cmp (got.expiry, 356 !=, 357 reserve.expiry)); 358 FAILIF (GNUNET_TIME_timestamp_cmp (got.gc, 359 !=, 360 reserve.gc)); 361 return 0; 362 } 363 364 365 /** 366 * Only an expired reserve that still holds money and has an originating 367 * account is reported for closing. 368 * 369 * @param pg the database context 370 * @return 0 on success 371 */ 372 static int 373 check_expired (struct TALER_EXCHANGEDB_PostgresContext *pg) 374 { 375 struct TDB_Account account; 376 struct TALER_ReservePublicKeyP funded; 377 struct TALER_ReservePublicKeyP drained; 378 struct ExpiredContext ctx; 379 struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); 380 struct TALER_Amount expect = TDB_amount ("7"); 381 char *hex; 382 383 /* nothing has expired yet: the reserves so far all expire in 2096 */ 384 memset (&ctx, 385 0, 386 sizeof (ctx)); 387 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 388 TALER_EXCHANGEDB_iterate_expired_reserves (pg, 389 now, 390 &expired_cb, 391 &ctx)); 392 393 /* expire the funded reserve from check_with_origin() */ 394 TDB_account (pg, 395 3, 396 &account); 397 TDB_fill (&funded, 398 sizeof (funded), 399 3); 400 hex = TDB_hex (&funded, 401 sizeof (funded)); 402 FAILIF_C (GNUNET_OK != 403 TDB_exec (pg, 404 "UPDATE reserves" 405 " SET expiration_date=1000000" 406 " WHERE reserve_pub=decode('%s','hex');", 407 hex), 408 GNUNET_free (hex); TDB_account_free (&account)); 409 GNUNET_free (hex); 410 memset (&ctx, 411 0, 412 sizeof (ctx)); 413 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 414 TALER_EXCHANGEDB_iterate_expired_reserves (pg, 415 now, 416 &expired_cb, 417 &ctx), 418 TDB_account_free (&account)); 419 FAILIF_C (1 != ctx.total, 420 TDB_account_free (&account)); 421 FAILIF_C (0 != GNUNET_memcmp (&ctx.reserve_pub, 422 &funded), 423 TDB_account_free (&account)); 424 FAILIF_C (0 != TALER_amount_cmp (&ctx.left, 425 &expect), 426 TDB_account_free (&account)); 427 FAILIF_C (! ctx.have_account, 428 TDB_account_free (&account)); 429 430 /* an expired reserve with a zero balance is not worth closing */ 431 make_reserve (pg, 432 4, 433 "0", 434 SECOND, 435 SECOND, 436 &drained); 437 { 438 char *rhex = TDB_hex (&drained, 439 sizeof (drained)); 440 char *ahex = TDB_hex (&account.h_full, 441 sizeof (account.h_full)); 442 443 FAILIF_C (GNUNET_OK != 444 TDB_exec (pg, 445 "INSERT INTO reserves_in" 446 " (reserve_pub,wire_reference,credit" 447 " ,wire_source_h_payto" 448 " ,exchange_account_section,execution_date)" 449 " VALUES (decode('%s','hex'),44" 450 " ,ROW(0,0)::taler_amount" 451 " ,decode('%s','hex'),'exchange-account-1'" 452 " ,1000000);", 453 rhex, 454 ahex), 455 GNUNET_free (rhex); GNUNET_free (ahex); 456 TDB_account_free (&account)); 457 GNUNET_free (rhex); 458 GNUNET_free (ahex); 459 } 460 memset (&ctx, 461 0, 462 sizeof (ctx)); 463 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 464 TALER_EXCHANGEDB_iterate_expired_reserves (pg, 465 now, 466 &expired_cb, 467 &ctx), 468 TDB_account_free (&account)); 469 /* the query returns at most one reserve, and it is still the funded one */ 470 FAILIF_C (1 != ctx.total, 471 TDB_account_free (&account)); 472 FAILIF_C (0 != GNUNET_memcmp (&ctx.reserve_pub, 473 &funded), 474 TDB_account_free (&account)); 475 TDB_account_free (&account); 476 477 /* an expired reserve with money but no originating account is skipped: 478 it was created by a merge, so there is nowhere to wire the money back 479 to. Drain the funded one first so it cannot mask the result. */ 480 hex = TDB_hex (&funded, 481 sizeof (funded)); 482 FAILIF_C (GNUNET_OK != 483 TDB_exec (pg, 484 "UPDATE reserves" 485 " SET current_balance=ROW(0,0)::taler_amount" 486 " WHERE reserve_pub=decode('%s','hex');", 487 hex), 488 GNUNET_free (hex)); 489 GNUNET_free (hex); 490 { 491 struct TALER_ReservePublicKeyP merged; 492 493 make_reserve (pg, 494 5, 495 "4", 496 SECOND, 497 SECOND, 498 &merged); 499 } 500 memset (&ctx, 501 0, 502 sizeof (ctx)); 503 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 504 TALER_EXCHANGEDB_iterate_expired_reserves (pg, 505 now, 506 &expired_cb, 507 &ctx)); 508 FAILIF (0 != ctx.total); 509 return 0; 510 } 511 512 513 /** 514 * Garbage collection removes drained, expired reserves and leaves the 515 * others alone. Runs last. 516 * 517 * @param pg the database context 518 * @return 0 on success 519 */ 520 static int 521 check_gc (struct TALER_EXCHANGEDB_PostgresContext *pg) 522 { 523 struct TALER_ReservePublicKeyP old_empty; 524 struct TALER_ReservePublicKeyP old_funded; 525 struct TALER_ReservePublicKeyP young_empty; 526 char *hex; 527 uint64_t before; 528 529 /* gc_date in the past and no money left: collectable */ 530 make_reserve (pg, 531 10, 532 "0", 533 SECOND, 534 SECOND, 535 &old_empty); 536 /* gc_date in the past but still holding money: kept */ 537 make_reserve (pg, 538 11, 539 "2", 540 SECOND, 541 SECOND, 542 &old_funded); 543 /* no money left but gc_date far in the future: kept */ 544 make_reserve (pg, 545 12, 546 "0", 547 4000000000000000LLU, 548 5000000000000000LLU, 549 &young_empty); 550 before = TDB_count (pg, 551 "FROM reserves"); 552 FAILIF (GNUNET_OK != 553 TALER_EXCHANGEDB_gc (pg)); 554 555 hex = TDB_hex (&old_empty, 556 sizeof (old_empty)); 557 FAILIF_C (0 != TDB_count (pg, 558 "FROM reserves" 559 " WHERE reserve_pub=decode('%s','hex')", 560 hex), 561 GNUNET_free (hex)); 562 GNUNET_free (hex); 563 hex = TDB_hex (&old_funded, 564 sizeof (old_funded)); 565 FAILIF_C (1 != TDB_count (pg, 566 "FROM reserves" 567 " WHERE reserve_pub=decode('%s','hex')", 568 hex), 569 GNUNET_free (hex)); 570 GNUNET_free (hex); 571 hex = TDB_hex (&young_empty, 572 sizeof (young_empty)); 573 FAILIF_C (1 != TDB_count (pg, 574 "FROM reserves" 575 " WHERE reserve_pub=decode('%s','hex')", 576 hex), 577 GNUNET_free (hex)); 578 GNUNET_free (hex); 579 FAILIF (before <= TDB_count (pg, 580 "FROM reserves")); 581 /* running it again on the same database changes nothing further */ 582 before = TDB_count (pg, 583 "FROM reserves"); 584 FAILIF (GNUNET_OK != 585 TALER_EXCHANGEDB_gc (pg)); 586 FAILIF (before != TDB_count (pg, 587 "FROM reserves")); 588 return 0; 589 } 590 591 592 /** 593 * The checks to run, in order. 594 */ 595 static const struct TDB_Test tests[] = { 596 { "reserves-empty", 597 &check_empty }, 598 { "reserves-without-origin", 599 &check_without_origin }, 600 { "reserves-with-origin", 601 &check_with_origin }, 602 { "reserves-update", 603 &check_update }, 604 { "reserves-expired", 605 &check_expired }, 606 { "reserves-gc", 607 &check_gc }, 608 { NULL, NULL } 609 }; 610 611 612 int 613 main (int argc, 614 char *const *argv) 615 { 616 return TDB_main (argc, 617 argv, 618 "test-reserves", 619 "Tests for the exchangedb `reserves' table", 620 tests); 621 } 622 623 624 /* end of test_reserves.c */