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_bank_history_credit.c (20514B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2018-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_bank_history_credit.c
     21  * @brief command to check the /history/incoming API from the bank.
     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_exchange_service.h"
     28 #include "taler/taler_testing_lib.h"
     29 #include "taler/taler_fakebank_lib.h"
     30 #include "taler/taler_bank_service.h"
     31 #include "taler/taler_fakebank_lib.h"
     32 
     33 
     34 /**
     35  * Item in the transaction history, as reconstructed from the
     36  * command history.
     37  */
     38 struct History
     39 {
     40 
     41   /**
     42    * Wire details.
     43    */
     44   struct TALER_BANK_CreditDetails credit_details;
     45 
     46   /**
     47    * Serial ID of the wire transfer.
     48    */
     49   uint64_t row_id;
     50 
     51   /**
     52    * URL to free.
     53    */
     54   char *url;
     55 };
     56 
     57 
     58 /**
     59  * State for a "history" CMD.
     60  */
     61 struct HistoryState
     62 {
     63   /**
     64    * Base URL of the account offering the "history" operation.
     65    */
     66   char *account_url;
     67 
     68   /**
     69    * Reference to command defining the
     70    * first row number we want in the result.
     71    */
     72   const char *start_row_reference;
     73 
     74   /**
     75    * How many rows we want in the result, _at most_,
     76    * and ascending/descending.
     77    */
     78   long long num_results;
     79 
     80   /**
     81    * Handle to a pending "history" operation.
     82    */
     83   struct TALER_BANK_CreditHistoryHandle *hh;
     84 
     85   /**
     86    * The interpreter.
     87    */
     88   struct TALER_TESTING_Interpreter *is;
     89 
     90   /**
     91    * Authentication data for the operation.
     92    */
     93   struct TALER_BANK_AuthenticationData auth;
     94 
     95   /**
     96    * Expected number of results (= rows).
     97    */
     98   uint64_t results_obtained;
     99 
    100   /**
    101    * Set to true if the callback detects something
    102    * unexpected.
    103    */
    104   bool failed;
    105 
    106   /**
    107    * Expected history.
    108    */
    109   struct History *h;
    110 
    111   /**
    112    * Length of @e h
    113    */
    114   unsigned int total;
    115 
    116 };
    117 
    118 
    119 /**
    120  * Log which history we expected.  Called when an error occurs.
    121  *
    122  * @param h what we expected.
    123  * @param h_len number of entries in @a h.
    124  * @param off position of the mismatch.
    125  */
    126 static void
    127 print_expected (struct History *h,
    128                 unsigned int h_len,
    129                 unsigned int off)
    130 {
    131   GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    132               "Transaction history (credit) mismatch at position %u/%u\n",
    133               off,
    134               h_len);
    135   GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    136               "Expected history:\n");
    137   for (unsigned int i = 0; i<h_len; i++)
    138   {
    139     const struct TALER_BANK_CreditDetails *cd
    140       = &h[i].credit_details;
    141 
    142     switch (cd->type)
    143     {
    144     case TALER_BANK_CT_RESERVE:
    145       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    146                   "H(%u): %s (serial: %llu, RES: %s,"
    147                   " counterpart: %s)\n",
    148                   i,
    149                   TALER_amount2s (&cd->amount),
    150                   (unsigned long long) h[i].row_id,
    151                   TALER_B2S (&cd->details.reserve.reserve_pub),
    152                   cd->debit_account_uri.full_payto);
    153       break;
    154     case TALER_BANK_CT_KYCAUTH:
    155       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    156                   "H(%u): %s (serial: %llu, KYC: %s,"
    157                   " counterpart: %s)\n",
    158                   i,
    159                   TALER_amount2s (&cd->amount),
    160                   (unsigned long long) h[i].row_id,
    161                   TALER_B2S (&cd->details.kycauth.account_pub),
    162                   cd->debit_account_uri.full_payto);
    163       break;
    164     case TALER_BANK_CT_WAD:
    165       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    166                   "H(%u): %s (serial: %llu, WAD: %s-%s,"
    167                   " counterpart: %s)\n",
    168                   i,
    169                   TALER_amount2s (&cd->amount),
    170                   (unsigned long long) h[i].row_id,
    171                   TALER_B2S (&cd->details.wad.wad_id),
    172                   cd->details.wad.origin_exchange_url,
    173                   cd->debit_account_uri.full_payto);
    174       break;
    175     }
    176   }
    177 }
    178 
    179 
    180 /**
    181  * Closure for command_cb().
    182  */
    183 struct IteratorContext
    184 {
    185   /**
    186    * Array of history items to return.
    187    */
    188   struct History *h;
    189 
    190   /**
    191    * Set to the row ID from where on we should actually process history items,
    192    * or NULL if we should process all of them.
    193    */
    194   const uint64_t *row_id_start;
    195 
    196   /**
    197    * History state we are working on.
    198    */
    199   struct HistoryState *hs;
    200 
    201   /**
    202    * Current length of the @e h array.
    203    */
    204   unsigned int total;
    205 
    206   /**
    207    * Current write position in @e h array.
    208    */
    209   unsigned int pos;
    210 
    211   /**
    212    * Ok equals True whenever a starting row_id was provided AND was found
    213    * among the CMDs, OR no starting row was given in the first place.
    214    */
    215   bool ok;
    216 
    217 };
    218 
    219 
    220 /**
    221  * Helper function of build_history() that expands
    222  * the history for each relevant command encountered.
    223  *
    224  * @param[in,out] cls our `struct IteratorContext`
    225  * @param cmd a command to process
    226  */
    227 static void
    228 command_cb (void *cls,
    229             const struct TALER_TESTING_Command *cmd)
    230 {
    231   struct IteratorContext *ic = cls;
    232   struct HistoryState *hs = ic->hs;
    233   const uint64_t *row_id;
    234   const struct TALER_FullPayto *credit_account;
    235   const struct TALER_FullPayto *debit_account;
    236   const struct TALER_Amount *amount;
    237   const struct TALER_ReservePublicKeyP *reserve_pub;
    238   const char *exchange_credit_url;
    239 
    240   /**
    241    * The following command allows us to skip over those CMDs
    242    * that do not offer a "row_id" trait.  Such skipped CMDs are
    243    * not interesting for building a history.
    244    */
    245   if ( (GNUNET_OK !=
    246         TALER_TESTING_get_trait_bank_row (cmd,
    247                                           &row_id)) ||
    248        (GNUNET_OK !=
    249         TALER_TESTING_get_trait_credit_payto_uri (cmd,
    250                                                   &credit_account)) ||
    251        (GNUNET_OK !=
    252         TALER_TESTING_get_trait_debit_payto_uri (cmd,
    253                                                  &debit_account)) ||
    254        (GNUNET_OK !=
    255         TALER_TESTING_get_trait_amount (cmd,
    256                                         &amount)) ||
    257        (GNUNET_OK !=
    258         TALER_TESTING_get_trait_reserve_pub (cmd,
    259                                              &reserve_pub)) ||
    260        (GNUNET_OK !=
    261         TALER_TESTING_get_trait_exchange_bank_account_url (
    262           cmd,
    263           &exchange_credit_url)) )
    264     return;   // Not an interesting event
    265   // FIXME: support KYCAUTH transfer events!
    266   // FIXME-#7271: support WAD transfer events!
    267 
    268   /**
    269    * Is the interesting event a match with regard to
    270    * the row_id value?  If yes, store this condition
    271    * to the state and analyze the next CMDs.
    272    */
    273   if ( (NULL != ic->row_id_start) &&
    274        (*(ic->row_id_start) == *row_id) &&
    275        (! ic->ok) )
    276   {
    277     ic->ok = true;
    278     return;
    279   }
    280   /**
    281    * The interesting event didn't match the wanted
    282    * row_id value, analyze the next CMDs.  Note: this
    283    * branch is relevant only when row_id WAS given.
    284    */
    285   if (! ic->ok)
    286     return;
    287   if (0 != strcasecmp (hs->account_url,
    288                        exchange_credit_url))
    289     return;   // Account mismatch
    290   if (ic->total >= GNUNET_MAX (hs->num_results,
    291                                -hs->num_results) )
    292   {
    293     TALER_LOG_DEBUG ("Hit history limit\n");
    294     return;
    295   }
    296   TALER_LOG_INFO ("Found history: %s->%s for account %s\n",
    297                   debit_account->full_payto,
    298                   credit_account->full_payto,
    299                   hs->account_url);
    300   /* found matching record, make sure we have room */
    301   if (ic->pos == ic->total)
    302     GNUNET_array_grow (ic->h,
    303                        ic->total,
    304                        ic->pos * 2);
    305   ic->h[ic->pos].url
    306     = GNUNET_strdup (debit_account->full_payto);
    307   ic->h[ic->pos].row_id
    308     = *row_id;
    309   ic->h[ic->pos].credit_details.type
    310     = TALER_BANK_CT_RESERVE;
    311   ic->h[ic->pos].credit_details.debit_account_uri.full_payto
    312     = ic->h[ic->pos].url;
    313   ic->h[ic->pos].credit_details.amount
    314     = *amount;
    315   ic->h[ic->pos].credit_details.details.reserve.reserve_pub
    316     = *reserve_pub;
    317   ic->pos++;
    318 }
    319 
    320 
    321 /**
    322  * This function constructs the list of history elements that
    323  * interest the account number of the caller.  It has two main
    324  * loops: the first to figure out how many history elements have
    325  * to be allocated, and the second to actually populate every
    326  * element.
    327  *
    328  * @param hs history state
    329  * @param[out] rh history array to initialize.
    330  * @return number of entries in @a rh.
    331  */
    332 static unsigned int
    333 build_history (struct HistoryState *hs,
    334                struct History **rh)
    335 {
    336   struct TALER_TESTING_Interpreter *is = hs->is;
    337   struct IteratorContext ic = {
    338     .hs = hs
    339   };
    340 
    341   if (NULL != hs->start_row_reference)
    342   {
    343     const struct TALER_TESTING_Command *add_incoming_cmd;
    344 
    345     TALER_LOG_INFO ("`%s': start row given via reference `%s'\n",
    346                     TALER_TESTING_interpreter_get_current_label (is),
    347                     hs->start_row_reference);
    348     add_incoming_cmd
    349       = TALER_TESTING_interpreter_lookup_command (is,
    350                                                   hs->start_row_reference);
    351     GNUNET_assert (NULL != add_incoming_cmd);
    352     GNUNET_assert (GNUNET_OK ==
    353                    TALER_TESTING_get_trait_row (add_incoming_cmd,
    354                                                 &ic.row_id_start));
    355   }
    356 
    357   ic.ok = false;
    358   if (NULL == ic.row_id_start)
    359     ic.ok = true;
    360   GNUNET_array_grow (ic.h,
    361                      ic.total,
    362                      4);
    363   GNUNET_assert (0 != hs->num_results);
    364   TALER_TESTING_iterate (is,
    365                          hs->num_results > 0,
    366                          &command_cb,
    367                          &ic);
    368   GNUNET_assert (ic.ok);
    369   GNUNET_array_grow (ic.h,
    370                      ic.total,
    371                      ic.pos);
    372   if (0 == ic.pos)
    373     TALER_LOG_DEBUG ("Empty credit history computed\n");
    374   *rh = ic.h;
    375   return ic.pos;
    376 }
    377 
    378 
    379 /**
    380  * Check that the "/history/incoming" response matches the
    381  * CMD whose offset in the list of CMDs is @a off.
    382  *
    383  * @param h expected history (array)
    384  * @param total length of @a h
    385  * @param off the offset (of the CMD list) where the command
    386  *        to check is.
    387  * @param credit_details the expected transaction details.
    388  * @return #GNUNET_OK if the transaction is what we expect.
    389  */
    390 static enum GNUNET_GenericReturnValue
    391 check_result (struct History *h,
    392               unsigned int total,
    393               unsigned int off,
    394               const struct TALER_BANK_CreditDetails *credit_details)
    395 {
    396   if (off >= total)
    397   {
    398     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    399                 "Test says history has at most %u"
    400                 " results, but got result #%u to check\n",
    401                 total,
    402                 off);
    403     print_expected (h,
    404                     total,
    405                     off);
    406     return GNUNET_SYSERR;
    407   }
    408   if ( (h[off].credit_details.type !=
    409         credit_details->type) ||
    410        (0 != TALER_amount_cmp (&h[off].credit_details.amount,
    411                                &credit_details->amount)) ||
    412        (0 != TALER_full_payto_normalize_and_cmp (
    413           h[off].credit_details.debit_account_uri,
    414           credit_details->debit_account_uri)) )
    415   {
    416     GNUNET_break (0);
    417     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    418                 "expected debit_account_uri: %s with %s\n",
    419                 h[off].credit_details.debit_account_uri.full_payto,
    420                 TALER_amount2s (&h[off].credit_details.amount));
    421     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    422                 "actual debit_account_uri: %s with %s\n",
    423                 credit_details->debit_account_uri.full_payto,
    424                 TALER_amount2s (&credit_details->amount));
    425     print_expected (h,
    426                     total,
    427                     off);
    428     return GNUNET_SYSERR;
    429   }
    430   switch (credit_details->type)
    431   {
    432   case TALER_BANK_CT_RESERVE:
    433     if (0 !=
    434         GNUNET_memcmp (&h[off].credit_details.details.reserve.reserve_pub,
    435                        &credit_details->details.reserve.reserve_pub))
    436     {
    437       GNUNET_break (0);
    438       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    439                   "expected debit_account_uri: %s with %s for %s\n",
    440                   h[off].credit_details.debit_account_uri.full_payto,
    441                   TALER_amount2s (&h[off].credit_details.amount),
    442                   TALER_B2S (&h[off].credit_details.details.reserve.reserve_pub)
    443                   );
    444       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    445                   "actual debit_account_uri: %s with %s for %s\n",
    446                   credit_details->debit_account_uri.full_payto,
    447                   TALER_amount2s (&credit_details->amount),
    448                   TALER_B2S (&credit_details->details.reserve.reserve_pub));
    449       print_expected (h,
    450                       total,
    451                       off);
    452       return GNUNET_SYSERR;
    453     }
    454     break;
    455   case TALER_BANK_CT_KYCAUTH:
    456     if (0 != GNUNET_memcmp (&h[off].credit_details.details.kycauth.account_pub,
    457                             &credit_details->details.kycauth.account_pub))
    458     {
    459       GNUNET_break (0);
    460       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    461                   "expected debit_account_uri: %s with %s for %s\n",
    462                   h[off].credit_details.debit_account_uri.full_payto,
    463                   TALER_amount2s (&h[off].credit_details.amount),
    464                   TALER_B2S (&h[off].credit_details.details.kycauth.account_pub)
    465                   );
    466       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    467                   "actual debit_account_uri: %s with %s for %s\n",
    468                   credit_details->debit_account_uri.full_payto,
    469                   TALER_amount2s (&credit_details->amount),
    470                   TALER_B2S (&credit_details->details.kycauth.account_pub));
    471       print_expected (h,
    472                       total,
    473                       off);
    474       return GNUNET_SYSERR;
    475     }
    476     break;
    477   case TALER_BANK_CT_WAD:
    478     if ( (0 != GNUNET_memcmp (&h[off].credit_details.details.wad.wad_id,
    479                               &credit_details->details.wad.wad_id)) ||
    480          (0 != strcmp (h[off].credit_details.details.wad.origin_exchange_url,
    481                        credit_details->details.wad.origin_exchange_url)) )
    482     {
    483       GNUNET_break (0);
    484       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    485                   "expected debit_account_uri: %s with %s for %s-%s\n",
    486                   h[off].credit_details.debit_account_uri.full_payto,
    487                   TALER_amount2s (&h[off].credit_details.amount),
    488                   h[off].credit_details.details.wad.origin_exchange_url,
    489                   TALER_B2S (&h[off].credit_details.details.wad.wad_id));
    490       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    491                   "actual debit_account_uri: %s with %s for %s-%s\n",
    492                   credit_details->debit_account_uri.full_payto,
    493                   TALER_amount2s (&credit_details->amount),
    494                   credit_details->details.wad.origin_exchange_url,
    495                   TALER_B2S (&credit_details->details.wad.wad_id));
    496       print_expected (h,
    497                       total,
    498                       off);
    499       return GNUNET_SYSERR;
    500     }
    501     break;
    502   }
    503   return GNUNET_OK;
    504 }
    505 
    506 
    507 /**
    508  * This callback will (1) check that the HTTP response code
    509  * is acceptable and (2) that the history is consistent.  The
    510  * consistency is checked by going through all the past CMDs,
    511  * reconstructing then the expected history as of those, and
    512  * finally check it against what the bank returned.
    513  *
    514  * @param cls closure.
    515  * @param chr http response details
    516  */
    517 static void
    518 history_cb (void *cls,
    519             const struct TALER_BANK_CreditHistoryResponse *chr)
    520 {
    521   struct HistoryState *hs = cls;
    522   struct TALER_TESTING_Interpreter *is = hs->is;
    523 
    524   hs->hh = NULL;
    525   switch (chr->http_status)
    526   {
    527   case 0:
    528     GNUNET_break (0);
    529     goto error;
    530   case MHD_HTTP_OK:
    531     for (unsigned int i = 0; i<chr->details.ok.details_length; i++)
    532     {
    533       const struct TALER_BANK_CreditDetails *cd =
    534         &chr->details.ok.details[i];
    535 
    536       /* check current element */
    537       if (GNUNET_OK !=
    538           check_result (hs->h,
    539                         hs->total,
    540                         hs->results_obtained,
    541                         cd))
    542       {
    543         GNUNET_break (0);
    544         json_dumpf (chr->response,
    545                     stderr,
    546                     JSON_COMPACT);
    547         hs->failed = true;
    548         hs->hh = NULL;
    549         TALER_TESTING_interpreter_fail (is);
    550         return;
    551       }
    552       hs->results_obtained++;
    553     }
    554     TALER_TESTING_interpreter_next (is);
    555     return;
    556   case MHD_HTTP_NO_CONTENT:
    557     if (0 == hs->total)
    558     {
    559       /* not found is OK for empty history */
    560       TALER_TESTING_interpreter_next (is);
    561       return;
    562     }
    563     GNUNET_break (0);
    564     goto error;
    565   case MHD_HTTP_NOT_FOUND:
    566     if (0 == hs->total)
    567     {
    568       /* not found is OK for empty history */
    569       TALER_TESTING_interpreter_next (is);
    570       return;
    571     }
    572     GNUNET_break (0);
    573     goto error;
    574   default:
    575     hs->hh = NULL;
    576     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    577                 "Unwanted response code from /history/incoming: %u\n",
    578                 chr->http_status);
    579     TALER_TESTING_interpreter_fail (is);
    580     return;
    581   }
    582 error:
    583   GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    584               "Expected history of length %u, got %llu;"
    585               " HTTP status code: %u/%d, failed: %d\n",
    586               hs->total,
    587               (unsigned long long) hs->results_obtained,
    588               chr->http_status,
    589               (int) chr->ec,
    590               hs->failed ? 1 : 0);
    591   print_expected (hs->h,
    592                   hs->total,
    593                   UINT_MAX);
    594   TALER_TESTING_interpreter_fail (is);
    595 }
    596 
    597 
    598 /**
    599  * Run the command.
    600  *
    601  * @param cls closure.
    602  * @param cmd the command to execute.
    603  * @param is the interpreter state.
    604  */
    605 static void
    606 history_run (void *cls,
    607              const struct TALER_TESTING_Command *cmd,
    608              struct TALER_TESTING_Interpreter *is)
    609 {
    610   struct HistoryState *hs = cls;
    611   uint64_t row_id = (hs->num_results > 0) ? 0 : UINT64_MAX;
    612   const uint64_t *row_ptr;
    613 
    614   (void) cmd;
    615   hs->is = is;
    616   /* Get row_id from trait. */
    617   if (NULL != hs->start_row_reference)
    618   {
    619     const struct TALER_TESTING_Command *history_cmd;
    620 
    621     history_cmd = TALER_TESTING_interpreter_lookup_command (
    622       is,
    623       hs->start_row_reference);
    624     if (NULL == history_cmd)
    625       TALER_TESTING_FAIL (is);
    626 
    627     if (GNUNET_OK !=
    628         TALER_TESTING_get_trait_row (history_cmd,
    629                                      &row_ptr))
    630       TALER_TESTING_FAIL (is);
    631     else
    632       row_id = *row_ptr;
    633     TALER_LOG_DEBUG ("row id (from trait) is %llu\n",
    634                      (unsigned long long) row_id);
    635   }
    636   hs->total = build_history (hs,
    637                              &hs->h);
    638   hs->hh = TALER_BANK_credit_history (
    639     TALER_TESTING_interpreter_get_context (is),
    640     &hs->auth,
    641     row_id,
    642     hs->num_results,
    643     GNUNET_TIME_UNIT_ZERO,
    644     &history_cb,
    645     hs);
    646   GNUNET_assert (NULL != hs->hh);
    647 }
    648 
    649 
    650 /**
    651  * Free the state from a "history" CMD, and possibly cancel
    652  * a pending operation thereof.
    653  *
    654  * @param cls closure.
    655  * @param cmd the command which is being cleaned up.
    656  */
    657 static void
    658 history_cleanup (void *cls,
    659                  const struct TALER_TESTING_Command *cmd)
    660 {
    661   struct HistoryState *hs = cls;
    662 
    663   (void) cmd;
    664   if (NULL != hs->hh)
    665   {
    666     TALER_TESTING_command_incomplete (hs->is,
    667                                       cmd->label);
    668     TALER_BANK_credit_history_cancel (hs->hh);
    669   }
    670   GNUNET_free (hs->account_url);
    671   for (unsigned int off = 0; off<hs->total; off++)
    672     GNUNET_free (hs->h[off].url);
    673   GNUNET_free (hs->h);
    674   GNUNET_free (hs);
    675 }
    676 
    677 
    678 struct TALER_TESTING_Command
    679 TALER_TESTING_cmd_bank_credits (
    680   const char *label,
    681   const struct TALER_BANK_AuthenticationData *auth,
    682   const char *start_row_reference,
    683   long long num_results)
    684 {
    685   struct HistoryState *hs;
    686 
    687   hs = GNUNET_new (struct HistoryState);
    688   hs->account_url = GNUNET_strdup (auth->wire_gateway_url);
    689   hs->start_row_reference = start_row_reference;
    690   hs->num_results = num_results;
    691   hs->auth = *auth;
    692   {
    693     struct TALER_TESTING_Command cmd = {
    694       .label = label,
    695       .cls = hs,
    696       .run = &history_run,
    697       .cleanup = &history_cleanup
    698     };
    699 
    700     return cmd;
    701   }
    702 }
    703 
    704 
    705 /* end of testing_api_cmd_credit_history.c */