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