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 : #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 23 : history_entry_cmp (
115 : const struct TALER_EXCHANGE_ReserveHistoryEntry *h1,
116 : const struct TALER_EXCHANGE_ReserveHistoryEntry *h2)
117 : {
118 23 : if (h1->type != h2->type)
119 0 : return 1;
120 23 : switch (h1->type)
121 : {
122 8 : case TALER_EXCHANGE_RTT_CREDIT:
123 8 : if ( (0 ==
124 8 : TALER_amount_cmp (&h1->amount,
125 8 : &h2->amount)) &&
126 : (0 ==
127 8 : TALER_full_payto_cmp (h1->details.in_details.sender_url,
128 8 : h2->details.in_details.sender_url)) &&
129 8 : (h1->details.in_details.wire_reference ==
130 8 : h2->details.in_details.wire_reference) &&
131 8 : (GNUNET_TIME_timestamp_cmp (h1->details.in_details.timestamp,
132 : ==,
133 : h2->details.in_details.timestamp)) )
134 8 : return 0;
135 0 : return 1;
136 7 : case TALER_EXCHANGE_RTT_WITHDRAWAL:
137 7 : if ( (0 ==
138 7 : TALER_amount_cmp (&h1->amount,
139 7 : &h2->amount)) &&
140 : (0 ==
141 7 : TALER_amount_cmp (&h1->details.withdraw.fee,
142 7 : &h2->details.withdraw.fee)) &&
143 7 : (h1->details.withdraw.age_restricted ==
144 7 : h2->details.withdraw.age_restricted) &&
145 7 : ((! h1->details.withdraw.age_restricted) ||
146 0 : (h1->details.withdraw.max_age == h2->details.withdraw.max_age) ))
147 7 : return 0;
148 0 : return 1;
149 0 : 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 0 : if ( (0 ==
153 0 : TALER_amount_cmp (&h1->amount,
154 0 : &h2->amount)) &&
155 : (0 ==
156 0 : GNUNET_memcmp (&h1->details.recoup_details.coin_pub,
157 : &h2->details.recoup_details.coin_pub)) )
158 0 : return 0;
159 0 : return 1;
160 0 : 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 0 : if ( (0 ==
166 0 : TALER_amount_cmp (&h1->amount,
167 0 : &h2->amount)) &&
168 : (0 ==
169 0 : TALER_amount_cmp (&h1->details.close_details.fee,
170 : &h2->details.close_details.fee)) )
171 0 : return 0;
172 0 : return 1;
173 8 : case TALER_EXCHANGE_RTT_MERGE:
174 8 : if ( (0 ==
175 8 : TALER_amount_cmp (&h1->amount,
176 8 : &h2->amount)) &&
177 : (0 ==
178 8 : TALER_amount_cmp (&h1->details.merge_details.purse_fee,
179 8 : &h2->details.merge_details.purse_fee)) &&
180 8 : (GNUNET_TIME_timestamp_cmp (h1->details.merge_details.merge_timestamp,
181 : ==,
182 : h2->details.merge_details.merge_timestamp))
183 8 : &&
184 8 : (GNUNET_TIME_timestamp_cmp (h1->details.merge_details.purse_expiration,
185 : ==,
186 : h2->details.merge_details.purse_expiration)
187 : )
188 8 : &&
189 : (0 ==
190 8 : GNUNET_memcmp (&h1->details.merge_details.merge_pub,
191 8 : &h2->details.merge_details.merge_pub)) &&
192 : (0 ==
193 8 : GNUNET_memcmp (&h1->details.merge_details.h_contract_terms,
194 8 : &h2->details.merge_details.h_contract_terms)) &&
195 : (0 ==
196 8 : GNUNET_memcmp (&h1->details.merge_details.purse_pub,
197 8 : &h2->details.merge_details.purse_pub)) &&
198 : (0 ==
199 8 : GNUNET_memcmp (&h1->details.merge_details.reserve_sig,
200 8 : &h2->details.merge_details.reserve_sig)) &&
201 8 : (h1->details.merge_details.min_age ==
202 8 : h2->details.merge_details.min_age) &&
203 8 : (h1->details.merge_details.flags ==
204 8 : h2->details.merge_details.flags) )
205 8 : return 0;
206 0 : return 1;
207 0 : case TALER_EXCHANGE_RTT_OPEN:
208 0 : if ( (0 ==
209 0 : TALER_amount_cmp (&h1->amount,
210 0 : &h2->amount)) &&
211 0 : (GNUNET_TIME_timestamp_cmp (
212 : h1->details.open_request.request_timestamp,
213 : ==,
214 0 : h2->details.open_request.request_timestamp)) &&
215 0 : (GNUNET_TIME_timestamp_cmp (
216 : h1->details.open_request.reserve_expiration,
217 : ==,
218 0 : h2->details.open_request.reserve_expiration)) &&
219 0 : (h1->details.open_request.purse_limit ==
220 0 : h2->details.open_request.purse_limit) &&
221 : (0 ==
222 0 : TALER_amount_cmp (&h1->details.open_request.reserve_payment,
223 0 : &h2->details.open_request.reserve_payment)) &&
224 : (0 ==
225 0 : GNUNET_memcmp (&h1->details.open_request.reserve_sig,
226 : &h2->details.open_request.reserve_sig)) )
227 0 : return 0;
228 0 : return 1;
229 0 : case TALER_EXCHANGE_RTT_CLOSE:
230 0 : if ( (0 ==
231 0 : TALER_amount_cmp (&h1->amount,
232 0 : &h2->amount)) &&
233 0 : (GNUNET_TIME_timestamp_cmp (
234 : h1->details.close_request.request_timestamp,
235 : ==,
236 0 : h2->details.close_request.request_timestamp)) &&
237 : (0 ==
238 0 : GNUNET_memcmp (&h1->details.close_request.target_account_h_payto,
239 0 : &h2->details.close_request.target_account_h_payto)) &&
240 : (0 ==
241 0 : GNUNET_memcmp (&h1->details.close_request.reserve_sig,
242 : &h2->details.close_request.reserve_sig)) )
243 0 : return 0;
244 0 : return 1;
245 : }
246 0 : 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 584 : analyze_command (void *cls,
261 : const struct TALER_TESTING_Command *cmd)
262 : {
263 584 : struct AnalysisContext *ac = cls;
264 584 : const struct TALER_ReservePublicKeyP *reserve_pub = ac->reserve_pub;
265 584 : const struct TALER_EXCHANGE_ReserveHistoryEntry *history = ac->history;
266 584 : unsigned int history_length = ac->history_length;
267 584 : bool *found = ac->found;
268 :
269 584 : if (TALER_TESTING_cmd_is_batch (cmd))
270 : {
271 : struct TALER_TESTING_Command *cur;
272 : struct TALER_TESTING_Command *bcmd;
273 :
274 47 : cur = TALER_TESTING_cmd_batch_get_current (cmd);
275 47 : if (GNUNET_OK !=
276 47 : TALER_TESTING_get_trait_batch_cmds (cmd,
277 : &bcmd))
278 : {
279 0 : GNUNET_break (0);
280 0 : ac->failure = true;
281 0 : return;
282 : }
283 552 : for (unsigned int i = 0; NULL != bcmd[i].label; i++)
284 : {
285 513 : struct TALER_TESTING_Command *step = &bcmd[i];
286 :
287 513 : analyze_command (ac,
288 : step);
289 513 : if (ac->failure)
290 : {
291 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
292 : "Entry for batch step `%s' missing in reserve history\n",
293 : step->label);
294 0 : return;
295 : }
296 513 : if (step == cur)
297 8 : break; /* if *we* are in a batch, make sure not to analyze commands past 'now' */
298 : }
299 47 : return;
300 : }
301 :
302 : {
303 : const struct TALER_ReservePublicKeyP *rp;
304 537 : bool matched = false;
305 :
306 537 : if (GNUNET_OK !=
307 537 : TALER_TESTING_get_trait_reserve_pub (cmd,
308 : &rp))
309 514 : return; /* command does nothing for reserves */
310 104 : if (0 !=
311 104 : GNUNET_memcmp (rp,
312 : reserve_pub))
313 66 : return; /* command affects some _other_ reserve */
314 38 : for (unsigned int j = 0; true; j++)
315 0 : {
316 : const struct TALER_EXCHANGE_ReserveHistoryEntry *he;
317 :
318 38 : if (GNUNET_OK !=
319 38 : TALER_TESTING_get_trait_reserve_history (cmd,
320 : j,
321 : &he))
322 : {
323 : /* NOTE: only for debugging... */
324 15 : 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 15 : return; /* command does nothing for reserves */
329 : }
330 47 : for (unsigned int i = 0; i<history_length; i++)
331 : {
332 47 : if (found[i])
333 24 : continue; /* already found, skip */
334 23 : if (0 ==
335 23 : history_entry_cmp (he,
336 23 : &history[i]))
337 : {
338 23 : found[i] = true;
339 23 : matched = true;
340 23 : ac->failure = false;
341 23 : break;
342 : }
343 : }
344 23 : if (matched)
345 23 : break;
346 : }
347 23 : if (! matched)
348 : {
349 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
350 : "Command `%s' no relevant reserve history entry not found\n",
351 : cmd->label);
352 0 : 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 8 : reserve_history_cb (void *cls,
368 : const struct TALER_EXCHANGE_GetReservesHistoryResponse *rs)
369 : {
370 8 : struct HistoryState *ss = cls;
371 8 : struct TALER_TESTING_Interpreter *is = ss->is;
372 : struct TALER_Amount eb;
373 :
374 8 : ss->rsh = NULL;
375 8 : if (ss->expected_response_code != rs->hr.http_status)
376 : {
377 0 : 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 0 : json_dumpf (rs->hr.reply,
383 : stderr,
384 : 0);
385 0 : TALER_TESTING_interpreter_fail (ss->is);
386 0 : return;
387 : }
388 8 : if (MHD_HTTP_OK != rs->hr.http_status)
389 : {
390 0 : TALER_TESTING_interpreter_next (is);
391 0 : return;
392 : }
393 8 : GNUNET_assert (GNUNET_OK ==
394 : TALER_string_to_amount (ss->expected_balance,
395 : &eb));
396 :
397 8 : if (0 != TALER_amount_cmp (&eb,
398 : &rs->details.ok.balance))
399 : {
400 0 : GNUNET_break (0);
401 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
402 : "Unexpected amount in reserve: %s\n",
403 : TALER_amount_to_string (&rs->details.ok.balance));
404 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
405 : "Expected balance of: %s\n",
406 : TALER_amount_to_string (&eb));
407 0 : TALER_TESTING_interpreter_fail (ss->is);
408 0 : return;
409 : }
410 8 : {
411 8 : bool found[rs->details.ok.history_len];
412 8 : struct AnalysisContext ac = {
413 8 : .reserve_pub = &ss->reserve_pub,
414 8 : .history = rs->details.ok.history,
415 8 : .history_length = rs->details.ok.history_len,
416 : .found = found
417 : };
418 :
419 8 : memset (found,
420 : 0,
421 : sizeof (found));
422 8 : TALER_TESTING_iterate (is,
423 : true,
424 : &analyze_command,
425 : &ac);
426 8 : if (ac.failure)
427 : {
428 0 : json_dumpf (rs->hr.reply,
429 : stderr,
430 : JSON_INDENT (2));
431 0 : TALER_TESTING_interpreter_fail (ss->is);
432 0 : return;
433 : }
434 31 : for (unsigned int i = 0; i<rs->details.ok.history_len; i++)
435 : {
436 23 : if (found[i])
437 23 : continue;
438 0 : 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 0 : json_dumpf (rs->hr.reply,
443 : stderr,
444 : JSON_INDENT (2));
445 0 : TALER_TESTING_interpreter_fail (ss->is);
446 0 : return;
447 : }
448 : }
449 8 : 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 8 : history_run (void *cls,
462 : const struct TALER_TESTING_Command *cmd,
463 : struct TALER_TESTING_Interpreter *is)
464 : {
465 8 : struct HistoryState *ss = cls;
466 : const struct TALER_TESTING_Command *create_reserve;
467 :
468 8 : ss->is = is;
469 : create_reserve
470 8 : = TALER_TESTING_interpreter_lookup_command (is,
471 : ss->reserve_reference);
472 8 : if (NULL == create_reserve)
473 : {
474 0 : GNUNET_break (0);
475 0 : TALER_TESTING_interpreter_fail (is);
476 0 : return;
477 : }
478 8 : if (GNUNET_OK !=
479 8 : TALER_TESTING_get_trait_reserve_priv (create_reserve,
480 : &ss->reserve_priv))
481 : {
482 0 : GNUNET_break (0);
483 0 : TALER_LOG_ERROR ("Failed to find reserve_priv for history query\n");
484 0 : TALER_TESTING_interpreter_fail (is);
485 0 : return;
486 : }
487 8 : GNUNET_CRYPTO_eddsa_key_get_public (&ss->reserve_priv->eddsa_priv,
488 : &ss->reserve_pub.eddsa_pub);
489 8 : 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 8 : if (NULL == ss->rsh)
495 : {
496 0 : GNUNET_break (0);
497 0 : TALER_TESTING_interpreter_fail (is);
498 0 : return;
499 : }
500 8 : if (TALER_EC_NONE !=
501 8 : TALER_EXCHANGE_get_reserves_history_start (ss->rsh,
502 : &reserve_history_cb,
503 : ss))
504 : {
505 0 : GNUNET_break (0);
506 0 : TALER_EXCHANGE_get_reserves_history_cancel (ss->rsh);
507 0 : ss->rsh = NULL;
508 0 : TALER_TESTING_interpreter_fail (is);
509 0 : 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 24 : history_traits (void *cls,
525 : const void **ret,
526 : const char *trait,
527 : unsigned int index)
528 : {
529 24 : struct HistoryState *hs = cls;
530 : struct TALER_TESTING_Trait traits[] = {
531 24 : TALER_TESTING_make_trait_reserve_pub (&hs->reserve_pub),
532 24 : TALER_TESTING_trait_end ()
533 : };
534 :
535 24 : 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 8 : history_cleanup (void *cls,
551 : const struct TALER_TESTING_Command *cmd)
552 : {
553 8 : struct HistoryState *ss = cls;
554 :
555 8 : if (NULL != ss->rsh)
556 : {
557 0 : TALER_TESTING_command_incomplete (ss->is,
558 : cmd->label);
559 0 : TALER_EXCHANGE_get_reserves_history_cancel (ss->rsh);
560 0 : ss->rsh = NULL;
561 : }
562 8 : GNUNET_free (ss);
563 8 : }
564 :
565 :
566 : struct TALER_TESTING_Command
567 8 : 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 8 : GNUNET_assert (NULL != reserve_reference);
575 8 : ss = GNUNET_new (struct HistoryState);
576 8 : ss->reserve_reference = reserve_reference;
577 8 : ss->expected_balance = expected_balance;
578 8 : ss->expected_response_code = expected_response_code;
579 : {
580 8 : 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 8 : return cmd;
589 : }
590 : }
|