/*
This file is part of TALER
Copyright (C) 2020 Taler Systems SA
TALER is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 3, or
(at your option) any later version.
TALER is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public
License along with TALER; see the file COPYING. If not, see
*/
/**
* @file testing_api_cmd_post_transfers.c
* @brief command to test POST /transfers
* @author Christian Grothoff
*/
#include "platform.h"
#include
#include
#include "taler_merchant_service.h"
#include "taler_merchant_testing_lib.h"
/**
* State of a "POST /transfers" CMD.
*/
struct PostTransfersState
{
/**
* Handle for a "POST /transfers" request.
*/
struct TALER_MERCHANT_PostTransfersHandle *pth;
/**
* Handle for a "GET" bank account history request.
*/
struct TALER_BANK_DebitHistoryHandle *dhh;
/**
* The interpreter state.
*/
struct TALER_TESTING_Interpreter *is;
/**
* Base URL of the merchant serving the request.
*/
const char *merchant_url;
/**
* URL of the bank to run history on (set once @e found is set).
*/
char *exchange_url;
/**
* Credit account of the merchant (set once @e found is set).
*/
char *credit_account;
/**
* Payto URI to filter on.
*/
const char *payto_uri;
/**
* Authentication details to authenticate to the bank.
*/
struct TALER_BANK_AuthenticationData auth;
/**
* Set once we discovered the WTID and thus @e found is true.
*/
struct TALER_WireTransferIdentifierRawP wtid;
/**
* the credit amount to look for at @e bank_url.
*/
struct TALER_Amount credit_amount;
/**
* The fee incurred on the wire transfer.
*/
struct TALER_Amount wire_fee;
/**
* Expected HTTP response code.
*/
unsigned int http_status;
/**
* Array of deposit command labels we expect to see aggregated.
*/
const char **deposits;
/**
* Length of @e deposits.
*/
unsigned int deposits_length;
/**
* Set to true once @e wtid and @e exchange_url are initialized.
*/
bool found;
/**
* When the exchange executed the transfer.
*/
struct GNUNET_TIME_Absolute execution_time;
};
/**
* Callback for a POST /transfers operation.
*
* @param cls closure for this function
* @param hr HTTP response details
* @param execution_time when did the transfer happen (according to the exchange),
* #GNUNET_TIME_UNIT_FOREVER_ABS if the transfer did not yet happen or if
* we have no data from the exchange about it
* @param total_amount total amount of the wire transfer, or NULL if the exchange did
* not provide any details
* @param wire_fee how much did the exchange charge in terms of wire fees, or NULL
* if the exchange did not provide any details
* @param details_length length of the @a details array
* @param details array with details about the combined transactions
*/
static void
transfers_cb (void *cls,
const struct TALER_MERCHANT_HttpResponse *hr,
struct GNUNET_TIME_Absolute execution_time,
const struct TALER_Amount *total_amount,
const struct TALER_Amount *wire_fee,
unsigned int details_length,
const struct TALER_MERCHANT_TrackTransferDetail details[])
{
struct PostTransfersState *pts = cls;
pts->pth = NULL;
if (pts->http_status != hr->http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u (%d) to command %s\n",
hr->http_status,
(int) hr->ec,
TALER_TESTING_interpreter_get_current_label (pts->is));
TALER_TESTING_interpreter_fail (pts->is);
return;
}
switch (hr->http_status)
{
case MHD_HTTP_OK:
{
pts->execution_time = execution_time;
pts->wire_fee = *wire_fee;
fprintf (stderr,
"FIXME");
json_dumpf (hr->reply,
stderr,
0);
#if FIXME_WRITE_PROPPER_CHECK_OF_RETURNED_DATA_HERE
/* this code is some legacy logic that is close to what we
need but needs to be updated to the current API */
struct TALER_Amount total;
if (0 >
TALER_amount_subtract (&total,
total_amount,
wire_fee))
{
GNUNET_break (0);
TALER_TESTING_interpreter_fail (pts->is);
return;
}
if (0 !=
TALER_amount_cmp (&total,
&pts->credit_amount))
{
GNUNET_break (0);
TALER_TESTING_interpreter_fail (pts->is);
return;
}
TALER_amount_get_zero (total.currency,
&total);
for (unsigned int i = 0; ideposit_value.currency,
&sum);
TALER_amount_get_zero (tdd->deposit_fee.currency,
&fees);
for (unsigned int j = 0; jdeposits_length; j++)
{
const char *label = pts->deposits[j];
const struct TALER_TESTING_Command *cmd;
const json_t *contract_terms;
const struct TALER_Amount *deposit_value;
const struct TALER_Amount *deposit_fee;
const char *order_id;
cmd = TALER_TESTING_interpreter_lookup_command (pts->is,
label);
if (NULL == cmd)
{
GNUNET_break (0);
TALER_TESTING_interpreter_fail (pts->is);
return;
}
if ( (GNUNET_OK !=
TALER_TESTING_get_trait_contract_terms (cmd,
0,
&contract_terms)) ||
(GNUNET_OK !=
TALER_TESTING_get_trait_amount_obj (cmd,
TALER_TESTING_CMD_DEPOSIT_TRAIT_IDX_DEPOSIT_VALUE,
&deposit_value)) ||
(GNUNET_OK !=
TALER_TESTING_get_trait_amount_obj (cmd,
TALER_TESTING_CMD_DEPOSIT_TRAIT_IDX_DEPOSIT_FEE,
&deposit_fee)) )
{
GNUNET_break (0);
TALER_TESTING_interpreter_fail (pts->is);
return;
}
order_id = json_string_value (json_object_get (contract_terms,
"order_id"));
if (NULL == order_id)
{
GNUNET_break (0);
TALER_TESTING_interpreter_fail (pts->is);
return;
}
if (0 != strcmp (tdd->order_id,
order_id))
continue;
if (0 >
TALER_amount_add (&sum,
&sum,
deposit_value))
{
GNUNET_break (0);
TALER_TESTING_interpreter_fail (pts->is);
return;
}
if (0 >
TALER_amount_add (&fees,
&fees,
deposit_fee))
{
GNUNET_break (0);
TALER_TESTING_interpreter_fail (pts->is);
return;
}
}
if (0 !=
TALER_amount_cmp (&sum,
&tdd->deposit_value))
{
GNUNET_break (0);
TALER_TESTING_interpreter_fail (pts->is);
return;
}
if (0 !=
TALER_amount_cmp (&fees,
&tdd->deposit_fee))
{
GNUNET_break (0);
TALER_TESTING_interpreter_fail (pts->is);
return;
}
GNUNET_assert (0 <=
TALER_amount_add (&total,
&total,
&tdd->deposit_value));
GNUNET_assert (0 <=
TALER_amount_subtract (&total,
&total,
&tdd->deposit_fee));
}
if (0 !=
TALER_amount_cmp (&total,
&pts->credit_amount))
{
GNUNET_break (0);
TALER_TESTING_interpreter_fail (pts->is);
return;
}
#endif
break;
}
default:
GNUNET_break (0);
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Unhandled HTTP status %d.\n",
hr->http_status);
}
TALER_TESTING_interpreter_next (pts->is);
}
/**
* Offers information from the POST /transfers CMD state to other
* commands.
*
* @param cls closure
* @param[out] ret result (could be anything)
* @param trait name of the trait
* @param index index number of the object to extract.
* @return #GNUNET_OK on success
*/
static int
post_transfers_traits (void *cls,
const void **ret,
const char *trait,
unsigned int index)
{
struct PostTransfersState *pts = cls;
struct TALER_TESTING_Trait traits[] = {
TALER_TESTING_make_trait_wtid (0, &pts->wtid),
TALER_TESTING_make_trait_string (0, pts->credit_account),
TALER_TESTING_make_trait_amount_obj (0, &pts->credit_amount),
TALER_TESTING_make_trait_amount_obj (1, &pts->wire_fee),
TALER_TESTING_make_trait_string (1, pts->exchange_url),
TALER_TESTING_make_trait_absolute_time (0, &pts->execution_time),
TALER_TESTING_trait_end (),
};
return TALER_TESTING_get_trait (traits,
ret,
trait,
index);
}
/**
* Callbacks of this type are used to serve the result of asking
* the bank for the debit transaction history.
*
* @param cls closure with a `struct PostTransfersState *`
* @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request
* 0 if the bank's reply is bogus (fails to follow the protocol),
* #MHD_HTTP_NO_CONTENT if there are no more results; on success the
* last callback is always of this status (even if `abs(num_results)` were
* already returned).
* @param ec detailed error code
* @param serial_id monotonically increasing counter corresponding to the transaction
* @param details details about the wire transfer
* @param json detailed response from the HTTPD, or NULL if reply was not in JSON
* @return #GNUNET_OK to continue, #GNUNET_SYSERR to abort iteration
*/
static int
debit_cb (
void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
uint64_t serial_id,
const struct TALER_BANK_DebitDetails *details,
const json_t *json)
{
struct PostTransfersState *pts = cls;
if (MHD_HTTP_NO_CONTENT == http_status)
{
pts->dhh = NULL;
if (! pts->found)
{
GNUNET_break (0);
TALER_TESTING_interpreter_fail (pts->is);
return GNUNET_OK;
}
GNUNET_assert (NULL != pts->exchange_url);
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Bank transfer found, checking with merchant backend at %s about %s from %s to %s with %s\n",
pts->merchant_url,
TALER_amount2s (&pts->credit_amount),
pts->payto_uri,
pts->exchange_url,
TALER_B2S (&pts->wtid));
pts->pth = TALER_MERCHANT_transfers_post (pts->is->ctx,
pts->merchant_url,
&pts->credit_amount,
&pts->wtid,
pts->credit_account,
pts->exchange_url,
&transfers_cb,
pts);
return GNUNET_OK;
}
if (MHD_HTTP_OK != http_status)
{
GNUNET_break (0);
TALER_TESTING_interpreter_fail (pts->is);
pts->dhh = NULL;
return GNUNET_SYSERR;
}
if (pts->found)
return GNUNET_OK;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Bank reports transfer of %s to %s\n",
TALER_amount2s (&details->amount),
details->credit_account_url);
if (0 != TALER_amount_cmp (&pts->credit_amount,
&details->amount))
return GNUNET_OK;
pts->found = true;
pts->wtid = details->wtid;
pts->credit_account = GNUNET_strdup (details->credit_account_url);
pts->exchange_url = GNUNET_strdup (details->exchange_base_url);
return GNUNET_OK;
}
/**
* Run the "POST /transfers" CMD. First, get the bank history to find
* the wtid.
*
* @param cls closure.
* @param cmd command being run now.
* @param is interpreter state.
*/
static void
post_transfers_run (void *cls,
const struct TALER_TESTING_Command *cmd,
struct TALER_TESTING_Interpreter *is)
{
struct PostTransfersState *pts = cls;
pts->is = is;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Looking for transfer of %s from %s at bank\n",
TALER_amount2s (&pts->credit_amount),
pts->payto_uri);
pts->dhh = TALER_BANK_debit_history (is->ctx,
&pts->auth,
UINT64_MAX,
-INT64_MAX,
&debit_cb,
pts);
GNUNET_assert (NULL != pts->dhh);
}
/**
* Free the state of a "POST product" CMD, and possibly
* cancel a pending operation thereof.
*
* @param cls closure.
* @param cmd command being run.
*/
static void
post_transfers_cleanup (void *cls,
const struct TALER_TESTING_Command *cmd)
{
struct PostTransfersState *pts = cls;
if (NULL != pts->pth)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"POST /transfers operation did not complete\n");
TALER_MERCHANT_transfers_post_cancel (pts->pth);
}
if (NULL != pts->dhh)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"GET debit history operation did not complete\n");
TALER_BANK_debit_history_cancel (pts->dhh);
}
GNUNET_array_grow (pts->deposits,
pts->deposits_length,
0);
GNUNET_free (pts->exchange_url);
GNUNET_free (pts->credit_account);
GNUNET_free (pts);
}
struct TALER_TESTING_Command
TALER_TESTING_cmd_merchant_post_transfer (
const char *label,
const struct TALER_BANK_AuthenticationData *auth,
const char *payto_uri,
const char *merchant_url,
const char *credit_amount,
unsigned int http_code,
...)
{
struct PostTransfersState *pts;
pts = GNUNET_new (struct PostTransfersState);
pts->merchant_url = merchant_url;
pts->auth = *auth;
pts->payto_uri = payto_uri;
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (credit_amount,
&pts->credit_amount));
pts->http_status = http_code;
{
const char *clabel;
va_list ap;
va_start (ap, http_code);
while (NULL != (clabel = va_arg (ap, const char *)))
{
GNUNET_array_append (pts->deposits,
pts->deposits_length,
clabel);
}
va_end (ap);
}
{
struct TALER_TESTING_Command cmd = {
.cls = pts,
.label = label,
.run = &post_transfers_run,
.cleanup = &post_transfers_cleanup,
.traits = &post_transfers_traits
};
return cmd;
}
}
/* end of testing_api_cmd_post_transfers.c */