testing_api_cmd_post_products.c (18696B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2020 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 src/testing/testing_api_cmd_post_products.c 21 * @brief command to test POST /products 22 * @author Christian Grothoff 23 */ 24 #include "platform.h" 25 struct PostProductsState; 26 #define TALER_MERCHANT_POST_PRIVATE_PRODUCTS_RESULT_CLOSURE struct PostProductsState 27 #include <taler/taler_exchange_service.h> 28 #include <taler/taler_testing_lib.h> 29 #include "taler/taler_merchant_testing_lib.h" 30 #include <taler/merchant/post-private-products.h> 31 32 33 /** 34 * State of a "POST /products" CMD. 35 */ 36 struct PostProductsState 37 { 38 39 /** 40 * Handle for a "POST /products" request. 41 */ 42 struct TALER_MERCHANT_PostPrivateProductsHandle *iph; 43 44 /** 45 * The interpreter state. 46 */ 47 struct TALER_TESTING_Interpreter *is; 48 49 /** 50 * Base URL of the merchant serving the request. 51 */ 52 const char *merchant_url; 53 54 /** 55 * ID of the product to run POST for. 56 */ 57 const char *product_id; 58 59 /** 60 * description of the product 61 */ 62 const char *description; 63 64 /** 65 * Map from IETF BCP 47 language tags to localized descriptions 66 */ 67 json_t *description_i18n; 68 69 /** 70 * unit in which the product is measured (liters, kilograms, packages, etc.) 71 */ 72 const char *unit; 73 74 /** 75 * the price for one @a unit of the product 76 */ 77 struct TALER_Amount price; 78 79 /** 80 * Array of unit prices (may point to @e price for single-entry usage). 81 */ 82 struct TALER_Amount *unit_prices; 83 84 /** 85 * Number of entries in @e unit_prices. 86 */ 87 size_t unit_prices_len; 88 89 /** 90 * True if @e unit_prices must be freed. 91 */ 92 bool owns_unit_prices; 93 94 /** 95 * True if we should send an explicit unit_price array. 96 */ 97 bool use_unit_price_array; 98 99 /** 100 * base64-encoded product image 101 */ 102 char *image; 103 104 /** 105 * list of taxes paid by the merchant 106 */ 107 json_t *taxes; 108 109 /** 110 * in @e units, -1 to indicate "infinite" (i.e. electronic books) 111 */ 112 int64_t total_stock; 113 114 /** 115 * Fractional stock component when fractional quantities are enabled. 116 */ 117 uint32_t total_stock_frac; 118 119 /** 120 * Fractional precision level associated with fractional quantities. 121 */ 122 uint32_t unit_precision_level; 123 124 /** 125 * whether fractional quantities are allowed for this product. 126 */ 127 bool unit_allow_fraction; 128 129 /** 130 * Cached string representation of the stock level. 131 */ 132 char unit_total_stock[64]; 133 134 /** 135 * set to true if we should use the extended fractional API. 136 */ 137 bool use_fractional; 138 139 /** 140 * where the product is in stock 141 */ 142 json_t *address; 143 144 /** 145 * Minimum age requirement to use for the product. 146 */ 147 unsigned int minimum_age; 148 149 /** 150 * when the next restocking is expected to happen, 0 for unknown, 151 */ 152 struct GNUNET_TIME_Timestamp next_restock; 153 154 /** 155 * Categories array length. 156 */ 157 unsigned int num_cats; 158 159 /** 160 * Categories array. 161 */ 162 uint64_t *cats; 163 164 /** 165 * Expected HTTP response code. 166 */ 167 unsigned int http_status; 168 169 }; 170 171 static void 172 post_products_update_unit_total_stock (struct PostProductsState *pps) 173 { 174 if (-1 == pps->total_stock) 175 { 176 pps->total_stock = INT64_MAX; 177 pps->total_stock_frac = INT32_MAX; 178 } 179 else if (! pps->unit_allow_fraction) 180 { 181 pps->total_stock_frac = 0; 182 } 183 TALER_MERCHANT_format_stock_string ((uint64_t) pps->total_stock, 184 pps->total_stock_frac, 185 pps->unit_total_stock, 186 sizeof (pps->unit_total_stock)); 187 } 188 189 190 static uint32_t 191 default_precision_from_unit (const char *unit) 192 { 193 struct PrecisionRule 194 { 195 const char *unit; 196 uint32_t precision; 197 }; 198 static const struct PrecisionRule rules[] = { 199 { "WeightUnitMg", 0 }, 200 { "SizeUnitMm", 0 }, 201 { "WeightUnitG", 1 }, 202 { "SizeUnitCm", 1 }, 203 { "SurfaceUnitMm2", 1 }, 204 { "VolumeUnitMm3", 1 }, 205 { "WeightUnitOunce", 2 }, 206 { "SizeUnitInch", 2 }, 207 { "SurfaceUnitCm2", 2 }, 208 { "VolumeUnitOunce", 2 }, 209 { "VolumeUnitInch3", 2 }, 210 { "TimeUnitHour", 2 }, 211 { "TimeUnitMonth", 2 }, 212 { "WeightUnitTon", 3 }, 213 { "WeightUnitKg", 3 }, 214 { "WeightUnitPound", 3 }, 215 { "SizeUnitM", 3 }, 216 { "SizeUnitDm", 3 }, 217 { "SizeUnitFoot", 3 }, 218 { "SurfaceUnitDm2", 3 }, 219 { "SurfaceUnitFoot2", 3 }, 220 { "VolumeUnitCm3", 3 }, 221 { "VolumeUnitLitre", 3 }, 222 { "VolumeUnitGallon", 3 }, 223 { "TimeUnitSecond", 3 }, 224 { "TimeUnitMinute", 3 }, 225 { "TimeUnitDay", 3 }, 226 { "TimeUnitWeek", 3 }, 227 { "SurfaceUnitM2", 4 }, 228 { "SurfaceUnitInch2", 4 }, 229 { "TimeUnitYear", 4 }, 230 { "VolumeUnitDm3", 5 }, 231 { "VolumeUnitFoot3", 5 }, 232 { "VolumeUnitM3", 6 } 233 }; 234 235 const size_t rules_len = sizeof (rules) / sizeof (rules[0]); 236 if (NULL == unit) 237 return 0; 238 239 for (size_t i = 0; i<rules_len; i++) 240 if (0 == strcmp (unit, 241 rules[i].unit)) 242 return rules[i].precision; 243 return 0; 244 } 245 246 247 /** 248 * Callback for a POST /products operation. 249 * 250 * @param cls closure for this function 251 * @param ppr response being processed 252 */ 253 static void 254 post_products_cb (struct PostProductsState *pis, 255 const struct TALER_MERCHANT_PostPrivateProductsResponse *ppr) 256 { 257 258 pis->iph = NULL; 259 if (pis->http_status != ppr->hr.http_status) 260 { 261 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 262 "Unexpected response code %u (%d) to command %s\n", 263 ppr->hr.http_status, 264 (int) ppr->hr.ec, 265 TALER_TESTING_interpreter_get_current_label (pis->is)); 266 TALER_TESTING_interpreter_fail (pis->is); 267 return; 268 } 269 switch (ppr->hr.http_status) 270 { 271 case MHD_HTTP_NO_CONTENT: 272 break; 273 case MHD_HTTP_BAD_REQUEST: 274 break; 275 case MHD_HTTP_UNAUTHORIZED: 276 break; 277 case MHD_HTTP_FORBIDDEN: 278 break; 279 case MHD_HTTP_NOT_FOUND: 280 break; 281 case MHD_HTTP_CONFLICT: 282 break; 283 default: 284 GNUNET_break (0); 285 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 286 "Unhandled HTTP status %u for POST /products.\n", 287 ppr->hr.http_status); 288 } 289 TALER_TESTING_interpreter_next (pis->is); 290 } 291 292 293 /** 294 * Run the "POST /products" CMD. 295 * 296 * 297 * @param cls closure. 298 * @param cmd command being run now. 299 * @param is interpreter state. 300 */ 301 static void 302 post_products_run (void *cls, 303 const struct TALER_TESTING_Command *cmd, 304 struct TALER_TESTING_Interpreter *is) 305 { 306 struct PostProductsState *pis = cls; 307 308 pis->is = is; 309 pis->iph = TALER_MERCHANT_post_private_products_create ( 310 TALER_TESTING_interpreter_get_context (is), 311 pis->merchant_url, 312 pis->product_id, 313 pis->description, 314 pis->unit, 315 pis->unit_prices_len, 316 pis->unit_prices); 317 GNUNET_assert (GNUNET_OK == 318 TALER_MERCHANT_post_private_products_set_options ( 319 pis->iph, 320 TALER_MERCHANT_post_private_products_option_description_i18n ( 321 pis->description_i18n), 322 TALER_MERCHANT_post_private_products_option_total_stock_val ( 323 pis->total_stock), 324 TALER_MERCHANT_post_private_products_option_taxes ( 325 pis->taxes), 326 TALER_MERCHANT_post_private_products_option_image ( 327 pis->image), 328 TALER_MERCHANT_post_private_products_option_address ( 329 pis->address), 330 TALER_MERCHANT_post_private_products_option_next_restock ( 331 pis->next_restock), 332 TALER_MERCHANT_post_private_products_option_minimum_age ( 333 pis->minimum_age), 334 TALER_MERCHANT_post_private_products_option_categories ( 335 pis->num_cats, 336 pis->cats))); 337 if (pis->use_fractional) 338 { 339 GNUNET_assert ( 340 GNUNET_OK == 341 TALER_MERCHANT_post_private_products_set_options ( 342 pis->iph, 343 TALER_MERCHANT_post_private_products_option_total_stock_frac ( 344 pis->total_stock_frac), 345 TALER_MERCHANT_post_private_products_option_unit_allow_fraction ( 346 pis->unit_allow_fraction), 347 TALER_MERCHANT_post_private_products_option_unit_precision_level ( 348 pis->unit_precision_level))); 349 } 350 { 351 enum TALER_ErrorCode ec; 352 353 ec = TALER_MERCHANT_post_private_products_start ( 354 pis->iph, 355 &post_products_cb, 356 pis); 357 GNUNET_assert (TALER_EC_NONE == ec); 358 } 359 } 360 361 362 /** 363 * Offers information from the POST /products CMD state to other 364 * commands. 365 * 366 * @param cls closure 367 * @param[out] ret result (could be anything) 368 * @param trait name of the trait 369 * @param index index number of the object to extract. 370 * @return #GNUNET_OK on success 371 */ 372 static enum GNUNET_GenericReturnValue 373 post_products_traits (void *cls, 374 const void **ret, 375 const char *trait, 376 unsigned int index) 377 { 378 struct PostProductsState *pps = cls; 379 struct TALER_TESTING_Trait traits[] = { 380 TALER_TESTING_make_trait_product_description (pps->description), 381 TALER_TESTING_make_trait_i18n_description (pps->description_i18n), 382 TALER_TESTING_make_trait_product_unit (pps->unit), 383 TALER_TESTING_make_trait_amount (&pps->price), 384 TALER_TESTING_make_trait_product_image (pps->image), 385 TALER_TESTING_make_trait_taxes (pps->taxes), 386 TALER_TESTING_make_trait_product_stock (&pps->total_stock), 387 TALER_TESTING_make_trait_product_unit_total_stock ( 388 pps->unit_total_stock), 389 TALER_TESTING_make_trait_product_unit_precision_level ( 390 &pps->unit_precision_level), 391 TALER_TESTING_make_trait_product_unit_allow_fraction ( 392 &pps->unit_allow_fraction), 393 TALER_TESTING_make_trait_address (pps->address), 394 TALER_TESTING_make_trait_timestamp (0, 395 &pps->next_restock), 396 TALER_TESTING_make_trait_product_id (pps->product_id), 397 TALER_TESTING_trait_end (), 398 }; 399 400 return TALER_TESTING_get_trait (traits, 401 ret, 402 trait, 403 index); 404 } 405 406 407 /** 408 * Free the state of a "POST product" CMD, and possibly 409 * cancel a pending operation thereof. 410 * 411 * @param cls closure. 412 * @param cmd command being run. 413 */ 414 static void 415 post_products_cleanup (void *cls, 416 const struct TALER_TESTING_Command *cmd) 417 { 418 struct PostProductsState *pis = cls; 419 420 if (NULL != pis->iph) 421 { 422 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 423 "POST /products operation did not complete\n"); 424 TALER_MERCHANT_post_private_products_cancel (pis->iph); 425 } 426 if (pis->owns_unit_prices) 427 GNUNET_free (pis->unit_prices); 428 json_decref (pis->description_i18n); 429 GNUNET_free (pis->image); 430 json_decref (pis->taxes); 431 json_decref (pis->address); 432 GNUNET_free (pis->cats); 433 GNUNET_free (pis); 434 } 435 436 437 struct TALER_TESTING_Command 438 TALER_TESTING_cmd_merchant_post_products2 ( 439 const char *label, 440 const char *merchant_url, 441 const char *product_id, 442 const char *description, 443 json_t *description_i18n, 444 const char *unit, 445 const char *price, 446 const char *image, 447 json_t *taxes, 448 int64_t total_stock, 449 uint32_t minimum_age, 450 json_t *address, 451 struct GNUNET_TIME_Timestamp next_restock, 452 unsigned int http_status) 453 { 454 struct PostProductsState *pis; 455 456 GNUNET_assert ((NULL == taxes) || 457 json_is_array (taxes)); 458 GNUNET_assert ((NULL == description_i18n) || 459 json_is_object (description_i18n)); 460 pis = GNUNET_new (struct PostProductsState); 461 pis->merchant_url = merchant_url; 462 pis->product_id = product_id; 463 pis->http_status = http_status; 464 pis->description = description; 465 pis->description_i18n = description_i18n; /* ownership taken */ 466 pis->unit = unit; 467 pis->unit_precision_level = default_precision_from_unit (unit); 468 GNUNET_assert (GNUNET_OK == 469 TALER_string_to_amount (price, 470 &pis->price)); 471 pis->unit_prices = &pis->price; 472 pis->unit_prices_len = 1; 473 pis->owns_unit_prices = false; 474 pis->use_unit_price_array = false; 475 pis->image = GNUNET_strdup (image); 476 pis->taxes = taxes; /* ownership taken */ 477 pis->total_stock = total_stock; 478 pis->total_stock_frac = 0; 479 pis->unit_allow_fraction = false; 480 pis->use_fractional = false; 481 pis->minimum_age = minimum_age; 482 pis->address = address; /* ownership taken */ 483 pis->next_restock = next_restock; 484 pis->num_cats = 0; 485 pis->cats = NULL; 486 post_products_update_unit_total_stock (pis); 487 { 488 struct TALER_TESTING_Command cmd = { 489 .cls = pis, 490 .label = label, 491 .run = &post_products_run, 492 .cleanup = &post_products_cleanup, 493 .traits = &post_products_traits 494 }; 495 496 return cmd; 497 } 498 } 499 500 501 struct TALER_TESTING_Command 502 TALER_TESTING_cmd_merchant_post_products3 ( 503 const char *label, 504 const char *merchant_url, 505 const char *product_id, 506 const char *description, 507 json_t *description_i18n, 508 const char *unit, 509 const char *price, 510 const char *image, 511 json_t *taxes, 512 int64_t total_stock, 513 uint32_t total_stock_frac, 514 bool unit_allow_fraction, 515 uint32_t minimum_age, 516 json_t *address, 517 struct GNUNET_TIME_Timestamp next_restock, 518 unsigned int http_status) 519 { 520 struct TALER_TESTING_Command cmd; 521 522 cmd = TALER_TESTING_cmd_merchant_post_products2 (label, 523 merchant_url, 524 product_id, 525 description, 526 description_i18n, 527 unit, 528 price, 529 image, 530 taxes, 531 total_stock, 532 minimum_age, 533 address, 534 next_restock, 535 http_status); 536 { 537 struct PostProductsState *pis = cmd.cls; 538 539 pis->total_stock_frac = total_stock_frac; 540 pis->unit_allow_fraction = unit_allow_fraction; 541 pis->use_fractional = true; 542 post_products_update_unit_total_stock (pis); 543 } 544 return cmd; 545 } 546 547 548 struct TALER_TESTING_Command 549 TALER_TESTING_cmd_merchant_post_products_with_unit_prices ( 550 const char *label, 551 const char *merchant_url, 552 const char *product_id, 553 const char *description, 554 const char *unit, 555 const char *const *unit_prices, 556 size_t unit_prices_len, 557 unsigned int http_status) 558 { 559 struct PostProductsState *pis; 560 561 GNUNET_assert (0 < unit_prices_len); 562 GNUNET_assert (NULL != unit_prices); 563 pis = GNUNET_new (struct PostProductsState); 564 pis->merchant_url = merchant_url; 565 pis->product_id = product_id; 566 pis->http_status = http_status; 567 pis->description = description; 568 pis->description_i18n = json_pack ("{s:s}", "en", description); 569 pis->unit = unit; 570 pis->unit_precision_level = default_precision_from_unit (unit); 571 pis->unit_prices = GNUNET_new_array (unit_prices_len, 572 struct TALER_Amount); 573 pis->unit_prices_len = unit_prices_len; 574 pis->owns_unit_prices = true; 575 pis->use_unit_price_array = true; 576 for (size_t i = 0; i < unit_prices_len; i++) 577 GNUNET_assert (GNUNET_OK == 578 TALER_string_to_amount (unit_prices[i], 579 &pis->unit_prices[i])); 580 pis->price = pis->unit_prices[0]; 581 pis->image = GNUNET_strdup (""); 582 pis->taxes = json_array (); 583 pis->total_stock = 4; 584 pis->total_stock_frac = 0; 585 pis->unit_allow_fraction = false; 586 pis->use_fractional = false; 587 pis->minimum_age = 0; 588 pis->address = json_pack ("{s:s}", "street", "my street"); 589 pis->next_restock = GNUNET_TIME_UNIT_ZERO_TS; 590 pis->num_cats = 0; 591 pis->cats = NULL; 592 post_products_update_unit_total_stock (pis); 593 { 594 struct TALER_TESTING_Command cmd = { 595 .cls = pis, 596 .label = label, 597 .run = &post_products_run, 598 .cleanup = &post_products_cleanup, 599 .traits = &post_products_traits 600 }; 601 602 return cmd; 603 } 604 } 605 606 607 struct TALER_TESTING_Command 608 TALER_TESTING_cmd_merchant_post_products ( 609 const char *label, 610 const char *merchant_url, 611 const char *product_id, 612 const char *description, 613 const char *price, 614 unsigned int http_status) 615 { 616 return TALER_TESTING_cmd_merchant_post_products2 ( 617 label, 618 merchant_url, 619 product_id, 620 description, 621 json_pack ("{s:s}", "en", description), 622 "test-unit", 623 price, 624 "", 625 json_array (), 626 4, /* total stock */ 627 0, /* minimum age */ 628 json_pack ("{s:s}", "street", "my street"), 629 GNUNET_TIME_UNIT_ZERO_TS, 630 http_status); 631 } 632 633 634 struct TALER_TESTING_Command 635 TALER_TESTING_cmd_merchant_post_products_with_categories ( 636 const char *label, 637 const char *merchant_url, 638 const char *product_id, 639 const char *description, 640 const char *unit, 641 const char *price, 642 unsigned int num_cats, 643 const uint64_t *cats, 644 bool unit_allow_fraction, 645 uint32_t unit_precision_level, 646 unsigned int http_status) 647 { 648 struct TALER_TESTING_Command cmd; 649 650 cmd = TALER_TESTING_cmd_merchant_post_products2 ( 651 label, 652 merchant_url, 653 product_id, 654 description, 655 json_pack ("{s:s}", "en", description), 656 unit, 657 price, 658 "", 659 json_array (), 660 4, /* total stock */ 661 0, /* minimum age */ 662 json_pack ("{s:s}", "street", "my street"), 663 GNUNET_TIME_UNIT_ZERO_TS, 664 http_status); 665 { 666 struct PostProductsState *pis = cmd.cls; 667 668 pis->num_cats = num_cats; 669 if (0 < num_cats) 670 { 671 pis->cats = GNUNET_new_array (num_cats, 672 uint64_t); 673 for (unsigned int i = 0; i < num_cats; i++) 674 pis->cats[i] = cats[i]; 675 } 676 pis->unit_allow_fraction = unit_allow_fraction; 677 pis->unit_precision_level = unit_precision_level; 678 if (unit_allow_fraction) 679 pis->use_fractional = true; 680 } 681 return cmd; 682 } 683 684 685 /* end of testing_api_cmd_post_products.c */