LCOV - code coverage report
Current view: top level - testing - testing_api_cmd_bank_admin_add_incoming.c (source / functions) Hit Total Coverage
Test: GNU Taler exchange coverage report Lines: 84 158 53.2 %
Date: 2022-08-25 06:15:09 Functions: 8 10 80.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : /*
       2             :   This file is part of TALER
       3             :   Copyright (C) 2018-2021 Taler Systems SA
       4             : 
       5             :   TALER is free software; you can redistribute it and/or modify it
       6             :   under the terms of the GNU General Public License as published by
       7             :   the Free Software Foundation; either version 3, or (at your
       8             :   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 GNU
      13             :   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_admin_add_incoming.c
      21             :  * @brief implementation of a bank /admin/add-incoming command
      22             :  * @author Christian Grothoff
      23             :  * @author Marcello Stanisci
      24             :  */
      25             : #include "platform.h"
      26             : #include "backoff.h"
      27             : #include "taler_json_lib.h"
      28             : #include <gnunet/gnunet_curl_lib.h>
      29             : #include "taler_bank_service.h"
      30             : #include "taler_fakebank_lib.h"
      31             : #include "taler_signatures.h"
      32             : #include "taler_testing_lib.h"
      33             : 
      34             : /**
      35             :  * How long do we wait AT MOST when retrying?
      36             :  */
      37             : #define MAX_BACKOFF GNUNET_TIME_relative_multiply ( \
      38             :     GNUNET_TIME_UNIT_MILLISECONDS, 100)
      39             : 
      40             : 
      41             : /**
      42             :  * How often do we retry before giving up?
      43             :  */
      44             : #define NUM_RETRIES 5
      45             : 
      46             : 
      47             : /**
      48             :  * State for a "fakebank transfer" CMD.
      49             :  */
      50             : struct AdminAddIncomingState
      51             : {
      52             : 
      53             :   /**
      54             :    * Label of any command that can trait-offer a reserve priv.
      55             :    */
      56             :   const char *reserve_reference;
      57             : 
      58             :   /**
      59             :    * Wire transfer amount.
      60             :    */
      61             :   struct TALER_Amount amount;
      62             : 
      63             :   /**
      64             :    * Base URL of the credited account.
      65             :    */
      66             :   const char *exchange_credit_url;
      67             : 
      68             :   /**
      69             :    * Money sender payto URL.
      70             :    */
      71             :   const char *payto_debit_account;
      72             : 
      73             :   /**
      74             :    * Username to use for authentication.
      75             :    */
      76             :   struct TALER_BANK_AuthenticationData auth;
      77             : 
      78             :   /**
      79             :    * Set (by the interpreter) to the reserve's private key
      80             :    * we used to make a wire transfer subject line with.
      81             :    */
      82             :   struct TALER_ReservePrivateKeyP reserve_priv;
      83             : 
      84             :   /**
      85             :    * Whether we know the private key or not.
      86             :    */
      87             :   bool reserve_priv_known;
      88             : 
      89             :   /**
      90             :    * Reserve public key matching @e reserve_priv.
      91             :    */
      92             :   struct TALER_ReservePublicKeyP reserve_pub;
      93             : 
      94             :   /**
      95             :    * Handle to the pending request at the fakebank.
      96             :    */
      97             :   struct TALER_BANK_AdminAddIncomingHandle *aih;
      98             : 
      99             :   /**
     100             :    * Interpreter state.
     101             :    */
     102             :   struct TALER_TESTING_Interpreter *is;
     103             : 
     104             :   /**
     105             :    * Reserve history entry that corresponds to this operation.
     106             :    * Will be of type #TALER_EXCHANGE_RTT_CREDIT.  Note that
     107             :    * the "sender_url" field is set to a 'const char *' and
     108             :    * MUST NOT be free()'ed.
     109             :    */
     110             :   struct TALER_EXCHANGE_ReserveHistoryEntry reserve_history;
     111             : 
     112             :   /**
     113             :    * Set to the wire transfer's unique ID.
     114             :    */
     115             :   uint64_t serial_id;
     116             : 
     117             :   /**
     118             :    * Timestamp of the transaction (as returned from the bank).
     119             :    */
     120             :   struct GNUNET_TIME_Timestamp timestamp;
     121             : 
     122             :   /**
     123             :    * Merchant instance.  Sometimes used to get the tip reserve
     124             :    * private key by reading the appropriate config section.
     125             :    */
     126             :   const char *instance;
     127             : 
     128             :   /**
     129             :    * Configuration filename.  Used to get the tip reserve key
     130             :    * filename (used to obtain a public key to write in the
     131             :    * transfer subject).
     132             :    */
     133             :   const char *config_filename;
     134             : 
     135             :   /**
     136             :    * Task scheduled to try later.
     137             :    */
     138             :   struct GNUNET_SCHEDULER_Task *retry_task;
     139             : 
     140             :   /**
     141             :    * How long do we wait until we retry?
     142             :    */
     143             :   struct GNUNET_TIME_Relative backoff;
     144             : 
     145             :   /**
     146             :    * Was this command modified via
     147             :    * #TALER_TESTING_cmd_admin_add_incoming_with_retry to
     148             :    * enable retries? If so, how often should we still retry?
     149             :    */
     150             :   unsigned int do_retry;
     151             : 
     152             :   /**
     153             :    * Expected HTTP status code.
     154             :    */
     155             :   unsigned int expected_http_status;
     156             : };
     157             : 
     158             : 
     159             : /**
     160             :  * Run the "fakebank transfer" CMD.
     161             :  *
     162             :  * @param cls closure.
     163             :  * @param cmd CMD being run.
     164             :  * @param is interpreter state.
     165             :  */
     166             : static void
     167             : admin_add_incoming_run (void *cls,
     168             :                         const struct TALER_TESTING_Command *cmd,
     169             :                         struct TALER_TESTING_Interpreter *is);
     170             : 
     171             : 
     172             : /**
     173             :  * Task scheduled to re-try #admin_add_incoming_run.
     174             :  *
     175             :  * @param cls a `struct AdminAddIncomingState`
     176             :  */
     177             : static void
     178           0 : do_retry (void *cls)
     179             : {
     180           0 :   struct AdminAddIncomingState *fts = cls;
     181             : 
     182           0 :   fts->retry_task = NULL;
     183           0 :   fts->is->commands[fts->is->ip].last_req_time
     184           0 :     = GNUNET_TIME_absolute_get ();
     185           0 :   admin_add_incoming_run (fts,
     186             :                           NULL,
     187             :                           fts->is);
     188           0 : }
     189             : 
     190             : 
     191             : /**
     192             :  * This callback will process the fakebank response to the wire
     193             :  * transfer.  It just checks whether the HTTP response code is
     194             :  * acceptable.
     195             :  *
     196             :  * @param cls closure with the interpreter state
     197             :  * @param http_status HTTP response code, #MHD_HTTP_OK (200) for
     198             :  *        successful status request; 0 if the exchange's reply is
     199             :  *        bogus (fails to follow the protocol)
     200             :  * @param ec taler-specific error code, #TALER_EC_NONE on success
     201             :  * @param serial_id unique ID of the wire transfer
     202             :  * @param timestamp time stamp of the transaction made.
     203             :  * @param json raw response
     204             :  */
     205             : static void
     206           6 : confirmation_cb (void *cls,
     207             :                  unsigned int http_status,
     208             :                  enum TALER_ErrorCode ec,
     209             :                  uint64_t serial_id,
     210             :                  struct GNUNET_TIME_Timestamp timestamp,
     211             :                  const json_t *json)
     212             : {
     213           6 :   struct AdminAddIncomingState *fts = cls;
     214           6 :   struct TALER_TESTING_Interpreter *is = fts->is;
     215             : 
     216             :   (void) json;
     217           6 :   fts->reserve_history.details.in_details.timestamp = timestamp;
     218           6 :   fts->reserve_history.details.in_details.wire_reference = serial_id;
     219           6 :   fts->aih = NULL;
     220             :   /**
     221             :    * Test case not caring about the HTTP status code.
     222             :    * That helps when Fakebank and Libeufin diverge in
     223             :    * the response status code.  An example is the
     224             :    * /admin/add-incoming: libeufin return ALWAYS '200 OK'
     225             :    * (see note below) whereas the Fakebank responds with
     226             :    * '409 Conflict' upon a duplicate reserve public key.
     227             :    *
     228             :    * Note: this decision aims at avoiding to put Taler
     229             :    * logic into the Sandbox; that's because banks DO allow
     230             :    * their customers to wire the same subject multiple
     231             :    * times.  Hence, instead of triggering any error, libeufin
     232             :    * bounces the payment back in the same way it does for
     233             :    * malformed reserve public keys.
     234             :    */
     235           6 :   if (-1 == (int) fts->expected_http_status)
     236             :   {
     237           2 :     TALER_TESTING_interpreter_next (is);
     238           2 :     return;
     239             :   }
     240           4 :   if (http_status != fts->expected_http_status)
     241             :   {
     242           0 :     GNUNET_break (0);
     243           0 :     TALER_TESTING_interpreter_fail (is);
     244           0 :     return;
     245             :   }
     246           4 :   switch (http_status)
     247             :   {
     248           4 :   case MHD_HTTP_OK:
     249           4 :     fts->serial_id = serial_id;
     250           4 :     fts->timestamp = timestamp;
     251           4 :     TALER_TESTING_interpreter_next (is);
     252           4 :     return;
     253           0 :   case MHD_HTTP_UNAUTHORIZED:
     254           0 :     switch (fts->auth.method)
     255             :     {
     256           0 :     case TALER_BANK_AUTH_NONE:
     257           0 :       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     258             :                   "Authentication required, but none configure.\n");
     259           0 :       break;
     260           0 :     case TALER_BANK_AUTH_BASIC:
     261           0 :       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     262             :                   "Basic authentication (%s) failed.\n",
     263             :                   fts->auth.details.basic.username);
     264           0 :       break;
     265             :     }
     266           0 :     break;
     267           0 :   case MHD_HTTP_CONFLICT:
     268           0 :     TALER_TESTING_interpreter_next (is);
     269           0 :     return;
     270           0 :   default:
     271           0 :     if (0 != fts->do_retry)
     272             :     {
     273           0 :       fts->do_retry--;
     274           0 :       if ( (0 == http_status) ||
     275           0 :            (TALER_EC_GENERIC_DB_SOFT_FAILURE == ec) ||
     276             :            (MHD_HTTP_INTERNAL_SERVER_ERROR == http_status) )
     277             :       {
     278           0 :         GNUNET_log (
     279             :           GNUNET_ERROR_TYPE_INFO,
     280             :           "Retrying fakebank transfer failed with %u/%d\n",
     281             :           http_status,
     282             :           (int) ec);
     283             :         /* on DB conflicts, do not use backoff */
     284           0 :         if (TALER_EC_GENERIC_DB_SOFT_FAILURE == ec)
     285           0 :           fts->backoff = GNUNET_TIME_UNIT_ZERO;
     286             :         else
     287           0 :           fts->backoff = GNUNET_TIME_randomized_backoff (fts->backoff,
     288             :                                                          MAX_BACKOFF);
     289           0 :         fts->is->commands[fts->is->ip].num_tries++;
     290           0 :         fts->retry_task = GNUNET_SCHEDULER_add_delayed (
     291             :           fts->backoff,
     292             :           &do_retry,
     293             :           fts);
     294           0 :         return;
     295             :       }
     296             :     }
     297           0 :     break;
     298             :   }
     299           0 :   GNUNET_break (0);
     300           0 :   GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     301             :               "Fakebank returned HTTP status %u/%d\n",
     302             :               http_status,
     303             :               (int) ec);
     304           0 :   TALER_TESTING_interpreter_fail (is);
     305             : }
     306             : 
     307             : 
     308             : /**
     309             :  * Run the "fakebank transfer" CMD.
     310             :  *
     311             :  * @param cls closure.
     312             :  * @param cmd CMD being run.
     313             :  * @param is interpreter state.
     314             :  */
     315             : static void
     316           6 : admin_add_incoming_run (void *cls,
     317             :                         const struct TALER_TESTING_Command *cmd,
     318             :                         struct TALER_TESTING_Interpreter *is)
     319             : {
     320           6 :   struct AdminAddIncomingState *fts = cls;
     321           6 :   bool have_public = false;
     322             : 
     323             :   (void) cmd;
     324             :   /* Use reserve public key as subject */
     325           6 :   if (NULL != fts->reserve_reference)
     326             :   {
     327             :     const struct TALER_TESTING_Command *ref;
     328             :     const struct TALER_ReservePrivateKeyP *reserve_priv;
     329             :     const struct TALER_ReservePublicKeyP *reserve_pub;
     330             : 
     331           2 :     ref = TALER_TESTING_interpreter_lookup_command
     332             :             (is, fts->reserve_reference);
     333           2 :     if (NULL == ref)
     334             :     {
     335           0 :       GNUNET_break (0);
     336           0 :       TALER_TESTING_interpreter_fail (is);
     337           0 :       return;
     338             :     }
     339           2 :     if (GNUNET_OK !=
     340           2 :         TALER_TESTING_get_trait_reserve_priv (ref,
     341             :                                               &reserve_priv))
     342             :     {
     343           0 :       if (GNUNET_OK != TALER_TESTING_get_trait_reserve_pub (ref,
     344             :                                                             &reserve_pub))
     345             :       {
     346           0 :         GNUNET_break (0);
     347           0 :         TALER_TESTING_interpreter_fail (is);
     348           0 :         return;
     349             :       }
     350           0 :       have_public = true;
     351           0 :       fts->reserve_pub.eddsa_pub = reserve_pub->eddsa_pub;
     352           0 :       fts->reserve_priv_known = false;
     353             :     }
     354             :     else
     355             :     {
     356           2 :       fts->reserve_priv.eddsa_priv = reserve_priv->eddsa_priv;
     357           2 :       fts->reserve_priv_known = true;
     358             :     }
     359             :   }
     360             :   else
     361             :   {
     362             :     /* No referenced reserve, no instance to take priv
     363             :      * from, no explicit subject given: create new key! */
     364           4 :     GNUNET_CRYPTO_eddsa_key_create (&fts->reserve_priv.eddsa_priv);
     365           4 :     fts->reserve_priv_known = true;
     366             :   }
     367           6 :   if (! have_public)
     368           6 :     GNUNET_CRYPTO_eddsa_key_get_public (&fts->reserve_priv.eddsa_priv,
     369             :                                         &fts->reserve_pub.eddsa_pub);
     370           6 :   fts->reserve_history.type = TALER_EXCHANGE_RTT_CREDIT;
     371           6 :   fts->reserve_history.amount = fts->amount;
     372             :   fts->reserve_history.details.in_details.sender_url
     373           6 :     = (char *) fts->payto_debit_account; /* remember to NOT free this one... */
     374           6 :   fts->is = is;
     375             :   fts->aih
     376           6 :     = TALER_BANK_admin_add_incoming (
     377             :         TALER_TESTING_interpreter_get_context (is),
     378           6 :         &fts->auth,
     379           6 :         &fts->reserve_pub,
     380           6 :         &fts->amount,
     381             :         fts->payto_debit_account,
     382             :         &confirmation_cb,
     383             :         fts);
     384           6 :   if (NULL == fts->aih)
     385             :   {
     386           0 :     GNUNET_break (0);
     387           0 :     TALER_TESTING_interpreter_fail (is);
     388           0 :     return;
     389             :   }
     390             : }
     391             : 
     392             : 
     393             : /**
     394             :  * Free the state of a "/admin/add-incoming" CMD, and possibly
     395             :  * cancel a pending operation thereof.
     396             :  *
     397             :  * @param cls closure
     398             :  * @param cmd current CMD being cleaned up.
     399             :  */
     400             : static void
     401           6 : admin_add_incoming_cleanup (void *cls,
     402             :                             const struct TALER_TESTING_Command *cmd)
     403             : {
     404           6 :   struct AdminAddIncomingState *fts = cls;
     405             : 
     406           6 :   if (NULL != fts->aih)
     407             :   {
     408           0 :     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
     409             :                 "Command %s did not complete\n",
     410             :                 cmd->label);
     411           0 :     TALER_BANK_admin_add_incoming_cancel (fts->aih);
     412           0 :     fts->aih = NULL;
     413             :   }
     414           6 :   if (NULL != fts->retry_task)
     415             :   {
     416           0 :     GNUNET_SCHEDULER_cancel (fts->retry_task);
     417           0 :     fts->retry_task = NULL;
     418             :   }
     419           6 :   GNUNET_free (fts);
     420           6 : }
     421             : 
     422             : 
     423             : /**
     424             :  * Offer internal data from a "/admin/add-incoming" CMD to other
     425             :  * commands.
     426             :  *
     427             :  * @param cls closure.
     428             :  * @param[out] ret result
     429             :  * @param trait name of the trait.
     430             :  * @param index index number of the object to offer.
     431             :  * @return #GNUNET_OK on success.
     432             :  */
     433             : static enum GNUNET_GenericReturnValue
     434          50 : admin_add_incoming_traits (void *cls,
     435             :                            const void **ret,
     436             :                            const char *trait,
     437             :                            unsigned int index)
     438             : {
     439          50 :   struct AdminAddIncomingState *fts = cls;
     440             :   static const char *void_uri = "payto://void/the-exchange";
     441             : 
     442          50 :   if (MHD_HTTP_OK !=
     443          50 :       fts->expected_http_status)
     444           6 :     return GNUNET_NO; /* requests that failed generate no history */
     445          44 :   if (fts->reserve_priv_known)
     446             :   {
     447             :     struct TALER_TESTING_Trait traits[] = {
     448          44 :       TALER_TESTING_make_trait_bank_row (&fts->serial_id),
     449          44 :       TALER_TESTING_make_trait_debit_payto_uri (&fts->payto_debit_account),
     450          44 :       TALER_TESTING_make_trait_payto_uri (&fts->payto_debit_account),
     451             :       /* Used as a marker, content does not matter */
     452          44 :       TALER_TESTING_make_trait_credit_payto_uri (&void_uri),
     453          44 :       TALER_TESTING_make_trait_exchange_bank_account_url (
     454             :         &fts->exchange_credit_url),
     455          44 :       TALER_TESTING_make_trait_amount (&fts->amount),
     456          44 :       TALER_TESTING_make_trait_timestamp (0,
     457          44 :                                           &fts->timestamp),
     458          44 :       TALER_TESTING_make_trait_reserve_priv (&fts->reserve_priv),
     459          44 :       TALER_TESTING_make_trait_reserve_pub (&fts->reserve_pub),
     460          44 :       TALER_TESTING_make_trait_reserve_history (0,
     461          44 :                                                 &fts->reserve_history),
     462          44 :       TALER_TESTING_trait_end ()
     463             :     };
     464             : 
     465          44 :     return TALER_TESTING_get_trait (traits,
     466             :                                     ret,
     467             :                                     trait,
     468             :                                     index);
     469             :   }
     470             :   else
     471             :   {
     472             :     struct TALER_TESTING_Trait traits[] = {
     473           0 :       TALER_TESTING_make_trait_bank_row (&fts->serial_id),
     474           0 :       TALER_TESTING_make_trait_debit_payto_uri (&fts->payto_debit_account),
     475             :       /* Used as a marker, content does not matter */
     476           0 :       TALER_TESTING_make_trait_credit_payto_uri (&void_uri),
     477           0 :       TALER_TESTING_make_trait_exchange_bank_account_url (
     478             :         &fts->exchange_credit_url),
     479           0 :       TALER_TESTING_make_trait_amount (&fts->amount),
     480           0 :       TALER_TESTING_make_trait_timestamp (0, &fts->timestamp),
     481           0 :       TALER_TESTING_make_trait_reserve_pub (&fts->reserve_pub),
     482           0 :       TALER_TESTING_make_trait_reserve_history (0,
     483           0 :                                                 &fts->reserve_history),
     484           0 :       TALER_TESTING_trait_end ()
     485             :     };
     486             : 
     487           0 :     return TALER_TESTING_get_trait (traits,
     488             :                                     ret,
     489             :                                     trait,
     490             :                                     index);
     491             :   }
     492             : }
     493             : 
     494             : 
     495             : /**
     496             :  * Create internal state for "/admin/add-incoming" CMD.
     497             :  *
     498             :  * @param amount the amount to transfer.
     499             :  * @param payto_debit_account which account sends money
     500             :  * @param auth authentication data
     501             :  * @return the internal state
     502             :  */
     503             : static struct AdminAddIncomingState *
     504           6 : make_fts (const char *amount,
     505             :           const struct TALER_BANK_AuthenticationData *auth,
     506             :           const char *payto_debit_account)
     507             : {
     508             :   struct AdminAddIncomingState *fts;
     509             : 
     510           6 :   fts = GNUNET_new (struct AdminAddIncomingState);
     511           6 :   fts->exchange_credit_url = auth->wire_gateway_url;
     512           6 :   fts->payto_debit_account = payto_debit_account;
     513           6 :   fts->auth = *auth;
     514           6 :   fts->expected_http_status = MHD_HTTP_OK;
     515           6 :   if (GNUNET_OK !=
     516           6 :       TALER_string_to_amount (amount,
     517             :                               &fts->amount))
     518             :   {
     519           0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     520             :                 "Failed to parse amount `%s'\n",
     521             :                 amount);
     522           0 :     GNUNET_assert (0);
     523             :   }
     524           6 :   return fts;
     525             : }
     526             : 
     527             : 
     528             : /**
     529             :  * Helper function to create admin/add-incoming command.
     530             :  *
     531             :  * @param label command label.
     532             :  * @param fts internal state to use
     533             :  * @return the command.
     534             :  */
     535             : static struct TALER_TESTING_Command
     536           6 : make_command (const char *label,
     537             :               struct AdminAddIncomingState *fts)
     538             : {
     539           6 :   struct TALER_TESTING_Command cmd = {
     540             :     .cls = fts,
     541             :     .label = label,
     542             :     .run = &admin_add_incoming_run,
     543             :     .cleanup = &admin_add_incoming_cleanup,
     544             :     .traits = &admin_add_incoming_traits
     545             :   };
     546             : 
     547           6 :   return cmd;
     548             : }
     549             : 
     550             : 
     551             : struct TALER_TESTING_Command
     552           4 : TALER_TESTING_cmd_admin_add_incoming (
     553             :   const char *label,
     554             :   const char *amount,
     555             :   const struct TALER_BANK_AuthenticationData *auth,
     556             :   const char *payto_debit_account)
     557             : {
     558           4 :   return make_command (label,
     559             :                        make_fts (amount,
     560             :                                  auth,
     561             :                                  payto_debit_account));
     562             : }
     563             : 
     564             : 
     565             : struct TALER_TESTING_Command
     566           2 : TALER_TESTING_cmd_admin_add_incoming_with_ref (
     567             :   const char *label,
     568             :   const char *amount,
     569             :   const struct TALER_BANK_AuthenticationData *auth,
     570             :   const char *payto_debit_account,
     571             :   const char *ref,
     572             :   unsigned int http_status)
     573             : {
     574             :   struct AdminAddIncomingState *fts;
     575             : 
     576           2 :   fts = make_fts (amount,
     577             :                   auth,
     578             :                   payto_debit_account);
     579           2 :   fts->reserve_reference = ref;
     580           2 :   fts->expected_http_status = http_status;
     581           2 :   return make_command (label,
     582             :                        fts);
     583             : }
     584             : 
     585             : 
     586             : /**
     587             :  * Modify a fakebank transfer command to enable retries when the
     588             :  * reserve is not yet full or we get other transient errors from the
     589             :  * fakebank.
     590             :  *
     591             :  * @param cmd a fakebank transfer command
     592             :  * @return the command with retries enabled
     593             :  */
     594             : struct TALER_TESTING_Command
     595           0 : TALER_TESTING_cmd_admin_add_incoming_retry (struct TALER_TESTING_Command cmd)
     596             : {
     597             :   struct AdminAddIncomingState *fts;
     598             : 
     599           0 :   GNUNET_assert (&admin_add_incoming_run == cmd.run);
     600           0 :   fts = cmd.cls;
     601           0 :   fts->do_retry = NUM_RETRIES;
     602           0 :   return cmd;
     603             : }
     604             : 
     605             : 
     606             : /* end of testing_api_cmd_bank_admin_add_incoming.c */

Generated by: LCOV version 1.14