testing_api_cmd_coin_history.c (17540B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2023 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify 6 it under the terms of the GNU General Public License as 7 published by the Free Software Foundation; either version 3, or 8 (at your option) any later version. 9 10 TALER is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 GNU General Public License for more details. 14 15 You should have received a copy of the GNU General Public 16 License along with TALER; see the file COPYING. If not, see 17 <http://www.gnu.org/licenses/> 18 */ 19 /** 20 * @file testing/testing_api_cmd_coin_history.c 21 * @brief Implement the /coins/$COIN_PUB/history test command. 22 * @author Christian Grothoff 23 */ 24 #include "taler/taler_json_lib.h" 25 #include <gnunet/gnunet_curl_lib.h> 26 #include "taler/taler_testing_lib.h" 27 28 29 /** 30 * State for a "history" CMD. 31 */ 32 struct HistoryState 33 { 34 35 /** 36 * Public key of the coin being analyzed. 37 */ 38 struct TALER_CoinSpendPublicKeyP coin_pub; 39 40 /** 41 * Label to the command which created the coin to check, 42 * needed to resort the coin key. 43 */ 44 const char *coin_reference; 45 46 /** 47 * Handle to the "coin history" operation. 48 */ 49 struct TALER_EXCHANGE_GetCoinsHistoryHandle *rsh; 50 51 /** 52 * Expected coin balance. 53 */ 54 const char *expected_balance; 55 56 /** 57 * Private key of the coin being analyzed. 58 */ 59 const struct TALER_CoinSpendPrivateKeyP *coin_priv; 60 61 /** 62 * Interpreter state. 63 */ 64 struct TALER_TESTING_Interpreter *is; 65 66 /** 67 * Expected HTTP response code. 68 */ 69 unsigned int expected_response_code; 70 71 }; 72 73 74 /** 75 * Closure for analysis_cb(). 76 */ 77 struct AnalysisContext 78 { 79 /** 80 * Coin public key we are looking at. 81 */ 82 const struct TALER_CoinSpendPublicKeyP *coin_pub; 83 84 /** 85 * Length of the @e history array. 86 */ 87 unsigned int history_length; 88 89 /** 90 * Array of history items to match. 91 */ 92 const struct TALER_EXCHANGE_CoinHistoryEntry *history; 93 94 /** 95 * Array of @e history_length of matched entries. 96 */ 97 bool *found; 98 99 /** 100 * Set to true if an entry could not be found. 101 */ 102 bool failure; 103 }; 104 105 106 /** 107 * Compare @a h1 and @a h2. 108 * 109 * @param h1 a history entry 110 * @param h2 a history entry 111 * @return 0 if @a h1 and @a h2 are equal 112 */ 113 static int 114 history_entry_cmp ( 115 const struct TALER_EXCHANGE_CoinHistoryEntry *h1, 116 const struct TALER_EXCHANGE_CoinHistoryEntry *h2) 117 { 118 if (h1->type != h2->type) 119 return 1; 120 if (0 != TALER_amount_cmp (&h1->amount, 121 &h2->amount)) 122 { 123 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 124 "Amount mismatch (%s)\n", 125 TALER_amount2s (&h1->amount)); 126 return 1; 127 } 128 switch (h1->type) 129 { 130 case TALER_EXCHANGE_CTT_NONE: 131 GNUNET_break (0); 132 break; 133 case TALER_EXCHANGE_CTT_DEPOSIT: 134 if (0 != GNUNET_memcmp (&h1->details.deposit.h_contract_terms, 135 &h2->details.deposit.h_contract_terms)) 136 return 1; 137 if (0 != GNUNET_memcmp (&h1->details.deposit.merchant_pub, 138 &h2->details.deposit.merchant_pub)) 139 return 1; 140 if (0 != GNUNET_memcmp (&h1->details.deposit.h_wire, 141 &h2->details.deposit.h_wire)) 142 return 1; 143 if (0 != GNUNET_memcmp (&h1->details.deposit.sig, 144 &h2->details.deposit.sig)) 145 return 1; 146 return 0; 147 case TALER_EXCHANGE_CTT_MELT: 148 if (0 != GNUNET_memcmp (&h1->details.melt.h_age_commitment, 149 &h2->details.melt.h_age_commitment)) 150 return 1; 151 /* Note: most other fields are not initialized 152 in the trait as they are hard to extract from 153 the API */ 154 return 0; 155 case TALER_EXCHANGE_CTT_REFUND: 156 if (0 != GNUNET_memcmp (&h1->details.refund.sig, 157 &h2->details.refund.sig)) 158 return 1; 159 return 0; 160 case TALER_EXCHANGE_CTT_RECOUP: 161 if (0 != GNUNET_memcmp (&h1->details.recoup.coin_sig, 162 &h2->details.recoup.coin_sig)) 163 return 1; 164 /* Note: exchange_sig, exchange_pub and timestamp are 165 fundamentally not available in the initiating command */ 166 return 0; 167 case TALER_EXCHANGE_CTT_RECOUP_REFRESH: 168 if (0 != GNUNET_memcmp (&h1->details.recoup_refresh.coin_sig, 169 &h2->details.recoup_refresh.coin_sig)) 170 return 1; 171 /* Note: exchange_sig, exchange_pub and timestamp are 172 fundamentally not available in the initiating command */ 173 return 0; 174 case TALER_EXCHANGE_CTT_OLD_COIN_RECOUP: 175 if (0 != GNUNET_memcmp (&h1->details.old_coin_recoup.new_coin_pub, 176 &h2->details.old_coin_recoup.new_coin_pub)) 177 return 1; 178 /* Note: exchange_sig, exchange_pub and timestamp are 179 fundamentally not available in the initiating command */ 180 return 0; 181 case TALER_EXCHANGE_CTT_PURSE_DEPOSIT: 182 /* coin_sig is not initialized */ 183 if (0 != GNUNET_memcmp (&h1->details.purse_deposit.purse_pub, 184 &h2->details.purse_deposit.purse_pub)) 185 { 186 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 187 "Purse public key mismatch\n"); 188 return 1; 189 } 190 if (0 != strcmp (h1->details.purse_deposit.exchange_base_url, 191 h2->details.purse_deposit.exchange_base_url)) 192 { 193 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 194 "Exchange base URL mismatch (%s/%s)\n", 195 h1->details.purse_deposit.exchange_base_url, 196 h2->details.purse_deposit.exchange_base_url); 197 GNUNET_break (0); 198 return 1; 199 } 200 return 0; 201 case TALER_EXCHANGE_CTT_PURSE_REFUND: 202 /* NOTE: not supported yet (trait not returned) */ 203 return 0; 204 case TALER_EXCHANGE_CTT_RESERVE_OPEN_DEPOSIT: 205 /* NOTE: not supported yet (trait not returned) */ 206 if (0 != GNUNET_memcmp (&h1->details.reserve_open_deposit.coin_sig, 207 &h2->details.reserve_open_deposit.coin_sig)) 208 return 1; 209 return 0; 210 } 211 GNUNET_assert (0); 212 return -1; 213 } 214 215 216 /** 217 * Check if @a cmd changed the coin, if so, find the 218 * entry in our history and set the respective index in found 219 * to true. If the entry is not found, set failure. 220 * 221 * @param cls our `struct AnalysisContext *` 222 * @param cmd command to analyze for impact on history 223 */ 224 static void 225 analyze_command (void *cls, 226 const struct TALER_TESTING_Command *cmd) 227 { 228 struct AnalysisContext *ac = cls; 229 const struct TALER_CoinSpendPublicKeyP *coin_pub = ac->coin_pub; 230 const struct TALER_EXCHANGE_CoinHistoryEntry *history = ac->history; 231 unsigned int history_length = ac->history_length; 232 bool *found = ac->found; 233 234 if (TALER_TESTING_cmd_is_batch (cmd)) 235 { 236 struct TALER_TESTING_Command *cur; 237 struct TALER_TESTING_Command *bcmd; 238 239 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 240 "Checking `%s' for history of coin `%s'\n", 241 cmd->label, 242 TALER_B2S (coin_pub)); 243 cur = TALER_TESTING_cmd_batch_get_current (cmd); 244 if (GNUNET_OK != 245 TALER_TESTING_get_trait_batch_cmds (cmd, 246 &bcmd)) 247 { 248 GNUNET_break (0); 249 ac->failure = true; 250 return; 251 } 252 for (unsigned int i = 0; NULL != bcmd[i].label; i++) 253 { 254 struct TALER_TESTING_Command *step = &bcmd[i]; 255 256 analyze_command (ac, 257 step); 258 if (ac->failure) 259 { 260 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 261 "Entry for batch step `%s' missing in coin history\n", 262 step->label); 263 return; 264 } 265 if (step == cur) 266 break; /* if *we* are in a batch, make sure not to analyze commands past 'now' */ 267 } 268 return; 269 } 270 271 for (unsigned int j = 0; true; j++) 272 { 273 const struct TALER_CoinSpendPublicKeyP *rp; 274 const struct TALER_EXCHANGE_CoinHistoryEntry *he; 275 bool matched = false; 276 277 if (GNUNET_OK != 278 TALER_TESTING_get_trait_coin_pub (cmd, 279 j, 280 &rp)) 281 { 282 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 283 "Command `%s#%u' has no public key for a coin\n", 284 cmd->label, 285 j); 286 break; /* command does nothing for coins */ 287 } 288 if (0 != 289 GNUNET_memcmp (rp, 290 coin_pub)) 291 { 292 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 293 "Command `%s#%u' is about another coin %s\n", 294 cmd->label, 295 j, 296 TALER_B2S (rp)); 297 continue; /* command affects some _other_ coin */ 298 } 299 if (GNUNET_OK != 300 TALER_TESTING_get_trait_coin_history (cmd, 301 j, 302 &he)) 303 { 304 /* NOTE: only for debugging... */ 305 if (0 == j) 306 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 307 "Command `%s' has the coin_pub, but lacks coin history trait\n", 308 cmd->label); 309 return; /* command does nothing for coins */ 310 } 311 for (unsigned int i = 0; i<history_length; i++) 312 { 313 if (found[i]) 314 continue; /* already found, skip */ 315 if (0 == 316 history_entry_cmp (he, 317 &history[i])) 318 { 319 found[i] = true; 320 matched = true; 321 break; 322 } 323 } 324 if (! matched) 325 { 326 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 327 "Command `%s' coin history entry #%u not found\n", 328 cmd->label, 329 j); 330 ac->failure = true; 331 return; 332 } 333 } 334 } 335 336 337 /** 338 * Check that the coin balance and HTTP response code are 339 * both acceptable. 340 * 341 * @param cls closure. 342 * @param rs HTTP response details 343 */ 344 static void 345 coin_history_cb (void *cls, 346 const struct TALER_EXCHANGE_GetCoinsHistoryResponse *rs) 347 { 348 struct HistoryState *ss = cls; 349 struct TALER_TESTING_Interpreter *is = ss->is; 350 struct TALER_Amount eb; 351 unsigned int hlen; 352 353 ss->rsh = NULL; 354 if (ss->expected_response_code != rs->hr.http_status) 355 { 356 TALER_TESTING_unexpected_status (ss->is, 357 rs->hr.http_status, 358 ss->expected_response_code); 359 return; 360 } 361 if (MHD_HTTP_OK != rs->hr.http_status) 362 { 363 TALER_TESTING_interpreter_next (is); 364 return; 365 } 366 GNUNET_assert (GNUNET_OK == 367 TALER_string_to_amount (ss->expected_balance, 368 &eb)); 369 370 if (0 != TALER_amount_cmp (&eb, 371 &rs->details.ok.balance)) 372 { 373 GNUNET_break (0); 374 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 375 "Unexpected balance for coin: %s\n", 376 TALER_amount_to_string (&rs->details.ok.balance)); 377 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 378 "Expected balance of: %s\n", 379 TALER_amount_to_string (&eb)); 380 TALER_TESTING_interpreter_fail (ss->is); 381 return; 382 } 383 hlen = json_array_size (rs->details.ok.history); 384 { 385 bool found[GNUNET_NZL (hlen)]; 386 struct TALER_EXCHANGE_CoinHistoryEntry rhist[GNUNET_NZL (hlen)]; 387 struct AnalysisContext ac = { 388 .coin_pub = &ss->coin_pub, 389 .history = rhist, 390 .history_length = hlen, 391 .found = found 392 }; 393 const struct TALER_EXCHANGE_DenomPublicKey *dk; 394 struct TALER_Amount total_in; 395 struct TALER_Amount total_out; 396 struct TALER_Amount hbal; 397 398 dk = TALER_EXCHANGE_get_denomination_key_by_hash ( 399 TALER_TESTING_get_keys (is), 400 &rs->details.ok.h_denom_pub); 401 memset (found, 402 0, 403 sizeof (found)); 404 memset (rhist, 405 0, 406 sizeof (rhist)); 407 if (GNUNET_OK != 408 TALER_EXCHANGE_parse_coin_history ( 409 TALER_TESTING_get_keys (is), 410 dk, 411 rs->details.ok.history, 412 &ss->coin_pub, 413 &total_in, 414 &total_out, 415 hlen, 416 rhist)) 417 { 418 GNUNET_break (0); 419 json_dumpf (rs->hr.reply, 420 stderr, 421 JSON_INDENT (2)); 422 TALER_TESTING_interpreter_fail (ss->is); 423 return; 424 } 425 if (0 > 426 TALER_amount_subtract (&hbal, 427 &total_in, 428 &total_out)) 429 { 430 GNUNET_break (0); 431 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 432 "Coin credits: %s\n", 433 TALER_amount2s (&total_in)); 434 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 435 "Coin debits: %s\n", 436 TALER_amount2s (&total_out)); 437 TALER_TESTING_interpreter_fail (ss->is); 438 return; 439 } 440 if (0 != TALER_amount_cmp (&hbal, 441 &rs->details.ok.balance)) 442 { 443 GNUNET_break (0); 444 TALER_TESTING_interpreter_fail (ss->is); 445 return; 446 } 447 (void) ac; 448 TALER_TESTING_iterate (is, 449 true, 450 &analyze_command, 451 &ac); 452 if (ac.failure) 453 { 454 json_dumpf (rs->hr.reply, 455 stderr, 456 JSON_INDENT (2)); 457 TALER_TESTING_interpreter_fail (ss->is); 458 return; 459 } 460 #if 1 461 for (unsigned int i = 0; i<hlen; i++) 462 { 463 if (found[i]) 464 continue; 465 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 466 "History entry at index %u of type %d not justified by command history\n", 467 i, 468 rs->details.ok.history[i].type); 469 json_dumpf (rs->hr.reply, 470 stderr, 471 JSON_INDENT (2)); 472 TALER_TESTING_interpreter_fail (ss->is); 473 return; 474 } 475 #endif 476 } 477 TALER_TESTING_interpreter_next (is); 478 } 479 480 481 /** 482 * Run the command. 483 * 484 * @param cls closure. 485 * @param cmd the command being executed. 486 * @param is the interpreter state. 487 */ 488 static void 489 history_run (void *cls, 490 const struct TALER_TESTING_Command *cmd, 491 struct TALER_TESTING_Interpreter *is) 492 { 493 struct HistoryState *ss = cls; 494 const struct TALER_TESTING_Command *create_coin; 495 char *cref; 496 unsigned int idx; 497 498 ss->is = is; 499 GNUNET_assert ( 500 GNUNET_OK == 501 TALER_TESTING_parse_coin_reference ( 502 ss->coin_reference, 503 &cref, 504 &idx)); 505 create_coin 506 = TALER_TESTING_interpreter_lookup_command (is, 507 cref); 508 GNUNET_free (cref); 509 if (NULL == create_coin) 510 { 511 GNUNET_break (0); 512 TALER_TESTING_interpreter_fail (is); 513 return; 514 } 515 if (GNUNET_OK != 516 TALER_TESTING_get_trait_coin_priv (create_coin, 517 idx, 518 &ss->coin_priv)) 519 { 520 GNUNET_break (0); 521 TALER_LOG_ERROR ("Failed to find coin_priv for history query\n"); 522 TALER_TESTING_interpreter_fail (is); 523 return; 524 } 525 GNUNET_CRYPTO_eddsa_key_get_public (&ss->coin_priv->eddsa_priv, 526 &ss->coin_pub.eddsa_pub); 527 ss->rsh = TALER_EXCHANGE_get_coins_history_create ( 528 TALER_TESTING_interpreter_get_context (is), 529 TALER_TESTING_get_exchange_url (is), 530 ss->coin_priv); 531 GNUNET_assert (NULL != ss->rsh); 532 GNUNET_assert (TALER_EC_NONE == 533 TALER_EXCHANGE_get_coins_history_start (ss->rsh, 534 &coin_history_cb, 535 ss)); 536 } 537 538 539 /** 540 * Offer internal data from a "history" CMD, to other commands. 541 * 542 * @param cls closure. 543 * @param[out] ret result. 544 * @param trait name of the trait. 545 * @param index index number of the object to offer. 546 * @return #GNUNET_OK on success. 547 */ 548 static enum GNUNET_GenericReturnValue 549 history_traits (void *cls, 550 const void **ret, 551 const char *trait, 552 unsigned int index) 553 { 554 struct HistoryState *hs = cls; 555 struct TALER_TESTING_Trait traits[] = { 556 TALER_TESTING_make_trait_coin_pub (index, 557 &hs->coin_pub), 558 TALER_TESTING_trait_end () 559 }; 560 561 return TALER_TESTING_get_trait (traits, 562 ret, 563 trait, 564 index); 565 } 566 567 568 /** 569 * Cleanup the state from a "coin history" CMD, and possibly 570 * cancel a pending operation thereof. 571 * 572 * @param cls closure. 573 * @param cmd the command which is being cleaned up. 574 */ 575 static void 576 history_cleanup (void *cls, 577 const struct TALER_TESTING_Command *cmd) 578 { 579 struct HistoryState *ss = cls; 580 581 if (NULL != ss->rsh) 582 { 583 TALER_TESTING_command_incomplete (ss->is, 584 cmd->label); 585 TALER_EXCHANGE_get_coins_history_cancel (ss->rsh); 586 ss->rsh = NULL; 587 } 588 GNUNET_free (ss); 589 } 590 591 592 struct TALER_TESTING_Command 593 TALER_TESTING_cmd_coin_history (const char *label, 594 const char *coin_reference, 595 const char *expected_balance, 596 unsigned int expected_response_code) 597 { 598 struct HistoryState *ss; 599 600 GNUNET_assert (NULL != coin_reference); 601 ss = GNUNET_new (struct HistoryState); 602 ss->coin_reference = coin_reference; 603 ss->expected_balance = expected_balance; 604 ss->expected_response_code = expected_response_code; 605 { 606 struct TALER_TESTING_Command cmd = { 607 .cls = ss, 608 .label = label, 609 .run = &history_run, 610 .cleanup = &history_cleanup, 611 .traits = &history_traits 612 }; 613 614 return cmd; 615 } 616 }