Line data Source code
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 42 : post_products_update_unit_total_stock (struct PostProductsState *pps)
173 : {
174 42 : if (-1 == pps->total_stock)
175 : {
176 2 : pps->total_stock = INT64_MAX;
177 2 : pps->total_stock_frac = INT32_MAX;
178 : }
179 40 : else if (! pps->unit_allow_fraction)
180 : {
181 38 : pps->total_stock_frac = 0;
182 : }
183 42 : TALER_MERCHANT_format_stock_string ((uint64_t) pps->total_stock,
184 : pps->total_stock_frac,
185 42 : pps->unit_total_stock,
186 : sizeof (pps->unit_total_stock));
187 42 : }
188 :
189 :
190 : static uint32_t
191 40 : 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 40 : const size_t rules_len = sizeof (rules) / sizeof (rules[0]);
236 40 : if (NULL == unit)
237 0 : return 0;
238 :
239 1400 : for (size_t i = 0; i<rules_len; i++)
240 1360 : if (0 == strcmp (unit,
241 1360 : rules[i].unit))
242 0 : return rules[i].precision;
243 40 : 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 40 : post_products_cb (struct PostProductsState *pis,
255 : const struct TALER_MERCHANT_PostPrivateProductsResponse *ppr)
256 : {
257 :
258 40 : pis->iph = NULL;
259 40 : if (pis->http_status != ppr->hr.http_status)
260 : {
261 0 : 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 0 : TALER_TESTING_interpreter_fail (pis->is);
267 0 : return;
268 : }
269 40 : switch (ppr->hr.http_status)
270 : {
271 32 : case MHD_HTTP_NO_CONTENT:
272 32 : break;
273 4 : case MHD_HTTP_BAD_REQUEST:
274 4 : break;
275 0 : case MHD_HTTP_UNAUTHORIZED:
276 0 : break;
277 0 : case MHD_HTTP_FORBIDDEN:
278 0 : break;
279 2 : case MHD_HTTP_NOT_FOUND:
280 2 : break;
281 2 : case MHD_HTTP_CONFLICT:
282 2 : break;
283 0 : default:
284 0 : GNUNET_break (0);
285 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
286 : "Unhandled HTTP status %u for POST /products.\n",
287 : ppr->hr.http_status);
288 : }
289 40 : 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 40 : post_products_run (void *cls,
303 : const struct TALER_TESTING_Command *cmd,
304 : struct TALER_TESTING_Interpreter *is)
305 : {
306 40 : struct PostProductsState *pis = cls;
307 :
308 40 : pis->is = is;
309 40 : 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 40 : pis->unit_prices_len,
316 40 : pis->unit_prices);
317 40 : 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 40 : if (pis->use_fractional)
338 : {
339 4 : 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 40 : ec = TALER_MERCHANT_post_private_products_start (
354 : pis->iph,
355 : &post_products_cb,
356 : pis);
357 40 : GNUNET_assert (TALER_EC_NONE == ec);
358 : }
359 40 : }
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 144 : post_products_traits (void *cls,
374 : const void **ret,
375 : const char *trait,
376 : unsigned int index)
377 : {
378 144 : struct PostProductsState *pps = cls;
379 : struct TALER_TESTING_Trait traits[] = {
380 144 : TALER_TESTING_make_trait_product_description (pps->description),
381 144 : TALER_TESTING_make_trait_i18n_description (pps->description_i18n),
382 144 : TALER_TESTING_make_trait_product_unit (pps->unit),
383 144 : TALER_TESTING_make_trait_amount (&pps->price),
384 144 : TALER_TESTING_make_trait_product_image (pps->image),
385 144 : TALER_TESTING_make_trait_taxes (pps->taxes),
386 144 : TALER_TESTING_make_trait_product_stock (&pps->total_stock),
387 144 : TALER_TESTING_make_trait_product_unit_total_stock (
388 144 : pps->unit_total_stock),
389 144 : TALER_TESTING_make_trait_product_unit_precision_level (
390 144 : &pps->unit_precision_level),
391 144 : TALER_TESTING_make_trait_product_unit_allow_fraction (
392 144 : &pps->unit_allow_fraction),
393 144 : TALER_TESTING_make_trait_address (pps->address),
394 144 : TALER_TESTING_make_trait_timestamp (0,
395 144 : &pps->next_restock),
396 144 : TALER_TESTING_make_trait_product_id (pps->product_id),
397 144 : TALER_TESTING_trait_end (),
398 : };
399 :
400 144 : 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 40 : post_products_cleanup (void *cls,
416 : const struct TALER_TESTING_Command *cmd)
417 : {
418 40 : struct PostProductsState *pis = cls;
419 :
420 40 : if (NULL != pis->iph)
421 : {
422 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
423 : "POST /products operation did not complete\n");
424 0 : TALER_MERCHANT_post_private_products_cancel (pis->iph);
425 : }
426 40 : if (pis->owns_unit_prices)
427 6 : GNUNET_free (pis->unit_prices);
428 40 : json_decref (pis->description_i18n);
429 40 : GNUNET_free (pis->image);
430 40 : json_decref (pis->taxes);
431 40 : json_decref (pis->address);
432 40 : GNUNET_free (pis->cats);
433 40 : GNUNET_free (pis);
434 40 : }
435 :
436 :
437 : struct TALER_TESTING_Command
438 34 : 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 34 : GNUNET_assert ((NULL == taxes) ||
457 : json_is_array (taxes));
458 34 : GNUNET_assert ((NULL == description_i18n) ||
459 : json_is_object (description_i18n));
460 34 : pis = GNUNET_new (struct PostProductsState);
461 34 : pis->merchant_url = merchant_url;
462 34 : pis->product_id = product_id;
463 34 : pis->http_status = http_status;
464 34 : pis->description = description;
465 34 : pis->description_i18n = description_i18n; /* ownership taken */
466 34 : pis->unit = unit;
467 34 : pis->unit_precision_level = default_precision_from_unit (unit);
468 34 : GNUNET_assert (GNUNET_OK ==
469 : TALER_string_to_amount (price,
470 : &pis->price));
471 34 : pis->unit_prices = &pis->price;
472 34 : pis->unit_prices_len = 1;
473 34 : pis->owns_unit_prices = false;
474 34 : pis->use_unit_price_array = false;
475 34 : pis->image = GNUNET_strdup (image);
476 34 : pis->taxes = taxes; /* ownership taken */
477 34 : pis->total_stock = total_stock;
478 34 : pis->total_stock_frac = 0;
479 34 : pis->unit_allow_fraction = false;
480 34 : pis->use_fractional = false;
481 34 : pis->minimum_age = minimum_age;
482 34 : pis->address = address; /* ownership taken */
483 34 : pis->next_restock = next_restock;
484 34 : pis->num_cats = 0;
485 34 : pis->cats = NULL;
486 34 : post_products_update_unit_total_stock (pis);
487 : {
488 34 : 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 34 : return cmd;
497 : }
498 : }
499 :
500 :
501 : struct TALER_TESTING_Command
502 2 : 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 2 : 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 2 : struct PostProductsState *pis = cmd.cls;
538 :
539 2 : pis->total_stock_frac = total_stock_frac;
540 2 : pis->unit_allow_fraction = unit_allow_fraction;
541 2 : pis->use_fractional = true;
542 2 : post_products_update_unit_total_stock (pis);
543 : }
544 2 : return cmd;
545 : }
546 :
547 :
548 : struct TALER_TESTING_Command
549 6 : 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 6 : GNUNET_assert (0 < unit_prices_len);
562 6 : GNUNET_assert (NULL != unit_prices);
563 6 : pis = GNUNET_new (struct PostProductsState);
564 6 : pis->merchant_url = merchant_url;
565 6 : pis->product_id = product_id;
566 6 : pis->http_status = http_status;
567 6 : pis->description = description;
568 6 : pis->description_i18n = json_pack ("{s:s}", "en", description);
569 6 : pis->unit = unit;
570 6 : pis->unit_precision_level = default_precision_from_unit (unit);
571 6 : pis->unit_prices = GNUNET_new_array (unit_prices_len,
572 : struct TALER_Amount);
573 6 : pis->unit_prices_len = unit_prices_len;
574 6 : pis->owns_unit_prices = true;
575 6 : pis->use_unit_price_array = true;
576 18 : for (size_t i = 0; i < unit_prices_len; i++)
577 12 : GNUNET_assert (GNUNET_OK ==
578 : TALER_string_to_amount (unit_prices[i],
579 : &pis->unit_prices[i]));
580 6 : pis->price = pis->unit_prices[0];
581 6 : pis->image = GNUNET_strdup ("");
582 6 : pis->taxes = json_array ();
583 6 : pis->total_stock = 4;
584 6 : pis->total_stock_frac = 0;
585 6 : pis->unit_allow_fraction = false;
586 6 : pis->use_fractional = false;
587 6 : pis->minimum_age = 0;
588 6 : pis->address = json_pack ("{s:s}", "street", "my street");
589 6 : pis->next_restock = GNUNET_TIME_UNIT_ZERO_TS;
590 6 : pis->num_cats = 0;
591 6 : pis->cats = NULL;
592 6 : post_products_update_unit_total_stock (pis);
593 : {
594 6 : 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 6 : return cmd;
603 : }
604 : }
605 :
606 :
607 : struct TALER_TESTING_Command
608 12 : 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 12 : 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 12 : GNUNET_TIME_UNIT_ZERO_TS,
630 : http_status);
631 : }
632 :
633 :
634 : struct TALER_TESTING_Command
635 10 : 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 10 : 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 10 : GNUNET_TIME_UNIT_ZERO_TS,
664 : http_status);
665 : {
666 10 : struct PostProductsState *pis = cmd.cls;
667 :
668 10 : pis->num_cats = num_cats;
669 10 : if (0 < num_cats)
670 : {
671 10 : pis->cats = GNUNET_new_array (num_cats,
672 : uint64_t);
673 20 : for (unsigned int i = 0; i < num_cats; i++)
674 10 : pis->cats[i] = cats[i];
675 : }
676 10 : pis->unit_allow_fraction = unit_allow_fraction;
677 10 : pis->unit_precision_level = unit_precision_level;
678 10 : if (unit_allow_fraction)
679 2 : pis->use_fractional = true;
680 : }
681 10 : return cmd;
682 : }
683 :
684 :
685 : /* end of testing_api_cmd_post_products.c */
|