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