/* This file is part of TALER Copyright (C) 2018-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 bank-lib/testing_api_cmd_history.c * @brief command to check the /history API from the bank. * @author Marcello Stanisci */ #include "platform.h" #include "taler_json_lib.h" #include #include "taler_exchange_service.h" #include "taler_testing_lib.h" #include "taler_testing_bank_lib.h" #include "taler_fakebank_lib.h" #include "taler_bank_service.h" #include "taler_fakebank_lib.h" /** * State for a "history" CMD. */ struct HistoryState { /** * Base URL of the account offering the "history" operation. */ char *account_url; /** * Reference to command defining the * first row number we want in the result. */ const char *start_row_reference; /** * How many rows we want in the result, _at most_, * and ascending/descending. */ long long num_results; /** * Handle to a pending "history" operation. */ struct TALER_BANK_CreditHistoryHandle *hh; /** * Expected number of results (= rows). */ uint64_t results_obtained; /** * Set to GNUNET_YES if the callback detects something * unexpected. */ int failed; }; /** * Item in the transaction history, as reconstructed from the * command history. */ struct History { /** * Wire details. */ struct TALER_BANK_CreditDetails details; /** * Serial ID of the wire transfer. */ uint64_t row_id; /** * URL to free. */ char *url; }; /** * Offer internal data to other commands. * * @param cls closure. * @param ret[out] set to the wanted data. * @param trait name of the trait. * @param index index number of the traits to be returned. * * @return #GNUNET_OK on success */ static int history_traits (void *cls, const void **ret, const char *trait, unsigned int index) { (void) cls; (void) ret; (void) trait; (void) index; /* Must define this function because some callbacks * look for certain traits on _all_ the commands. */ return GNUNET_SYSERR; } /** * Free history @a h of length @a h_len. * * @param h history array to free. * @param h_len number of entries in @a h. */ static void free_history (struct History *h, uint64_t h_len) { for (uint64_t off = 0; off= hs->num_results; } /** * This function constructs the list of history elements that * interest the account number of the caller. It has two main * loops: the first to figure out how many history elements have * to be allocated, and the second to actually populate every * element. * * @param is interpreter state (supposedly having the * current CMD pointing at a "history" CMD). * @param[out] rh history array to initialize. * * @return number of entries in @a rh. */ static uint64_t build_history (struct TALER_TESTING_Interpreter *is, struct History **rh) { struct HistoryState *hs = is->commands[is->ip].cls; uint64_t total; struct History *h; const struct TALER_TESTING_Command *add_incoming_cmd; int inc; unsigned int start; unsigned int end; /** * @var turns GNUNET_YES whenever either no 'start' value was * given for the history query, or the given value is found * in the list of all the CMDs. */int ok; const uint64_t *row_id_start = NULL; if (NULL != hs->start_row_reference) { TALER_LOG_INFO ("`%s': start row given via reference `%s'\n", TALER_TESTING_interpreter_get_current_label (is), hs->start_row_reference); add_incoming_cmd = TALER_TESTING_interpreter_lookup_command (is, hs->start_row_reference); GNUNET_assert (NULL != add_incoming_cmd); GNUNET_assert (GNUNET_OK == TALER_TESTING_get_trait_uint64 (add_incoming_cmd, 0, &row_id_start)); } GNUNET_assert (0 != hs->num_results); if (0 == is->ip) { TALER_LOG_DEBUG ("Checking history at first CMD..\n"); *rh = NULL; return 0; } /* AKA 'delta'. */ if (hs->num_results > 0) { inc = 1; /* _inc_rement */ start = 0; end = is->ip - 1; } else { inc = -1; start = is->ip - 1; end = 0; } total = 0; ok = GNUNET_NO; if (NULL == row_id_start) ok = GNUNET_YES; /* This loop counts how many commands _later than "start"_ belong * to the history of the caller. This is stored in the @var total * variable. */ for (unsigned int off = start; off != end + inc; off += inc) { const struct TALER_TESTING_Command *pos = &is->commands[off]; const uint64_t *row_id; const char *credit_account; const char *debit_account; /** * The following command allows us to skip over those CMDs * that do not offer a "row_id" trait. Such skipped CMDs are * not interesting for building a history. */if (GNUNET_OK != TALER_TESTING_get_trait_uint64 (pos, 0, &row_id)) continue; /* Seek "/history" starting row. */ if (NULL != row_id_start) { if (*row_id_start == *row_id) { /* Doesn't count, start is excluded from output. */ total = 0; ok = GNUNET_YES; continue; } } /* when 'start' was _not_ given, then ok == GNUNET_YES */ if (GNUNET_NO == ok) continue; /* skip until we find the marker */ TALER_LOG_DEBUG ("Found first row\n"); if (build_history_hit_limit (total, hs, pos)) { TALER_LOG_DEBUG ("Hit history limit\n"); break; } GNUNET_assert (GNUNET_OK == TALER_TESTING_GET_TRAIT_CREDIT_ACCOUNT (pos, &credit_account)); GNUNET_assert (GNUNET_OK == TALER_TESTING_GET_TRAIT_DEBIT_ACCOUNT (pos, &debit_account)); TALER_LOG_INFO ("Potential history element:" " %s->%s; my account: %s\n", debit_account, credit_account, hs->account_url); if (0 == strcasecmp (hs->account_url, credit_account)) { TALER_LOG_INFO ("+1 my history\n"); total++; /* found matching record */ } } GNUNET_assert (GNUNET_YES == ok); if (0 == total) { TALER_LOG_DEBUG ("Checking history at first CMD.. (2)\n"); *rh = NULL; return 0; } GNUNET_assert (total < UINT_MAX); h = GNUNET_new_array ((unsigned int) total, struct History); total = 0; ok = GNUNET_NO; if (NULL == row_id_start) ok = GNUNET_YES; /** * This loop _only_ populates the array of history elements. */ for (unsigned int off = start; off != end + inc; off += inc) { const struct TALER_TESTING_Command *pos = &is->commands[off]; const uint64_t *row_id; char *bank_hostname; const char *credit_account; const char *debit_account; if (GNUNET_OK != TALER_TESTING_GET_TRAIT_ROW_ID (pos, &row_id)) continue; if (NULL != row_id_start) { if (*row_id_start == *row_id) { /** * Warning: this zeroing is superfluous, as * total doesn't get incremented if 'start' * was given and couldn't be found. */total = 0; ok = GNUNET_YES; continue; } } TALER_LOG_INFO ("Found first row (2)\n"); if (GNUNET_NO == ok) { TALER_LOG_INFO ("Skip on `%s'\n", pos->label); continue; /* skip until we find the marker */ } if (build_history_hit_limit (total, hs, pos)) { TALER_LOG_INFO ("Hit history limit (2)\n"); break; } GNUNET_assert (GNUNET_OK == TALER_TESTING_GET_TRAIT_CREDIT_ACCOUNT (pos, &credit_account)); GNUNET_assert (GNUNET_OK == TALER_TESTING_GET_TRAIT_DEBIT_ACCOUNT (pos, &debit_account)); TALER_LOG_INFO ("Potential history bit:" " %s->%s; my account: %s\n", debit_account, credit_account, hs->account_url); /** * Discard transactions where the audited account played * _both_ the credit and the debit roles, but _only if_ * the audit goes on both directions.. This needs more * explaination! */if (0 == strcasecmp (hs->account_url, credit_account)) { GNUNET_break (0); continue; } bank_hostname = strchr (hs->account_url, ':'); GNUNET_assert (NULL != bank_hostname); bank_hostname += 3; /* Next two blocks only put the 'direction' and 'banking' * information. */ /* Asked for credit, and account got the credit. */ if (0 == strcasecmp (hs->account_url, credit_account)) { h[total].url = GNUNET_strdup (debit_account); h[total].details.account_url = h[total].url; } /* This block _completes_ the information of the current item, * with amount / subject / exchange URL. */ if (0 == strcasecmp (hs->account_url, credit_account)) { const struct TALER_Amount *amount; const struct TALER_ReservePublicKeyP *reserve_pub; const char *account_url; GNUNET_assert (GNUNET_OK == TALER_TESTING_get_trait_amount_obj (pos, 0, &amount)); GNUNET_assert (GNUNET_OK == TALER_TESTING_get_trait_reserve_pub (pos, 0, &reserve_pub)); GNUNET_assert (GNUNET_OK == TALER_TESTING_get_trait_url (pos, 1, &account_url)); h[total].details.amount = *amount; h[total].row_id = *row_id; h[total].details.reserve_pub = *reserve_pub; h[total].details.account_url = account_url; TALER_LOG_INFO ("+1-bit of my history\n"); total++; } } *rh = h; return total; } /** * Compute how many results we expect to be returned for * the current command at @a is. * * @param is the interpreter state to inspect. * @return number of results expected. */ static uint64_t compute_result_count (struct TALER_TESTING_Interpreter *is) { uint64_t total; struct History *h; total = build_history (is, &h); free_history (h, total); return total; } /** * Check that the "/history" response matches the * CMD whose offset in the list of CMDs is @a off. * * @param is the interpreter state. * @param off the offset (of the CMD list) where the command * to check is. * @param dir the expected direction of the transaction. * @param details the expected transaction details. * * @return #GNUNET_OK if the transaction is what we expect. */ static int check_result (struct TALER_TESTING_Interpreter *is, unsigned int off, const struct TALER_BANK_CreditDetails *details) { uint64_t total; struct History *h; total = build_history (is, &h); if (off >= total) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Test says history has at most %u" " results, but got result #%u to check\n", (unsigned int) total, off); print_expected (h, total, off); return GNUNET_SYSERR; } if ( (0 != GNUNET_memcmp (&h[off].details.reserve_pub, &details->reserve_pub)) || (0 != TALER_amount_cmp (&h[off].details.amount, &details->amount)) || (0 != strcasecmp (h[off].details.account_url, details->account_url)) ) { GNUNET_break (0); print_expected (h, total, off); free_history (h, total); return GNUNET_SYSERR; } free_history (h, total); return GNUNET_OK; } /** * This callback will (1) check that the HTTP response code * is acceptable and (2) that the history is consistent. The * consistency is checked by going through all the past CMDs, * reconstructing then the expected history as of those, and * finally check it against what the bank returned. * * @param cls closure. * @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 taler status code. * @param dir direction of the transfer. * @param row_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 history_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, uint64_t row_id, const struct TALER_BANK_CreditDetails *details, const json_t *json) { struct TALER_TESTING_Interpreter *is = cls; struct HistoryState *hs = is->commands[is->ip].cls; (void) row_id; if (MHD_HTTP_OK != http_status) { hs->hh = NULL; GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unwanted response code from /history: %u\n", http_status); TALER_TESTING_interpreter_fail (is); return GNUNET_SYSERR; } if (NULL == details) { hs->hh = NULL; if ( (hs->results_obtained != compute_result_count (is)) || (GNUNET_YES == hs->failed) ) { uint64_t total; struct History *h; GNUNET_break (0); total = build_history (is, &h); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Expected history of length %llu, got %llu;" " HTTP status code: %u/%d, failed: %d\n", (unsigned long long) total, (unsigned long long) hs->results_obtained, http_status, (int) ec, hs->failed); print_expected (h, total, UINT_MAX); free_history (h, total); TALER_TESTING_interpreter_fail (is); return GNUNET_SYSERR; } TALER_TESTING_interpreter_next (is); return GNUNET_OK; } /* check current element */ if (GNUNET_OK != check_result (is, hs->results_obtained, details)) { char *acc; GNUNET_break (0); acc = json_dumps (json, JSON_COMPACT); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Result %u was `%s'\n", (unsigned int) hs->results_obtained++, acc); if (NULL != acc) free (acc); hs->failed = GNUNET_YES; return GNUNET_SYSERR; } hs->results_obtained++; return GNUNET_OK; } /** * Run the command. * * @param cls closure. * @param cmd the command to execute. * @param is the interpreter state. */ static void history_run (void *cls, const struct TALER_TESTING_Command *cmd, struct TALER_TESTING_Interpreter *is) { struct HistoryState *hs = cls; uint64_t row_id = (hs->num_results > 0) ? 0 : UINT64_MAX; const uint64_t *row_ptr; (void) cmd; /* Get row_id from trait. */ if (NULL != hs->start_row_reference) { const struct TALER_TESTING_Command *history_cmd; history_cmd = TALER_TESTING_interpreter_lookup_command (is, hs->start_row_reference); if (NULL == history_cmd) TALER_TESTING_FAIL (is); if (GNUNET_OK != TALER_TESTING_get_trait_uint64 (history_cmd, 0, &row_ptr)) TALER_TESTING_FAIL (is); else row_id = *row_ptr; TALER_LOG_DEBUG ("row id (from trait) is %llu\n", (unsigned long long) row_id); } hs->hh = TALER_BANK_credit_history (is->ctx, hs->account_url, NULL, row_id, hs->num_results, &history_cb, is); GNUNET_assert (NULL != hs->hh); } /** * Free the state from a "history" CMD, and possibly cancel * a pending operation thereof. * * @param cls closure. * @param cmd the command which is being cleaned up. */ static void history_cleanup (void *cls, const struct TALER_TESTING_Command *cmd) { struct HistoryState *hs = cls; (void) cmd; if (NULL != hs->hh) { TALER_LOG_WARNING ("/history did not complete\n"); TALER_BANK_credit_history_cancel (hs->hh); } GNUNET_free (hs->account_url); GNUNET_free (hs); } /** * Make a "history" CMD. * * @param label command label. * @param account_url base URL of the account offering the "history" * operation. * @param start_row_reference reference to a command that can * offer a row identifier, to be used as the starting row * to accept in the result. * @param num_results how many rows we want in the result. * @return the command. */ struct TALER_TESTING_Command TALER_TESTING_cmd_bank_credits (const char *label, const char *account_url, const char *start_row_reference, long long num_results) { struct HistoryState *hs; hs = GNUNET_new (struct HistoryState); hs->account_url = GNUNET_strdup (account_url); hs->start_row_reference = start_row_reference; hs->num_results = num_results; { struct TALER_TESTING_Command cmd = { .label = label, .cls = hs, .run = &history_run, .cleanup = &history_cleanup, .traits = &history_traits }; return cmd; } } /* end of testing_api_cmd_credit_history.c */