exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

testing_api_cmd_reserve_history.c (17524B)


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