test_reserves_close.c (17303B)
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_close.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `reserves_close` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_insert_reserve_closed(), 23 * #TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id() and 24 * #TALER_EXCHANGEDB_iterate_reserve_close_info(). 25 * 26 * The table references `reserves` and `wire_targets`, both of which the 27 * checks create with TDB_reserve() and TDB_account(). The KYC-facing 28 * iterator groups by the *normalized* account, so the checks close two 29 * reserves to the same account and one to another. 30 */ 31 #include "test_common.h" 32 #include "exchange-database/insert_reserve_closed.h" 33 #include "exchange-database/iterate_reserve_closed_above_serial_id.h" 34 #include "exchange-database/iterate_reserve_close_info.h" 35 36 37 /** 38 * Build a timestamp from a number of seconds since the epoch. 39 * 40 * @param secs seconds since the epoch 41 * @return the timestamp 42 */ 43 static struct GNUNET_TIME_Timestamp 44 ts (uint64_t secs) 45 { 46 struct GNUNET_TIME_Absolute abs = { 47 .abs_value_us = secs * 1000LLU * 1000LLU 48 }; 49 50 return GNUNET_TIME_absolute_to_timestamp (abs); 51 } 52 53 54 /** 55 * Closure for #closed_cb(). 56 */ 57 struct ClosedContext 58 { 59 /** 60 * How many rows did the callback see? 61 */ 62 unsigned int total; 63 64 /** 65 * Stop after this many rows; 0 for no limit. 66 */ 67 unsigned int stop_after; 68 69 /** 70 * Reserve we are looking for, NULL to match nothing. 71 */ 72 const struct TALER_ReservePublicKeyP *reserve_pub; 73 74 /** 75 * How many times did we see @e reserve_pub? 76 */ 77 unsigned int matched; 78 79 /** 80 * Amount reported for @e reserve_pub. 81 */ 82 struct TALER_Amount amount; 83 84 /** 85 * Closing fee reported for @e reserve_pub. 86 */ 87 struct TALER_Amount closing_fee; 88 89 /** 90 * Wire transfer identifier reported for @e reserve_pub. 91 */ 92 struct TALER_WireTransferIdentifierRawP wtid; 93 94 /** 95 * Close request row reported for @e reserve_pub. 96 */ 97 uint64_t close_request_row; 98 99 /** 100 * Receiver account reported for @e reserve_pub, owned by this struct. 101 */ 102 char *receiver; 103 }; 104 105 106 /** 107 * Callback for #TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id(). 108 * 109 * @param cls a `struct ClosedContext *` 110 * @param rowid row of the close operation 111 * @param execution_date when the transfer was made 112 * @param amount_with_fee how much was debited 113 * @param closing_fee how much was charged for closing 114 * @param reserve_pub the reserve that was closed 115 * @param receiver_account where the money went 116 * @param wtid identifier of the wire transfer 117 * @param close_request_row close request that triggered this, 0 if none 118 * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop 119 */ 120 static enum GNUNET_GenericReturnValue 121 closed_cb (void *cls, 122 uint64_t rowid, 123 struct GNUNET_TIME_Timestamp execution_date, 124 const struct TALER_Amount *amount_with_fee, 125 const struct TALER_Amount *closing_fee, 126 const struct TALER_ReservePublicKeyP *reserve_pub, 127 const struct TALER_FullPayto receiver_account, 128 const struct TALER_WireTransferIdentifierRawP *wtid, 129 uint64_t close_request_row) 130 { 131 struct ClosedContext *ctx = cls; 132 133 (void) rowid; 134 (void) execution_date; 135 ctx->total++; 136 if ( (NULL != ctx->reserve_pub) && 137 (0 == GNUNET_memcmp (reserve_pub, 138 ctx->reserve_pub)) ) 139 { 140 ctx->matched++; 141 ctx->amount = *amount_with_fee; 142 ctx->closing_fee = *closing_fee; 143 ctx->wtid = *wtid; 144 ctx->close_request_row = close_request_row; 145 GNUNET_free (ctx->receiver); 146 ctx->receiver = GNUNET_strdup (receiver_account.full_payto); 147 } 148 if ( (0 != ctx->stop_after) && 149 (ctx->total >= ctx->stop_after) ) 150 return GNUNET_SYSERR; 151 return GNUNET_OK; 152 } 153 154 155 /** 156 * Closure for #amount_cb(). 157 */ 158 struct AmountContext 159 { 160 /** 161 * How many amounts did the callback see? 162 */ 163 unsigned int total; 164 165 /** 166 * Sum of the whole-unit parts of the amounts seen. 167 */ 168 uint64_t value_sum; 169 170 /** 171 * Date of the first amount seen. 172 */ 173 struct GNUNET_TIME_Absolute first_date; 174 175 /** 176 * Return this from the callback. 177 */ 178 enum GNUNET_GenericReturnValue ret; 179 }; 180 181 182 /** 183 * Callback for #TALER_EXCHANGEDB_iterate_reserve_close_info(). 184 * 185 * @param cls a `struct AmountContext *` 186 * @param amount the closed amount 187 * @param date when the reserve was closed 188 * @return what @e ret of the closure says 189 */ 190 static enum GNUNET_GenericReturnValue 191 amount_cb (void *cls, 192 const struct TALER_Amount *amount, 193 struct GNUNET_TIME_Absolute date) 194 { 195 struct AmountContext *ctx = cls; 196 197 if (0 == ctx->total++) 198 ctx->first_date = date; 199 ctx->value_sum += amount->value; 200 return ctx->ret; 201 } 202 203 204 /** 205 * Close a reserve. 206 * 207 * @param pg the database context 208 * @param reserve_pub reserve to close 209 * @param account where the money goes 210 * @param seed seed for the wire transfer identifier 211 * @param when when the transfer was made, in seconds since the epoch 212 * @param amount amount debited from the reserve, e.g. "10" 213 * @param fee closing fee, e.g. "0.5" 214 * @param close_request_row close request that triggered this, 0 if none 215 * @param[out] wtid set to the wire transfer identifier used 216 * @return transaction status 217 */ 218 static enum GNUNET_DB_QueryStatus 219 close_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg, 220 const struct TALER_ReservePublicKeyP *reserve_pub, 221 const struct TDB_Account *account, 222 uint32_t seed, 223 uint64_t when, 224 const char *amount, 225 const char *fee, 226 uint64_t close_request_row, 227 struct TALER_WireTransferIdentifierRawP *wtid) 228 { 229 struct TALER_Amount a = TDB_amount (amount); 230 struct TALER_Amount f = TDB_amount (fee); 231 232 TDB_fill (wtid, 233 sizeof (*wtid), 234 seed); 235 return TALER_EXCHANGEDB_insert_reserve_closed (pg, 236 reserve_pub, 237 ts (when), 238 account->payto, 239 wtid, 240 &a, 241 &f, 242 close_request_row); 243 } 244 245 246 /** 247 * Nothing is reported while the table is empty. 248 * 249 * @param pg the database context 250 * @return 0 on success 251 */ 252 static int 253 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 254 { 255 struct ClosedContext ctx = { 0 }; 256 struct AmountContext actx = { 0 }; 257 struct TALER_NormalizedPaytoHashP h_payto; 258 259 TDB_FILL (h_payto, 260 1); 261 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 262 TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id ( 263 pg, 264 0, 265 &closed_cb, 266 &ctx)); 267 actx.ret = GNUNET_OK; 268 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 269 TALER_EXCHANGEDB_iterate_reserve_close_info ( 270 pg, 271 &h_payto, 272 GNUNET_TIME_UNIT_ZERO_ABS, 273 &amount_cb, 274 &actx)); 275 FAILIF (0 != ctx.total); 276 FAILIF (0 != actx.total); 277 return 0; 278 } 279 280 281 /** 282 * A closed reserve is reported with everything it was closed with. 283 * 284 * @param pg the database context 285 * @return 0 on success 286 */ 287 static int 288 check_insert_and_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg) 289 { 290 struct TDB_Account account; 291 struct TALER_ReservePublicKeyP reserve_pub; 292 struct TALER_WireTransferIdentifierRawP wtid; 293 struct TALER_Amount expect = TDB_amount ("10"); 294 struct TALER_Amount expect_fee = TDB_amount ("0.5"); 295 struct ClosedContext ctx; 296 297 TDB_account (pg, 298 10, 299 &account); 300 /* insert_reserve_closed() debits the reserve, so it has to hold exactly 301 what is being closed */ 302 TDB_reserve (pg, 303 10, 304 "10", 305 &reserve_pub); 306 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 307 close_reserve (pg, 308 &reserve_pub, 309 &account, 310 10, 311 1600000000, 312 "10", 313 "0.5", 314 0, 315 &wtid), 316 TDB_account_free (&account)); 317 memset (&ctx, 318 0, 319 sizeof (ctx)); 320 ctx.reserve_pub = &reserve_pub; 321 FAILIF_C (0 >= 322 TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id ( 323 pg, 324 0, 325 &closed_cb, 326 &ctx), 327 TDB_account_free (&account)); 328 FAILIF_C (1 != ctx.matched, 329 GNUNET_free (ctx.receiver); TDB_account_free (&account)); 330 FAILIF_C (0 != TALER_amount_cmp (&ctx.amount, 331 &expect), 332 GNUNET_free (ctx.receiver); TDB_account_free (&account)); 333 FAILIF_C (0 != TALER_amount_cmp (&ctx.closing_fee, 334 &expect_fee), 335 GNUNET_free (ctx.receiver); TDB_account_free (&account)); 336 FAILIF_C (0 != GNUNET_memcmp (&ctx.wtid, 337 &wtid), 338 GNUNET_free (ctx.receiver); TDB_account_free (&account)); 339 FAILIF_C (0 != ctx.close_request_row, 340 GNUNET_free (ctx.receiver); TDB_account_free (&account)); 341 FAILIF_C (0 != strcmp (ctx.receiver, 342 account.payto.full_payto), 343 GNUNET_free (ctx.receiver); TDB_account_free (&account)); 344 GNUNET_free (ctx.receiver); 345 TDB_account_free (&account); 346 return 0; 347 } 348 349 350 /** 351 * The serial bound, the abort return of the callback and a second account 352 * all behave as documented. 353 * 354 * @param pg the database context 355 * @return 0 on success 356 */ 357 static int 358 check_iterate_filters (struct TALER_EXCHANGEDB_PostgresContext *pg) 359 { 360 struct TDB_Account account; 361 struct TDB_Account other; 362 struct TALER_ReservePublicKeyP r2; 363 struct TALER_ReservePublicKeyP r3; 364 struct TALER_WireTransferIdentifierRawP wtid; 365 struct ClosedContext ctx; 366 367 TDB_account (pg, 368 10, 369 &account); 370 TDB_account (pg, 371 11, 372 &other); 373 TDB_reserve (pg, 374 11, 375 "4", 376 &r2); 377 TDB_reserve (pg, 378 12, 379 "9", 380 &r3); 381 /* a second close to the same account, later */ 382 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 383 close_reserve (pg, 384 &r2, 385 &account, 386 11, 387 1600003600, 388 "4", 389 "0.5", 390 0, 391 &wtid), 392 TDB_account_free (&account); TDB_account_free (&other)); 393 /* and one to a different account */ 394 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 395 close_reserve (pg, 396 &r3, 397 &other, 398 12, 399 1600007200, 400 "9", 401 "0.5", 402 0, 403 &wtid), 404 TDB_account_free (&account); TDB_account_free (&other)); 405 FAILIF_C (3 != TDB_count (pg, 406 "FROM reserves_close"), 407 TDB_account_free (&account); TDB_account_free (&other)); 408 409 memset (&ctx, 410 0, 411 sizeof (ctx)); 412 FAILIF_C (3 != 413 TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id ( 414 pg, 415 0, 416 &closed_cb, 417 &ctx), 418 TDB_account_free (&account); TDB_account_free (&other)); 419 memset (&ctx, 420 0, 421 sizeof (ctx)); 422 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 423 TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id ( 424 pg, 425 1000, 426 &closed_cb, 427 &ctx), 428 TDB_account_free (&account); TDB_account_free (&other)); 429 FAILIF_C (0 != ctx.total, 430 TDB_account_free (&account); TDB_account_free (&other)); 431 432 /* an aborting callback stops the iteration */ 433 memset (&ctx, 434 0, 435 sizeof (ctx)); 436 ctx.stop_after = 2; 437 FAILIF_C (3 != 438 TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id ( 439 pg, 440 0, 441 &closed_cb, 442 &ctx), 443 TDB_account_free (&account); TDB_account_free (&other)); 444 FAILIF_C (2 != ctx.total, 445 TDB_account_free (&account); TDB_account_free (&other)); 446 TDB_account_free (&account); 447 TDB_account_free (&other); 448 return 0; 449 } 450 451 452 /** 453 * The KYC view sums the closures per account, newest first, and honours 454 * the time limit and the callback's abort. 455 * 456 * @param pg the database context 457 * @return 0 on success 458 */ 459 static int 460 check_close_info (struct TALER_EXCHANGEDB_PostgresContext *pg) 461 { 462 struct TDB_Account account; 463 struct TDB_Account other; 464 struct AmountContext ctx; 465 466 TDB_account (pg, 467 10, 468 &account); 469 TDB_account (pg, 470 11, 471 &other); 472 473 /* both closures to the first account */ 474 memset (&ctx, 475 0, 476 sizeof (ctx)); 477 ctx.ret = GNUNET_OK; 478 FAILIF_C (2 != 479 TALER_EXCHANGEDB_iterate_reserve_close_info ( 480 pg, 481 &account.h_normalized, 482 GNUNET_TIME_UNIT_ZERO_ABS, 483 &amount_cb, 484 &ctx), 485 TDB_account_free (&account); TDB_account_free (&other)); 486 FAILIF_C (10 + 4 != ctx.value_sum, 487 TDB_account_free (&account); TDB_account_free (&other)); 488 /* newest first */ 489 FAILIF_C (ctx.first_date.abs_value_us != 490 ts (1600003600).abs_time.abs_value_us, 491 TDB_account_free (&account); TDB_account_free (&other)); 492 493 /* the other account has just the one */ 494 memset (&ctx, 495 0, 496 sizeof (ctx)); 497 ctx.ret = GNUNET_OK; 498 FAILIF_C (1 != 499 TALER_EXCHANGEDB_iterate_reserve_close_info ( 500 pg, 501 &other.h_normalized, 502 GNUNET_TIME_UNIT_ZERO_ABS, 503 &amount_cb, 504 &ctx), 505 TDB_account_free (&account); TDB_account_free (&other)); 506 FAILIF_C (9 != ctx.value_sum, 507 TDB_account_free (&account); TDB_account_free (&other)); 508 509 /* the time limit drops the older closure */ 510 memset (&ctx, 511 0, 512 sizeof (ctx)); 513 ctx.ret = GNUNET_OK; 514 FAILIF_C (1 != 515 TALER_EXCHANGEDB_iterate_reserve_close_info ( 516 pg, 517 &account.h_normalized, 518 ts (1600001800).abs_time, 519 &amount_cb, 520 &ctx), 521 TDB_account_free (&account); TDB_account_free (&other)); 522 FAILIF_C (4 != ctx.value_sum, 523 TDB_account_free (&account); TDB_account_free (&other)); 524 525 /* a limit past every closure leaves nothing */ 526 memset (&ctx, 527 0, 528 sizeof (ctx)); 529 ctx.ret = GNUNET_OK; 530 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 531 TALER_EXCHANGEDB_iterate_reserve_close_info ( 532 pg, 533 &account.h_normalized, 534 ts (1700000000).abs_time, 535 &amount_cb, 536 &ctx), 537 TDB_account_free (&account); TDB_account_free (&other)); 538 FAILIF_C (0 != ctx.total, 539 TDB_account_free (&account); TDB_account_free (&other)); 540 541 /* a callback that aborts with GNUNET_NO is not an error */ 542 memset (&ctx, 543 0, 544 sizeof (ctx)); 545 ctx.ret = GNUNET_NO; 546 FAILIF_C (0 > 547 TALER_EXCHANGEDB_iterate_reserve_close_info ( 548 pg, 549 &account.h_normalized, 550 GNUNET_TIME_UNIT_ZERO_ABS, 551 &amount_cb, 552 &ctx), 553 TDB_account_free (&account); TDB_account_free (&other)); 554 FAILIF_C (1 != ctx.total, 555 TDB_account_free (&account); TDB_account_free (&other)); 556 TDB_account_free (&account); 557 TDB_account_free (&other); 558 return 0; 559 } 560 561 562 /** 563 * The checks to run, in order. 564 */ 565 static const struct TDB_Test tests[] = { 566 { "reserves-close-empty", 567 &check_empty }, 568 { "reserves-close-insert-and-iterate", 569 &check_insert_and_iterate }, 570 { "reserves-close-iterate-filters", 571 &check_iterate_filters }, 572 { "reserves-close-close-info", 573 &check_close_info }, 574 { NULL, NULL } 575 }; 576 577 578 int 579 main (int argc, 580 char *const *argv) 581 { 582 return TDB_main (argc, 583 argv, 584 "test-reserves-close", 585 "Tests for the exchangedb `reserves_close' table", 586 tests); 587 } 588 589 590 /* end of test_reserves_close.c */