/* 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; /** * Serial number of the wire transfer in the merchant backend, * set by #TALER_TESTING_cmd_merchant_get_transfers(). 0 if unknown. */ uint64_t serial; /** * 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; } case MHD_HTTP_ACCEPTED: break; case MHD_HTTP_UNAUTHORIZED: break; case MHD_HTTP_NOT_FOUND: break; case MHD_HTTP_GATEWAY_TIMEOUT: break; default: GNUNET_break (0); GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Unhandled HTTP status %u for POST /transfers.\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_make_trait_bank_row (&pts->serial), TALER_TESTING_trait_end (), }; return TALER_TESTING_get_trait (traits, ret, trait, index); } /** * 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_run2 (void *cls, const struct TALER_TESTING_Command *cmd, struct TALER_TESTING_Interpreter *is) { struct PostTransfersState *pts = cls; pts->is = is; 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); GNUNET_assert (NULL != pts->pth); } /** * 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); GNUNET_assert (NULL != pts->pth); 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; } } struct TALER_TESTING_Command TALER_TESTING_cmd_merchant_post_transfer2 ( const char *label, const char *merchant_url, const char *payto_uri, const char *credit_amount, const char *wtid, const char *exchange_url, unsigned int http_code) { struct PostTransfersState *pts; pts = GNUNET_new (struct PostTransfersState); pts->merchant_url = merchant_url; pts->credit_account = GNUNET_strdup (payto_uri); pts->exchange_url = GNUNET_strdup (exchange_url); GNUNET_assert (GNUNET_OK == TALER_string_to_amount (credit_amount, &pts->credit_amount)); if (NULL == wtid) { GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, &pts->wtid, sizeof (pts->wtid)); } else { GNUNET_assert (GNUNET_OK == GNUNET_STRINGS_string_to_data (wtid, strlen (wtid), &pts->wtid, sizeof (pts->wtid))); } pts->http_status = http_code; { struct TALER_TESTING_Command cmd = { .cls = pts, .label = label, .run = &post_transfers_run2, .cleanup = &post_transfers_cleanup, .traits = &post_transfers_traits }; return cmd; } } void TALER_TESTING_cmd_merchant_post_transfer_set_serial ( struct TALER_TESTING_Command *cmd, uint64_t serial) { struct PostTransfersState *pts = cmd->cls; GNUNET_assert (cmd->run = &post_transfers_run); pts->serial = serial; } /* end of testing_api_cmd_post_transfers.c */