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