test_regressions.c (33456B)
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_regressions.c 18 * @brief regression tests for individual exchangedb operations 19 * 20 * Each check in the #tests table pins down one previously broken behaviour. 21 * The tests share one scratch database, which test_regressions.sh creates and 22 * removes; every check must therefore use fresh keys rather than assume an 23 * empty database. 24 */ 25 #include "exchangedb_lib.h" 26 #include "taler/taler_json_lib.h" 27 #include "helper.h" 28 #include "exchange-database/create_tables.h" 29 #include "exchange-database/do_aggregate.h" 30 #include "exchange-database/compute_shard.h" 31 #include "exchange-database/start.h" 32 #include "exchange-database/commit.h" 33 #include "exchange-database/rollback.h" 34 #include "exchange-database/do_reserve_open.h" 35 #include "exchange-database/get_purse_deposit.h" 36 #include "exchange-database/get_reserve_close_info.h" 37 #include "exchange-database/begin_shard.h" 38 #include "exchange-database/abort_shard.h" 39 #include "exchange-database/update_shard_progress.h" 40 #include "exchange-database/do_import_credits.h" 41 42 43 /** 44 * Currency we use, must match test_regressions.sh. 45 */ 46 #define CURRENCY "EUR" 47 48 /** 49 * Report a failed expectation and return 1 from the calling check. 50 */ 51 #define FAILIF(cond) \ 52 do { \ 53 if (! (cond)) break; \ 54 GNUNET_break (0); \ 55 fprintf (stderr, \ 56 "FAILED: %s at %s:%u\n", \ 57 # cond, __FILE__, __LINE__); \ 58 return 1; \ 59 } while (0) 60 61 62 /** 63 * Our database context. 64 */ 65 static struct TALER_EXCHANGEDB_PostgresContext *pg; 66 67 /** 68 * Name of the single check to run, NULL to run all of them. 69 */ 70 static char *only; 71 72 /** 73 * Return value of the process. 74 */ 75 static int result; 76 77 78 /** 79 * Run @a sql on our database connection, outside of any transaction. 80 * 81 * @param sql statement(s) to run 82 * @return #GNUNET_OK on success 83 */ 84 static enum GNUNET_GenericReturnValue 85 exec_sql (const char *sql) 86 { 87 struct GNUNET_PQ_ExecuteStatement es[] = { 88 GNUNET_PQ_make_execute (sql), 89 GNUNET_PQ_EXECUTE_STATEMENT_END 90 }; 91 92 return GNUNET_PQ_exec_statements (pg->conn, 93 es); 94 } 95 96 97 /** 98 * Convert @a val to an amount in our currency. 99 * 100 * @param str amount without the currency prefix, e.g. "10.5" 101 * @param[out] amount set to the parsed amount 102 */ 103 static void 104 parse_amount (const char *str, 105 struct TALER_Amount *amount) 106 { 107 char *s; 108 109 GNUNET_asprintf (&s, 110 CURRENCY ":%s", 111 str); 112 GNUNET_assert (GNUNET_OK == 113 TALER_string_to_amount (s, 114 amount)); 115 GNUNET_free (s); 116 } 117 118 119 /** 120 * Build an INSERT that creates a reserve with a zero balance and no 121 * `reserves_in` row, the way exchange_do_purse_merge() does. 122 * 123 * @param reserve_pub public key of the reserve to create 124 * @return SQL statement, to be freed by the caller 125 */ 126 static char * 127 hex_insert_reserve (const struct TALER_ReservePublicKeyP *reserve_pub) 128 { 129 char hex[sizeof (*reserve_pub) * 2 + 1]; 130 const unsigned char *raw = (const unsigned char *) reserve_pub; 131 char *sql; 132 133 for (unsigned int i = 0; i<sizeof (*reserve_pub); i++) 134 GNUNET_snprintf (&hex[i * 2], 135 3, 136 "%02x", 137 raw[i]); 138 GNUNET_asprintf (&sql, 139 "INSERT INTO reserves" 140 " (reserve_pub,current_balance,expiration_date,gc_date)" 141 " VALUES" 142 " (decode('%s','hex')" 143 " ,ROW(0,0)::taler_amount" 144 " ,1770000000000000" 145 " ,1780000000000000);", 146 hex); 147 return sql; 148 } 149 150 151 /** 152 * Render @a data as a lowercase hex string for use in an SQL literal. 153 * 154 * @param data binary data 155 * @param size number of bytes in @a data 156 * @return hex string, to be freed by the caller 157 */ 158 static char * 159 to_hex (const void *data, 160 size_t size) 161 { 162 const unsigned char *raw = data; 163 char *hex; 164 165 hex = GNUNET_malloc (size * 2 + 1); 166 for (size_t i = 0; i<size; i++) 167 GNUNET_snprintf (&hex[i * 2], 168 3, 169 "%02x", 170 raw[i]); 171 return hex; 172 } 173 174 175 /** 176 * E-2: a merchant may refund more than (deposit - deposit fee). The 177 * aggregator used to charge the full deposit fee on top of such a refund, 178 * making the amount to be wired out negative, and then abort() on the 179 * GNUNET_assert() guarding the subtraction -- stopping every payout for the 180 * shard, and doing so again on every restart. 181 * 182 * Deposit EUR:1.00 of a denomination with a EUR:0.10 deposit fee, refund 183 * EUR:0.95 of it, and require the aggregation to come out at EUR:0. 184 */ 185 static int 186 check_aggregate_refund_below_deposit_fee (void) 187 { 188 struct TALER_MerchantPublicKeyP merchant_pub; 189 struct TALER_FullPaytoHashP h_payto; 190 struct TALER_NormalizedPaytoHashP h_norm; 191 struct TALER_CoinSpendPublicKeyP coin_pub; 192 struct TALER_WireTransferIdentifierRawP wtid; 193 struct TALER_Amount total; 194 struct TALER_Amount zero; 195 char *sql; 196 char *m_hex; 197 char *p_hex; 198 char *n_hex; 199 char *c_hex; 200 enum GNUNET_GenericReturnValue ok; 201 202 parse_amount ("0", 203 &zero); 204 memset (&merchant_pub, 205 0x41, 206 sizeof (merchant_pub)); 207 memset (&h_payto, 208 0x42, 209 sizeof (h_payto)); 210 memset (&h_norm, 211 0x43, 212 sizeof (h_norm)); 213 memset (&coin_pub, 214 0x44, 215 sizeof (coin_pub)); 216 memset (&wtid, 217 0x45, 218 sizeof (wtid)); 219 m_hex = to_hex (&merchant_pub, 220 sizeof (merchant_pub)); 221 p_hex = to_hex (&h_payto, 222 sizeof (h_payto)); 223 n_hex = to_hex (&h_norm, 224 sizeof (h_norm)); 225 c_hex = to_hex (&coin_pub, 226 sizeof (coin_pub)); 227 GNUNET_asprintf ( 228 &sql, 229 "INSERT INTO kyc_targets (h_normalized_payto)" 230 " VALUES (decode('%s','hex')) ON CONFLICT DO NOTHING;" 231 "INSERT INTO wire_targets" 232 " (wire_target_h_payto,payto_uri,h_normalized_payto)" 233 " VALUES (decode('%s','hex'),'payto://x-taler-bank/h/e2'," 234 " decode('%s','hex')) ON CONFLICT DO NOTHING;" 235 "INSERT INTO denominations" 236 " (denom_pub_hash,denom_type,age_mask,denom_pub,master_sig" 237 " ,valid_from,expire_withdraw,expire_deposit,expire_legal" 238 " ,coin,fee_withdraw,fee_deposit,fee_refresh,fee_refund)" 239 " VALUES (decode(repeat('e2',64),'hex'),1,0,decode('00','hex')" 240 " ,decode(repeat('00',64),'hex'),0,0,0,0" 241 " ,ROW(1,0)::taler_amount,ROW(0,0)::taler_amount" 242 " ,ROW(0,10000000)::taler_amount,ROW(0,0)::taler_amount" 243 " ,ROW(0,0)::taler_amount);" 244 "INSERT INTO known_coins" 245 " (denominations_serial,coin_pub,denom_sig,remaining)" 246 " VALUES ((SELECT denominations_serial FROM denominations" 247 " WHERE denom_pub_hash=decode(repeat('e2',64),'hex'))" 248 " ,decode('%s','hex'),decode('00','hex'),ROW(0,0)::taler_amount);" 249 "INSERT INTO batch_deposits" 250 " (shard,merchant_pub,wallet_timestamp,exchange_timestamp" 251 " ,refund_deadline,wire_deadline,h_contract_terms,wire_salt" 252 " ,wire_target_h_payto,policy_blocked,total_amount,merchant_sig" 253 " ,done,total_without_fee)" 254 " VALUES (%llu,decode('%s','hex'),0,0,1,1" 255 " ,decode(repeat('e2',64),'hex'),decode(repeat('e2',16),'hex')" 256 " ,decode('%s','hex'),FALSE,ROW(1,0)::taler_amount" 257 " ,decode(repeat('00',64),'hex'),FALSE" 258 " ,ROW(0,90000000)::taler_amount);" 259 "INSERT INTO coin_deposits" 260 " (batch_deposit_serial_id,coin_pub,coin_sig,amount_with_fee)" 261 " VALUES ((SELECT batch_deposit_serial_id FROM batch_deposits" 262 " WHERE merchant_pub=decode('%s','hex'))" 263 " ,decode('%s','hex'),decode(repeat('e2',64),'hex')" 264 " ,ROW(1,0)::taler_amount);" 265 "INSERT INTO refunds" 266 " (coin_pub,batch_deposit_serial_id,merchant_sig,rtransaction_id" 267 " ,amount_with_fee)" 268 " VALUES (decode('%s','hex')" 269 " ,(SELECT batch_deposit_serial_id FROM batch_deposits" 270 " WHERE merchant_pub=decode('%s','hex'))" 271 " ,decode(repeat('00',64),'hex'),1" 272 " ,ROW(0,95000000)::taler_amount);", 273 n_hex, 274 p_hex, 275 n_hex, 276 c_hex, 277 (unsigned long long) TALER_EXCHANGEDB_compute_shard (&merchant_pub), 278 m_hex, 279 p_hex, 280 m_hex, 281 c_hex, 282 c_hex, 283 m_hex); 284 ok = exec_sql (sql); 285 GNUNET_free (sql); 286 GNUNET_free (m_hex); 287 GNUNET_free (p_hex); 288 GNUNET_free (n_hex); 289 GNUNET_free (c_hex); 290 FAILIF (GNUNET_OK != ok); 291 292 /* Before the fix this abort()ed inside TALER_EXCHANGEDB_do_aggregate(). */ 293 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 294 TALER_EXCHANGEDB_do_aggregate (pg, 295 &h_payto, 296 &merchant_pub, 297 &wtid, 298 &total)); 299 FAILIF (0 != 300 TALER_amount_cmp (&zero, 301 &total)); 302 return 0; 303 } 304 305 306 /** 307 * E-13: PostgreSQL accepts COMMIT on a transaction it has already aborted, 308 * rolls it back and answers with the command tag ROLLBACK and no error at 309 * all. TALER_EXCHANGEDB_commit() used to pass that straight through as 310 * GNUNET_DB_STATUS_SUCCESS_NO_RESULTS, i.e. every caller was told the 311 * transaction had committed while its writes were gone. 312 */ 313 static int 314 check_commit_detects_rolled_back_transaction (void) 315 { 316 /* A transaction that is aborted mid-way must NOT commit successfully. */ 317 FAILIF (GNUNET_OK != 318 TALER_EXCHANGEDB_start (pg, 319 "test-e13-aborted")); 320 FAILIF (GNUNET_OK != 321 exec_sql ("INSERT INTO kyc_targets (h_normalized_payto)" 322 " VALUES (decode(repeat('13',32),'hex'));")); 323 /* Provoke an error; from here on the transaction is doomed. */ 324 FAILIF (GNUNET_OK == 325 exec_sql ("SELECT 1/0;")); 326 /* Before the fix this returned SUCCESS_NO_RESULTS (0). */ 327 FAILIF (0 <= 328 TALER_EXCHANGEDB_commit (pg)); 329 /* ...and the row is indeed gone, so 'success' would have been a lie. */ 330 FAILIF (GNUNET_OK != 331 exec_sql ("DO $$ BEGIN" 332 " IF EXISTS (SELECT FROM kyc_targets" 333 " WHERE h_normalized_payto" 334 " =decode(repeat('13',32),'hex'))" 335 " THEN RAISE EXCEPTION 'row survived a rollback';" 336 " END IF; END $$;")); 337 338 /* A clean transaction must still commit and still return exactly 0. */ 339 FAILIF (GNUNET_OK != 340 TALER_EXCHANGEDB_start (pg, 341 "test-e13-clean")); 342 FAILIF (GNUNET_OK != 343 exec_sql ("INSERT INTO kyc_targets (h_normalized_payto)" 344 " VALUES (decode(repeat('14',32),'hex'));")); 345 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 346 TALER_EXCHANGEDB_commit (pg)); 347 FAILIF (GNUNET_OK != 348 exec_sql ("DO $$ BEGIN" 349 " IF NOT EXISTS (SELECT FROM kyc_targets" 350 " WHERE h_normalized_payto" 351 " =decode(repeat('14',32),'hex'))" 352 " THEN RAISE EXCEPTION 'committed row is missing';" 353 " END IF; END $$;")); 354 return 0; 355 } 356 357 358 /** 359 * Helper for the reserve-open checks: create a reserve with the given 360 * balance and run exchange_do_reserve_open() on it. 361 * 362 * @param reserve_pub reserve to create and open 363 * @param desired_expiration expiration the client asks for 364 * @param now current time to assume 365 * @param min_purse_limit number of purses the client asks for 366 * @param open_fee annual account fee of the exchange 367 * @param[out] open_cost set to the cost the exchange computed 368 * @return transaction status 369 */ 370 static enum GNUNET_DB_QueryStatus 371 try_reserve_open (const struct TALER_ReservePublicKeyP *reserve_pub, 372 struct GNUNET_TIME_Timestamp desired_expiration, 373 struct GNUNET_TIME_Timestamp now, 374 uint32_t min_purse_limit, 375 const struct TALER_Amount *open_fee, 376 struct TALER_Amount *open_cost) 377 { 378 struct TALER_ReserveSignatureP reserve_sig; 379 struct TALER_Amount zero; 380 struct TALER_Amount balance; 381 struct GNUNET_TIME_Timestamp final_expiration; 382 bool no_funds; 383 char *sql; 384 385 memset (&reserve_sig, 386 0x51, 387 sizeof (reserve_sig)); 388 parse_amount ("0", 389 &zero); 390 sql = hex_insert_reserve (reserve_pub); 391 GNUNET_assert (GNUNET_OK == 392 exec_sql (sql)); 393 GNUNET_free (sql); 394 return TALER_EXCHANGEDB_do_reserve_open (pg, 395 reserve_pub, 396 &zero, 397 &zero, 398 min_purse_limit, 399 &reserve_sig, 400 desired_expiration, 401 now, 402 open_fee, 403 &no_funds, 404 &balance, 405 open_cost, 406 &final_expiration); 407 } 408 409 410 /** 411 * E-3: `{"reserve_expiration":"never"}` is a perfectly well-formed request: 412 * GNUNET_JSON_spec_timestamp() turns it into GNUNET_TIME_UNIT_FOREVER_ABS and 413 * qconv_abs_time() clamps that to INT64_MAX. The stored procedure then 414 * overflowed INT8 computing the number of years, which SQLSTATE 22003 turns 415 * into a hard error and the handler into an HTTP 500. 416 */ 417 static int 418 check_reserve_open_never_expires (void) 419 { 420 struct TALER_ReservePublicKeyP reserve_pub; 421 struct TALER_Amount open_fee; 422 struct TALER_Amount open_cost; 423 424 memset (&reserve_pub, 425 0x33, 426 sizeof (reserve_pub)); 427 /* No fractional part here; that is E-4's business. */ 428 parse_amount ("1", 429 &open_fee); 430 /* Before the fix: 'bigint out of range' -> HARD_ERROR. */ 431 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 432 try_reserve_open (&reserve_pub, 433 GNUNET_TIME_UNIT_FOREVER_TS, 434 GNUNET_TIME_timestamp_get (), 435 1, 436 &open_fee, 437 &open_cost)); 438 return 0; 439 } 440 441 442 /** 443 * E-4: `my_years * in_open_fee.frac` was an INT4 multiplication that 444 * overflowed *before* the division, so the author's own overflow guard could 445 * never be reached: an exchange whose ACCOUNT_FEE has a fractional part 446 * answered a far-future reserve-open request with an HTTP 500. 447 * 448 * The same block computed the new purse limit in INT4 from the client's 449 * `purse_limit`, which overflows for any value near INT32_MAX. 450 */ 451 static int 452 check_reserve_open_int4_overflows (void) 453 { 454 struct TALER_ReservePublicKeyP reserve_pub; 455 struct TALER_Amount open_fee; 456 struct TALER_Amount open_cost; 457 struct TALER_Amount expected; 458 struct GNUNET_TIME_Timestamp now; 459 460 parse_amount ("0.5", 461 &open_fee); 462 parse_amount ("25", 463 &expected); 464 465 /* 50 years at EUR:0.50/year: 50 * 50000000 does not fit into an INT4. */ 466 memset (&reserve_pub, 467 0x34, 468 sizeof (reserve_pub)); 469 now = GNUNET_TIME_timestamp_get (); 470 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 471 try_reserve_open (&reserve_pub, 472 GNUNET_TIME_absolute_to_timestamp ( 473 GNUNET_TIME_absolute_add ( 474 now.abs_time, 475 GNUNET_TIME_relative_multiply ( 476 GNUNET_TIME_UNIT_YEARS, 477 50))), 478 now, 479 1, 480 &open_fee, 481 &open_cost)); 482 FAILIF (0 != 483 TALER_amount_cmp (&expected, 484 &open_cost)); 485 486 /* An absurd purse_limit must be priced out of range, not overflow. */ 487 memset (&reserve_pub, 488 0x35, 489 sizeof (reserve_pub)); 490 now = GNUNET_TIME_timestamp_get (); 491 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 492 try_reserve_open (&reserve_pub, 493 now, 494 now, 495 2147483647, 496 &open_fee, 497 &open_cost)); 498 return 0; 499 } 500 501 502 /** 503 * E-5: `known_coins.age_commitment_hash` is NULL for every coin without an 504 * age commitment -- the common case -- but the result spec read it without 505 * GNUNET_PQ_result_spec_allow_null(), so building the 409 conflict proof for 506 * POST /purses/$PURSE_PUB/deposit failed with an HTTP 500 instead. 507 */ 508 static int 509 check_purse_deposit_without_age_commitment (void) 510 { 511 struct TALER_PurseContractPublicKeyP purse_pub; 512 struct TALER_CoinSpendPublicKeyP coin_pub; 513 struct TALER_Amount amount; 514 struct TALER_DenominationHashP h_denom_pub; 515 struct TALER_AgeCommitmentHashP hac; 516 struct TALER_CoinSpendSignatureP coin_sig; 517 bool no_age_commitment; 518 char *partner_url = NULL; 519 char *sql; 520 char *p_hex; 521 char *c_hex; 522 enum GNUNET_GenericReturnValue ok; 523 524 memset (&purse_pub, 525 0x55, 526 sizeof (purse_pub)); 527 memset (&coin_pub, 528 0x56, 529 sizeof (coin_pub)); 530 p_hex = to_hex (&purse_pub, 531 sizeof (purse_pub)); 532 c_hex = to_hex (&coin_pub, 533 sizeof (coin_pub)); 534 GNUNET_asprintf ( 535 &sql, 536 "INSERT INTO denominations" 537 " (denom_pub_hash,denom_type,age_mask,denom_pub,master_sig" 538 " ,valid_from,expire_withdraw,expire_deposit,expire_legal" 539 " ,coin,fee_withdraw,fee_deposit,fee_refresh,fee_refund)" 540 " VALUES (decode(repeat('e5',64),'hex'),1,0,decode('00','hex')" 541 " ,decode(repeat('00',64),'hex'),0,0,0,0" 542 " ,ROW(1,0)::taler_amount,ROW(0,0)::taler_amount" 543 " ,ROW(0,0)::taler_amount,ROW(0,0)::taler_amount" 544 " ,ROW(0,0)::taler_amount);" 545 /* age_commitment_hash deliberately left NULL */ 546 "INSERT INTO known_coins" 547 " (denominations_serial,coin_pub,denom_sig,remaining)" 548 " VALUES ((SELECT denominations_serial FROM denominations" 549 " WHERE denom_pub_hash=decode(repeat('e5',64),'hex'))" 550 " ,decode('%s','hex'),decode('00','hex')" 551 " ,ROW(0,0)::taler_amount);" 552 "INSERT INTO purse_deposits" 553 " (purse_pub,coin_pub,amount_with_fee,coin_sig)" 554 " VALUES (decode('%s','hex'),decode('%s','hex')" 555 " ,ROW(1,0)::taler_amount,decode(repeat('e5',64),'hex'));", 556 c_hex, 557 p_hex, 558 c_hex); 559 ok = exec_sql (sql); 560 GNUNET_free (sql); 561 GNUNET_free (p_hex); 562 GNUNET_free (c_hex); 563 FAILIF (GNUNET_OK != ok); 564 565 /* Before the fix: HARD_ERROR from the failed NULL extraction. */ 566 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 567 TALER_EXCHANGEDB_get_purse_deposit (pg, 568 &purse_pub, 569 &coin_pub, 570 &amount, 571 &h_denom_pub, 572 &hac, 573 &no_age_commitment, 574 &coin_sig, 575 &partner_url)); 576 GNUNET_free (partner_url); 577 FAILIF (! no_age_commitment); 578 return 0; 579 } 580 581 582 /** 583 * E-6: a reserve funded by a purse merge has no `reserves_in` row, so the 584 * LEFT JOIN in get_reserve_close_info() returns a NULL payto_uri. Without 585 * allow_null the extraction failed and POST /reserves/$RP/close answered 500 586 * instead of the 409 the handler already implements. 587 */ 588 static int 589 check_reserve_close_info_without_origin (void) 590 { 591 struct TALER_ReservePublicKeyP reserve_pub; 592 struct TALER_Amount balance; 593 struct TALER_FullPayto payto_uri; 594 char *sql; 595 enum GNUNET_GenericReturnValue ok; 596 597 memset (&reserve_pub, 598 0x36, 599 sizeof (reserve_pub)); 600 sql = hex_insert_reserve (&reserve_pub); 601 ok = exec_sql (sql); 602 GNUNET_free (sql); 603 FAILIF (GNUNET_OK != ok); 604 605 /* Before the fix: HARD_ERROR from the failed NULL extraction. */ 606 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 607 TALER_EXCHANGEDB_get_reserve_close_info (pg, 608 &reserve_pub, 609 &balance, 610 &payto_uri)); 611 FAILIF (NULL != payto_uri.full_payto); 612 return 0; 613 } 614 615 616 /** 617 * Read back the bookkeeping of one work shard. 618 * 619 * @param job_name job the shard belongs to 620 * @param start_row inclusive start row of the shard 621 * @param end_row exclusive end row of the shard 622 * @param[out] progress_row how far the shard has come 623 * @param[out] completed whether the shard is done 624 * @return transaction status code 625 */ 626 static enum GNUNET_DB_QueryStatus 627 get_shard_state (const char *job_name, 628 uint64_t start_row, 629 uint64_t end_row, 630 uint64_t *progress_row, 631 bool *completed) 632 { 633 struct GNUNET_PQ_QueryParam params[] = { 634 GNUNET_PQ_query_param_string (job_name), 635 GNUNET_PQ_query_param_uint64 (&start_row), 636 GNUNET_PQ_query_param_uint64 (&end_row), 637 GNUNET_PQ_query_param_end 638 }; 639 struct GNUNET_PQ_ResultSpec rs[] = { 640 GNUNET_PQ_result_spec_uint64 ("progress_row", 641 progress_row), 642 GNUNET_PQ_result_spec_bool ("completed", 643 completed), 644 GNUNET_PQ_result_spec_end 645 }; 646 647 PREPARE (pg, 648 "test_get_shard_state", 649 "SELECT" 650 " progress_row" 651 ",completed" 652 " FROM work_shards" 653 " WHERE job_name=$1" 654 " AND start_row=$2" 655 " AND end_row=$3;"); 656 return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, 657 "test_get_shard_state", 658 params, 659 rs); 660 } 661 662 663 /** 664 * A worker that gets part of the way through a shard and then stops used to 665 * leave nothing behind: the shard was either untouched or completed, so the 666 * next worker to pick it up redid all of it, and nothing it had imported was 667 * visible until the whole shard was done. Check that the progress marker is 668 * kept, that it survives releasing the shard, that it never moves backwards, 669 * and that reaching the end of the shard is what completes it. 670 */ 671 static int 672 check_shard_progress_survives_abort (void) 673 { 674 const char *job = "test-shard-progress"; 675 uint64_t start; 676 uint64_t end; 677 uint64_t progress; 678 uint64_t start2; 679 uint64_t end2; 680 uint64_t progress2; 681 bool completed; 682 683 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 684 TALER_EXCHANGEDB_begin_shard (pg, 685 job, 686 GNUNET_TIME_UNIT_HOURS, 687 1024, 688 &start, 689 &end, 690 &progress)); 691 /* A fresh shard has nothing done yet. */ 692 FAILIF (progress != start); 693 694 /* Get half way, then let go of the shard. */ 695 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 696 TALER_EXCHANGEDB_update_shard_progress (pg, 697 job, 698 start, 699 end, 700 start + 512, 701 GNUNET_TIME_UNIT_HOURS)); 702 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 703 TALER_EXCHANGEDB_abort_shard (pg, 704 job, 705 start, 706 end)); 707 708 /* The next worker gets the same shard back, but resumes in the middle. */ 709 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 710 TALER_EXCHANGEDB_begin_shard (pg, 711 job, 712 GNUNET_TIME_UNIT_HOURS, 713 1024, 714 &start2, 715 &end2, 716 &progress2)); 717 FAILIF (start2 != start); 718 FAILIF (end2 != end); 719 FAILIF (progress2 != start + 512); 720 721 /* A straggler reporting older progress must not rewind the shard. */ 722 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 723 TALER_EXCHANGEDB_update_shard_progress (pg, 724 job, 725 start, 726 end, 727 start + 1, 728 GNUNET_TIME_UNIT_HOURS)); 729 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 730 get_shard_state (job, 731 start, 732 end, 733 &progress, 734 &completed)); 735 FAILIF (progress != start + 512); 736 FAILIF (completed); 737 738 /* Reaching the end completes the shard; there is no second statement for 739 the caller to forget, or to crash before. */ 740 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 741 TALER_EXCHANGEDB_update_shard_progress (pg, 742 job, 743 start, 744 end, 745 end, 746 GNUNET_TIME_UNIT_HOURS)); 747 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 748 get_shard_state (job, 749 start, 750 end, 751 &progress, 752 &completed)); 753 FAILIF (progress != end); 754 FAILIF (! completed); 755 return 0; 756 } 757 758 759 /** 760 * Importing a batch of incoming wire transfers and recording how far the 761 * shard has come is one statement, so a crash cannot land one without the 762 * other. Check that a batch moves both, that importing it again is harmless 763 * and reported as a duplicate, and that the shard completes as part of the 764 * import rather than in a transaction of its own. 765 */ 766 static int 767 check_import_credits_advances_shard (void) 768 { 769 const char *job = "test-import-credits"; 770 struct TALER_ReservePublicKeyP reserve_pub; 771 struct TALER_Amount balance; 772 struct TALER_EXCHANGEDB_ReserveInInfo reserve; 773 struct TALER_EXCHANGEDB_CreditBatch batch; 774 enum GNUNET_DB_QueryStatus results[1]; 775 uint64_t start; 776 uint64_t end; 777 uint64_t progress; 778 bool completed; 779 780 memset (&reserve_pub, 781 0x51, 782 sizeof (reserve_pub)); 783 parse_amount ("4.00", 784 &balance); 785 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 786 TALER_EXCHANGEDB_begin_shard (pg, 787 job, 788 GNUNET_TIME_UNIT_HOURS, 789 1024, 790 &start, 791 &end, 792 &progress)); 793 reserve.reserve_pub = &reserve_pub; 794 reserve.balance = &balance; 795 reserve.execution_time = GNUNET_TIME_timestamp_get (); 796 reserve.sender_account_details.full_payto 797 = (char *) "payto://x-taler-bank/localhost/shard-test?receiver-name=Shard"; 798 reserve.wire_reference = start + 1; 799 memset (&batch, 800 0, 801 sizeof (batch)); 802 batch.exchange_account_name = "exchange-account-test"; 803 batch.reserves = &reserve; 804 batch.reserves_length = 1; 805 batch.job_name = job; 806 batch.shard_start = start; 807 batch.shard_end = end; 808 batch.progress_row = start + 1; 809 batch.lease = GNUNET_TIME_UNIT_HOURS; 810 811 FAILIF (0 > 812 TALER_EXCHANGEDB_do_import_credits (pg, 813 &batch, 814 results)); 815 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != results[0]); 816 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 817 get_shard_state (job, 818 start, 819 end, 820 &progress, 821 &completed)); 822 /* The transfer is committed and so is the fact that we consumed its row -- 823 long before the rest of the shard has been looked at. */ 824 FAILIF (progress != start + 1); 825 FAILIF (completed); 826 827 /* Re-importing the same batch is what happens whenever a shard is picked up 828 twice. It has to be harmless. */ 829 batch.progress_row = end; 830 FAILIF (0 > 831 TALER_EXCHANGEDB_do_import_credits (pg, 832 &batch, 833 results)); 834 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != results[0]); 835 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 836 get_shard_state (job, 837 start, 838 end, 839 &progress, 840 &completed)); 841 FAILIF (progress != end); 842 FAILIF (! completed); 843 return 0; 844 } 845 846 847 /** 848 * All checks we know about. 849 */ 850 static const struct 851 { 852 const char *name; 853 int (*fn)(void); 854 } tests[] = { 855 { "aggregate-refund-below-deposit-fee", 856 &check_aggregate_refund_below_deposit_fee }, 857 { "commit-detects-rolled-back-transaction", 858 &check_commit_detects_rolled_back_transaction }, 859 { "reserve-open-never-expires", 860 &check_reserve_open_never_expires }, 861 { "reserve-open-int4-overflows", 862 &check_reserve_open_int4_overflows }, 863 { "purse-deposit-without-age-commitment", 864 &check_purse_deposit_without_age_commitment }, 865 { "reserve-close-info-without-origin", 866 &check_reserve_close_info_without_origin }, 867 { "shard-progress-survives-abort", 868 &check_shard_progress_survives_abort }, 869 { "import-credits-advances-shard", 870 &check_import_credits_advances_shard }, 871 { NULL, NULL } 872 }; 873 874 875 /** 876 * Main function that runs the checks. 877 * 878 * @param cls closure 879 * @param args remaining command-line arguments 880 * @param cfgfile name of the configuration file used 881 * @param cfg configuration 882 */ 883 static void 884 run (void *cls, 885 char *const *args, 886 const char *cfgfile, 887 const struct GNUNET_CONFIGURATION_Handle *cfg) 888 { 889 unsigned int ran = 0; 890 891 (void) cls; 892 (void) args; 893 (void) cfgfile; 894 pg = TALER_EXCHANGEDB_connect_admin (cfg); 895 if (NULL == pg) 896 { 897 fprintf (stderr, 898 "Failed to connect to the database\n"); 899 result = 77; 900 return; 901 } 902 if (GNUNET_OK != 903 TALER_EXCHANGEDB_create_tables (pg, 904 false, 905 0)) 906 { 907 fprintf (stderr, 908 "Failed to create the database schema\n"); 909 result = 77; 910 goto cleanup; 911 } 912 for (unsigned int i = 0; NULL != tests[i].name; i++) 913 { 914 if ( (NULL != only) && 915 (0 != strcmp (only, 916 tests[i].name)) ) 917 continue; 918 fprintf (stderr, 919 "Running check `%s'\n", 920 tests[i].name); 921 ran++; 922 if (0 != tests[i].fn ()) 923 { 924 fprintf (stderr, 925 "Check `%s' FAILED\n", 926 tests[i].name); 927 result = 1; 928 } 929 } 930 if (0 == ran) 931 { 932 fprintf (stderr, 933 "No check matched `%s'\n", 934 only); 935 result = 1; 936 } 937 cleanup: 938 TALER_EXCHANGEDB_disconnect (pg); 939 pg = NULL; 940 } 941 942 943 int 944 main (int argc, 945 char *const *argv) 946 { 947 struct GNUNET_GETOPT_CommandLineOption options[] = { 948 GNUNET_GETOPT_option_string ('t', 949 "test", 950 "NAME", 951 "only run the check called NAME", 952 &only), 953 GNUNET_GETOPT_OPTION_END 954 }; 955 enum GNUNET_GenericReturnValue ret; 956 957 result = 0; 958 ret = GNUNET_PROGRAM_run (TALER_EXCHANGE_project_data (), 959 argc, 960 argv, 961 "test-regressions", 962 "Regression tests for the exchange database layer", 963 options, 964 &run, 965 NULL); 966 if (GNUNET_SYSERR == ret) 967 return 3; 968 if (GNUNET_NO == ret) 969 return 0; 970 return result; 971 } 972 973 974 /* end of test_regressions.c */