testing_api_cmd_reserve_history.c (17524B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2014-2024 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_reserve_history.c 21 * @brief Implement the /reserve/history test command. 22 * @author Marcello Stanisci 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 reserve being analyzed. 37 */ 38 struct TALER_ReservePublicKeyP reserve_pub; 39 40 /** 41 * Label to the command which created the reserve to check, 42 * needed to resort the reserve key. 43 */ 44 const char *reserve_reference; 45 46 /** 47 * Handle to the "reserve history" operation. 48 */ 49 struct TALER_EXCHANGE_GetReservesHistoryHandle *rsh; 50 51 /** 52 * Expected reserve balance. 53 */ 54 const char *expected_balance; 55 56 /** 57 * Private key of the reserve being analyzed. 58 */ 59 const struct TALER_ReservePrivateKeyP *reserve_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 * Reserve public key we are looking at. 81 */ 82 const struct TALER_ReservePublicKeyP *reserve_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_ReserveHistoryEntry *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_ReserveHistoryEntry *h1, 116 const struct TALER_EXCHANGE_ReserveHistoryEntry *h2) 117 { 118 if (h1->type != h2->type) 119 return 1; 120 switch (h1->type) 121 { 122 case TALER_EXCHANGE_RTT_CREDIT: 123 if ( (0 == 124 TALER_amount_cmp (&h1->amount, 125 &h2->amount)) && 126 (0 == 127 TALER_full_payto_cmp (h1->details.in_details.sender_url, 128 h2->details.in_details.sender_url)) && 129 (h1->details.in_details.wire_reference == 130 h2->details.in_details.wire_reference) && 131 (GNUNET_TIME_timestamp_cmp (h1->details.in_details.timestamp, 132 ==, 133 h2->details.in_details.timestamp)) ) 134 return 0; 135 return 1; 136 case TALER_EXCHANGE_RTT_WITHDRAWAL: 137 if ( (0 == 138 TALER_amount_cmp (&h1->amount, 139 &h2->amount)) && 140 (0 == 141 TALER_amount_cmp (&h1->details.withdraw.fee, 142 &h2->details.withdraw.fee)) && 143 (h1->details.withdraw.age_restricted == 144 h2->details.withdraw.age_restricted) && 145 ((! h1->details.withdraw.age_restricted) || 146 (h1->details.withdraw.max_age == h2->details.withdraw.max_age) )) 147 return 0; 148 return 1; 149 case TALER_EXCHANGE_RTT_RECOUP: 150 /* exchange_sig, exchange_pub and timestamp are NOT available 151 from the original recoup response, hence here NOT check(able/ed) */ 152 if ( (0 == 153 TALER_amount_cmp (&h1->amount, 154 &h2->amount)) && 155 (0 == 156 GNUNET_memcmp (&h1->details.recoup_details.coin_pub, 157 &h2->details.recoup_details.coin_pub)) ) 158 return 0; 159 return 1; 160 case TALER_EXCHANGE_RTT_CLOSING: 161 /* testing_api_cmd_exec_closer doesn't set the 162 receiver_account_details, exchange_sig, exchange_pub or wtid or timestamp 163 so we cannot test for it here. but if the amount matches, 164 that should be good enough. */ 165 if ( (0 == 166 TALER_amount_cmp (&h1->amount, 167 &h2->amount)) && 168 (0 == 169 TALER_amount_cmp (&h1->details.close_details.fee, 170 &h2->details.close_details.fee)) ) 171 return 0; 172 return 1; 173 case TALER_EXCHANGE_RTT_MERGE: 174 if ( (0 == 175 TALER_amount_cmp (&h1->amount, 176 &h2->amount)) && 177 (0 == 178 TALER_amount_cmp (&h1->details.merge_details.purse_fee, 179 &h2->details.merge_details.purse_fee)) && 180 (GNUNET_TIME_timestamp_cmp (h1->details.merge_details.merge_timestamp, 181 ==, 182 h2->details.merge_details.merge_timestamp)) 183 && 184 (GNUNET_TIME_timestamp_cmp (h1->details.merge_details.purse_expiration, 185 ==, 186 h2->details.merge_details.purse_expiration) 187 ) 188 && 189 (0 == 190 GNUNET_memcmp (&h1->details.merge_details.merge_pub, 191 &h2->details.merge_details.merge_pub)) && 192 (0 == 193 GNUNET_memcmp (&h1->details.merge_details.h_contract_terms, 194 &h2->details.merge_details.h_contract_terms)) && 195 (0 == 196 GNUNET_memcmp (&h1->details.merge_details.purse_pub, 197 &h2->details.merge_details.purse_pub)) && 198 (0 == 199 GNUNET_memcmp (&h1->details.merge_details.reserve_sig, 200 &h2->details.merge_details.reserve_sig)) && 201 (h1->details.merge_details.min_age == 202 h2->details.merge_details.min_age) && 203 (h1->details.merge_details.flags == 204 h2->details.merge_details.flags) ) 205 return 0; 206 return 1; 207 case TALER_EXCHANGE_RTT_OPEN: 208 if ( (0 == 209 TALER_amount_cmp (&h1->amount, 210 &h2->amount)) && 211 (GNUNET_TIME_timestamp_cmp ( 212 h1->details.open_request.request_timestamp, 213 ==, 214 h2->details.open_request.request_timestamp)) && 215 (GNUNET_TIME_timestamp_cmp ( 216 h1->details.open_request.reserve_expiration, 217 ==, 218 h2->details.open_request.reserve_expiration)) && 219 (h1->details.open_request.purse_limit == 220 h2->details.open_request.purse_limit) && 221 (0 == 222 TALER_amount_cmp (&h1->details.open_request.reserve_payment, 223 &h2->details.open_request.reserve_payment)) && 224 (0 == 225 GNUNET_memcmp (&h1->details.open_request.reserve_sig, 226 &h2->details.open_request.reserve_sig)) ) 227 return 0; 228 return 1; 229 case TALER_EXCHANGE_RTT_CLOSE: 230 if ( (0 == 231 TALER_amount_cmp (&h1->amount, 232 &h2->amount)) && 233 (GNUNET_TIME_timestamp_cmp ( 234 h1->details.close_request.request_timestamp, 235 ==, 236 h2->details.close_request.request_timestamp)) && 237 (0 == 238 GNUNET_memcmp (&h1->details.close_request.target_account_h_payto, 239 &h2->details.close_request.target_account_h_payto)) && 240 (0 == 241 GNUNET_memcmp (&h1->details.close_request.reserve_sig, 242 &h2->details.close_request.reserve_sig)) ) 243 return 0; 244 return 1; 245 } 246 GNUNET_assert (0); 247 return 1; 248 } 249 250 251 /** 252 * Check if @a cmd changed the reserve, if so, find the 253 * entry in our history and set the respective index in found 254 * to true. If the entry is not found, set failure. 255 * 256 * @param cls our `struct AnalysisContext *` 257 * @param cmd command to analyze for impact on history 258 */ 259 static void 260 analyze_command (void *cls, 261 const struct TALER_TESTING_Command *cmd) 262 { 263 struct AnalysisContext *ac = cls; 264 const struct TALER_ReservePublicKeyP *reserve_pub = ac->reserve_pub; 265 const struct TALER_EXCHANGE_ReserveHistoryEntry *history = ac->history; 266 unsigned int history_length = ac->history_length; 267 bool *found = ac->found; 268 269 if (TALER_TESTING_cmd_is_batch (cmd)) 270 { 271 struct TALER_TESTING_Command *cur; 272 struct TALER_TESTING_Command *bcmd; 273 274 cur = TALER_TESTING_cmd_batch_get_current (cmd); 275 if (GNUNET_OK != 276 TALER_TESTING_get_trait_batch_cmds (cmd, 277 &bcmd)) 278 { 279 GNUNET_break (0); 280 ac->failure = true; 281 return; 282 } 283 for (unsigned int i = 0; NULL != bcmd[i].label; i++) 284 { 285 struct TALER_TESTING_Command *step = &bcmd[i]; 286 287 analyze_command (ac, 288 step); 289 if (ac->failure) 290 { 291 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 292 "Entry for batch step `%s' missing in reserve history\n", 293 step->label); 294 return; 295 } 296 if (step == cur) 297 break; /* if *we* are in a batch, make sure not to analyze commands past 'now' */ 298 } 299 return; 300 } 301 302 { 303 const struct TALER_ReservePublicKeyP *rp; 304 bool matched = false; 305 306 if (GNUNET_OK != 307 TALER_TESTING_get_trait_reserve_pub (cmd, 308 &rp)) 309 return; /* command does nothing for reserves */ 310 if (0 != 311 GNUNET_memcmp (rp, 312 reserve_pub)) 313 return; /* command affects some _other_ reserve */ 314 for (unsigned int j = 0; true; j++) 315 { 316 const struct TALER_EXCHANGE_ReserveHistoryEntry *he; 317 318 if (GNUNET_OK != 319 TALER_TESTING_get_trait_reserve_history (cmd, 320 j, 321 &he)) 322 { 323 /* NOTE: only for debugging... */ 324 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 325 "Command `%s' has the reserve_pub, but lacks reserve history trait for index #%u\n", 326 cmd->label, 327 j); 328 return; /* command does nothing for reserves */ 329 } 330 for (unsigned int i = 0; i<history_length; i++) 331 { 332 if (found[i]) 333 continue; /* already found, skip */ 334 if (0 == 335 history_entry_cmp (he, 336 &history[i])) 337 { 338 found[i] = true; 339 matched = true; 340 ac->failure = false; 341 break; 342 } 343 } 344 if (matched) 345 break; 346 } 347 if (! matched) 348 { 349 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 350 "Command `%s' no relevant reserve history entry not found\n", 351 cmd->label); 352 ac->failure = true; 353 ; 354 } 355 } 356 } 357 358 359 /** 360 * Check that the reserve balance and HTTP response code are 361 * both acceptable. 362 * 363 * @param cls closure. 364 * @param rs HTTP response details 365 */ 366 static void 367 reserve_history_cb (void *cls, 368 const struct TALER_EXCHANGE_GetReservesHistoryResponse *rs) 369 { 370 struct HistoryState *ss = cls; 371 struct TALER_TESTING_Interpreter *is = ss->is; 372 struct TALER_Amount eb; 373 374 ss->rsh = NULL; 375 if (ss->expected_response_code != rs->hr.http_status) 376 { 377 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 378 "Unexpected HTTP response code: %d in %s:%u\n", 379 rs->hr.http_status, 380 __FILE__, 381 __LINE__); 382 json_dumpf (rs->hr.reply, 383 stderr, 384 0); 385 TALER_TESTING_interpreter_fail (ss->is); 386 return; 387 } 388 if (MHD_HTTP_OK != rs->hr.http_status) 389 { 390 TALER_TESTING_interpreter_next (is); 391 return; 392 } 393 GNUNET_assert (GNUNET_OK == 394 TALER_string_to_amount (ss->expected_balance, 395 &eb)); 396 397 if (0 != TALER_amount_cmp (&eb, 398 &rs->details.ok.balance)) 399 { 400 GNUNET_break (0); 401 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 402 "Unexpected amount in reserve: %s\n", 403 TALER_amount_to_string (&rs->details.ok.balance)); 404 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 405 "Expected balance of: %s\n", 406 TALER_amount_to_string (&eb)); 407 TALER_TESTING_interpreter_fail (ss->is); 408 return; 409 } 410 { 411 bool found[rs->details.ok.history_len]; 412 struct AnalysisContext ac = { 413 .reserve_pub = &ss->reserve_pub, 414 .history = rs->details.ok.history, 415 .history_length = rs->details.ok.history_len, 416 .found = found 417 }; 418 419 memset (found, 420 0, 421 sizeof (found)); 422 TALER_TESTING_iterate (is, 423 true, 424 &analyze_command, 425 &ac); 426 if (ac.failure) 427 { 428 json_dumpf (rs->hr.reply, 429 stderr, 430 JSON_INDENT (2)); 431 TALER_TESTING_interpreter_fail (ss->is); 432 return; 433 } 434 for (unsigned int i = 0; i<rs->details.ok.history_len; i++) 435 { 436 if (found[i]) 437 continue; 438 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 439 "History entry at index %u of type %d not justified by command history\n", 440 i, 441 rs->details.ok.history[i].type); 442 json_dumpf (rs->hr.reply, 443 stderr, 444 JSON_INDENT (2)); 445 TALER_TESTING_interpreter_fail (ss->is); 446 return; 447 } 448 } 449 TALER_TESTING_interpreter_next (is); 450 } 451 452 453 /** 454 * Run the command. 455 * 456 * @param cls closure. 457 * @param cmd the command being executed. 458 * @param is the interpreter state. 459 */ 460 static void 461 history_run (void *cls, 462 const struct TALER_TESTING_Command *cmd, 463 struct TALER_TESTING_Interpreter *is) 464 { 465 struct HistoryState *ss = cls; 466 const struct TALER_TESTING_Command *create_reserve; 467 468 ss->is = is; 469 create_reserve 470 = TALER_TESTING_interpreter_lookup_command (is, 471 ss->reserve_reference); 472 if (NULL == create_reserve) 473 { 474 GNUNET_break (0); 475 TALER_TESTING_interpreter_fail (is); 476 return; 477 } 478 if (GNUNET_OK != 479 TALER_TESTING_get_trait_reserve_priv (create_reserve, 480 &ss->reserve_priv)) 481 { 482 GNUNET_break (0); 483 TALER_LOG_ERROR ("Failed to find reserve_priv for history query\n"); 484 TALER_TESTING_interpreter_fail (is); 485 return; 486 } 487 GNUNET_CRYPTO_eddsa_key_get_public (&ss->reserve_priv->eddsa_priv, 488 &ss->reserve_pub.eddsa_pub); 489 ss->rsh = TALER_EXCHANGE_get_reserves_history_create ( 490 TALER_TESTING_interpreter_get_context (is), 491 TALER_TESTING_get_exchange_url (is), 492 TALER_TESTING_get_keys (is), 493 ss->reserve_priv); 494 if (NULL == ss->rsh) 495 { 496 GNUNET_break (0); 497 TALER_TESTING_interpreter_fail (is); 498 return; 499 } 500 if (TALER_EC_NONE != 501 TALER_EXCHANGE_get_reserves_history_start (ss->rsh, 502 &reserve_history_cb, 503 ss)) 504 { 505 GNUNET_break (0); 506 TALER_EXCHANGE_get_reserves_history_cancel (ss->rsh); 507 ss->rsh = NULL; 508 TALER_TESTING_interpreter_fail (is); 509 return; 510 } 511 } 512 513 514 /** 515 * Offer internal data from a "history" CMD, to other commands. 516 * 517 * @param cls closure. 518 * @param[out] ret result. 519 * @param trait name of the trait. 520 * @param index index number of the object to offer. 521 * @return #GNUNET_OK on success. 522 */ 523 static enum GNUNET_GenericReturnValue 524 history_traits (void *cls, 525 const void **ret, 526 const char *trait, 527 unsigned int index) 528 { 529 struct HistoryState *hs = cls; 530 struct TALER_TESTING_Trait traits[] = { 531 TALER_TESTING_make_trait_reserve_pub (&hs->reserve_pub), 532 TALER_TESTING_trait_end () 533 }; 534 535 return TALER_TESTING_get_trait (traits, 536 ret, 537 trait, 538 index); 539 } 540 541 542 /** 543 * Cleanup the state from a "reserve history" CMD, and possibly 544 * cancel a pending operation thereof. 545 * 546 * @param cls closure. 547 * @param cmd the command which is being cleaned up. 548 */ 549 static void 550 history_cleanup (void *cls, 551 const struct TALER_TESTING_Command *cmd) 552 { 553 struct HistoryState *ss = cls; 554 555 if (NULL != ss->rsh) 556 { 557 TALER_TESTING_command_incomplete (ss->is, 558 cmd->label); 559 TALER_EXCHANGE_get_reserves_history_cancel (ss->rsh); 560 ss->rsh = NULL; 561 } 562 GNUNET_free (ss); 563 } 564 565 566 struct TALER_TESTING_Command 567 TALER_TESTING_cmd_reserve_history (const char *label, 568 const char *reserve_reference, 569 const char *expected_balance, 570 unsigned int expected_response_code) 571 { 572 struct HistoryState *ss; 573 574 GNUNET_assert (NULL != reserve_reference); 575 ss = GNUNET_new (struct HistoryState); 576 ss->reserve_reference = reserve_reference; 577 ss->expected_balance = expected_balance; 578 ss->expected_response_code = expected_response_code; 579 { 580 struct TALER_TESTING_Command cmd = { 581 .cls = ss, 582 .label = label, 583 .run = &history_run, 584 .cleanup = &history_cleanup, 585 .traits = &history_traits 586 }; 587 588 return cmd; 589 } 590 }