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 (17090B)


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