/* This file is part of TALER Copyright (C) 2016, 2017 GNUnet e.V. 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/test_bank_interpreter.c * @brief interpreter for tests of the bank's HTTP API interface * @author Christian Grothoff */ #include "platform.h" #include "taler_util.h" #include "taler_signatures.h" #include "taler_bank_service.h" #include #include #include #include "test_bank_interpreter.h" #include "taler_fakebank_lib.h" /** * State of the interpreter loop. */ struct InterpreterState { /** * Keys from the bank. */ const struct TALER_BANK_Keys *keys; /** * Commands the interpreter will run. */ struct TBI_Command *commands; /** * Interpreter task (if one is scheduled). */ struct GNUNET_SCHEDULER_Task *task; /** * Main execution context for the main loop. */ struct GNUNET_CURL_Context *ctx; /** * Task run on timeout. */ struct GNUNET_SCHEDULER_Task *timeout_task; /** * Context for running the main loop with GNUnet's SCHEDULER API. */ struct GNUNET_CURL_RescheduleContext *rc; /** * Where to store the final result. */ int *resultp; /** * Fakebank, or NULL if we are not using the fakebank. */ struct TALER_FAKEBANK_Handle *fakebank; /** * Instruction pointer. Tells #interpreter_run() which * instruction to run next. */ unsigned int ip; }; /** * The testcase failed, return with an error code. * * @param is interpreter state to clean up */ static void fail (struct InterpreterState *is) { *is->resultp = GNUNET_SYSERR; GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Interpreter failed at command `%s'\n", is->commands[is->ip].label); GNUNET_SCHEDULER_shutdown (); } /** * Find a command by label. * * @param is interpreter state to search * @param label label to look for * @return NULL if command was not found */ static const struct TBI_Command * find_command (const struct InterpreterState *is, const char *label) { unsigned int i; const struct TBI_Command *cmd; if (NULL == label) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Attempt to lookup command for empty label\n"); return NULL; } for (i=0;TBI_OC_END != (cmd = &is->commands[i])->oc;i++) if ( (NULL != cmd->label) && (0 == strcmp (cmd->label, label)) ) return cmd; GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command not found: %s\n", label); return NULL; } /** * Item in the transaction history, as reconstructed from the * command history. */ struct History { /** * Wire details. */ struct TALER_BANK_TransferDetails details; /** * Serial ID of the wire transfer. */ uint64_t serial_id; /** * Direction of the transfer. */ enum TALER_BANK_Direction direction; }; /** * Build history of transactions matching the current * command in @a is. * * @param is interpreter state * @param[out] rh history array to initialize * @return number of entries in @a rh */ static uint64_t build_history (struct InterpreterState *is, struct History **rh) { const struct TBI_Command *cmd = &is->commands[is->ip]; uint64_t total; struct History *h; const struct TBI_Command *ref; int inc; unsigned int start; unsigned int end; int ok; GNUNET_assert (TBI_OC_HISTORY == cmd->oc); if (NULL != cmd->details.history.start_row_ref) { ref = find_command (is, cmd->details.history.start_row_ref); GNUNET_assert (NULL != ref); } else { ref = NULL; } GNUNET_assert (0 != cmd->details.history.num_results); if (0 == is->ip) { *rh = NULL; return 0; } if (cmd->details.history.num_results > 0) { inc = 1; start = 0; end = is->ip - 1; } else { inc = -1; start = is->ip - 1; end = 0; } total = 0; ok = GNUNET_NO; if (NULL == ref) ok = GNUNET_YES; for (unsigned int off = start;off != end + inc; off += inc) { const struct TBI_Command *pos = &is->commands[off]; if (TBI_OC_ADMIN_ADD_INCOMING != pos->oc) continue; if ( (NULL != ref) && (ref->details.admin_add_incoming.serial_id == pos->details.admin_add_incoming.serial_id) ) { total = 0; ok = GNUNET_YES; continue; } if (GNUNET_NO == ok) continue; /* skip until we find the marker */ if (total >= cmd->details.history.num_results * inc) break; /* hit limit specified by command */ if ( ( (0 != (cmd->details.history.direction & TALER_BANK_DIRECTION_CREDIT)) && (cmd->details.history.account_number == pos->details.admin_add_incoming.credit_account_no)) || ( (0 != (cmd->details.history.direction & TALER_BANK_DIRECTION_DEBIT)) && (cmd->details.history.account_number == pos->details.admin_add_incoming.debit_account_no)) ) total++; /* found matching record */ } GNUNET_assert (GNUNET_YES == ok); if (0 == total) { *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 == ref) ok = GNUNET_YES; for (unsigned int off = start;off != end + inc; off += inc) { const struct TBI_Command *pos = &is->commands[off]; if (TBI_OC_ADMIN_ADD_INCOMING != pos->oc) continue; if ( (NULL != ref) && (ref->details.admin_add_incoming.serial_id == pos->details.admin_add_incoming.serial_id) ) { total = 0; ok = GNUNET_YES; continue; } if (GNUNET_NO == ok) continue; /* skip until we find the marker */ if (total >= cmd->details.history.num_results * inc) break; /* hit limit specified by command */ if ( ( (0 != (cmd->details.history.direction & TALER_BANK_DIRECTION_CREDIT)) && (cmd->details.history.account_number == pos->details.admin_add_incoming.credit_account_no)) && ( (0 != (cmd->details.history.direction & TALER_BANK_DIRECTION_DEBIT)) && (cmd->details.history.account_number == pos->details.admin_add_incoming.debit_account_no)) ) { GNUNET_break (0); continue; } if ( (0 != (cmd->details.history.direction & TALER_BANK_DIRECTION_CREDIT)) && (cmd->details.history.account_number == pos->details.admin_add_incoming.credit_account_no)) { h[total].direction = TALER_BANK_DIRECTION_CREDIT; h[total].details.account_details = json_pack ("{s:s, s:s, s:I}", "type", "test", "bank_uri", "http://localhost:8080", "account_number", (json_int_t) pos->details.admin_add_incoming.debit_account_no); GNUNET_assert (NULL != h[total].details.account_details); } if ( (0 != (cmd->details.history.direction & TALER_BANK_DIRECTION_DEBIT)) && (cmd->details.history.account_number == pos->details.admin_add_incoming.debit_account_no)) { h[total].direction = TALER_BANK_DIRECTION_DEBIT; h[total].details.account_details = json_pack ("{s:s, s:s, s:I}", "type", "test", "bank_uri", "http://localhost:8080", "account_number", (json_int_t) pos->details.admin_add_incoming.credit_account_no); GNUNET_assert (NULL != h[total].details.account_details); } if ( ( (0 != (cmd->details.history.direction & TALER_BANK_DIRECTION_CREDIT)) && (cmd->details.history.account_number == pos->details.admin_add_incoming.credit_account_no)) || ( (0 != (cmd->details.history.direction & TALER_BANK_DIRECTION_DEBIT)) && (cmd->details.history.account_number == pos->details.admin_add_incoming.debit_account_no)) ) { GNUNET_assert (GNUNET_OK == TALER_string_to_amount (pos->details.admin_add_incoming.amount, &h[total].details.amount)); /* h[total].execution_date; // unknown here */ h[total].serial_id = pos->details.admin_add_incoming.serial_id; { char *ws; ws = GNUNET_STRINGS_data_to_string_alloc (&pos->details.admin_add_incoming.wtid, sizeof (struct TALER_WireTransferIdentifierRawP)); GNUNET_asprintf (&h[total].details.wire_transfer_subject, "%s %s", ws, pos->details.admin_add_incoming.exchange_base_url); GNUNET_free (ws); } total++; } } *rh = h; return total; } /** * Log which history we expected. * * @param h what we expected * @param h_len number of entries in @a h * @param off position of the missmatch */ static void print_expected (struct History *h, uint64_t h_len, unsigned int off) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Transaction history missmatch at position %u/%llu\n", off, (unsigned long long) h_len); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Expected history:\n"); for (uint64_t i=0;i= 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 (h[off].direction != dir) { GNUNET_break (0); print_expected (h, total, off); free_history (h, total); return GNUNET_SYSERR; } if ( (0 != strcmp (h[off].details.wire_transfer_subject, details->wire_transfer_subject)) || (0 != TALER_amount_cmp (&h[off].details.amount, &details->amount)) || (1 != json_equal (h[off].details.account_details, details->account_details)) ) { GNUNET_break (0); print_expected (h, total, off); free_history (h, total); return GNUNET_SYSERR; } free_history (h, total); return GNUNET_OK; } /** * Run the main interpreter loop that performs bank operations. * * @param cls contains the `struct InterpreterState` */ static void interpreter_run (void *cls); /** * Function called upon completion of our /admin/add/incoming request. * * @param cls closure with the interpreter state * @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) * @param serial_id unique ID of the wire transfer in the bank's records; UINT64_MAX on error * @param json detailed response from the HTTPD, or NULL if reply was not in JSON */ static void add_incoming_cb (void *cls, unsigned int http_status, uint64_t serial_id, const json_t *json) { struct InterpreterState *is = cls; struct TBI_Command *cmd = &is->commands[is->ip]; cmd->details.admin_add_incoming.aih = NULL; cmd->details.admin_add_incoming.serial_id = serial_id; if (cmd->details.admin_add_incoming.expected_response_code != http_status) { GNUNET_break (0); fprintf (stderr, "Unexpected response code %u:\n", http_status); if (NULL != json) { json_dumpf (json, stderr, 0); fprintf (stderr, "\n"); } fail (is); return; } is->ip++; is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, is); } /** * Callbacks of this type are used to serve the result of asking * the bank for the transaction history. * * @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 dir direction of the transfer * @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 */ static void history_cb (void *cls, unsigned int http_status, enum TALER_BANK_Direction dir, uint64_t serial_id, const struct TALER_BANK_TransferDetails *details, const json_t *json) { struct InterpreterState *is = cls; struct TBI_Command *cmd = &is->commands[is->ip]; if (MHD_HTTP_OK != http_status) { cmd->details.history.hh = NULL; if ( (cmd->details.history.results_obtained != compute_result_count (is)) || (GNUNET_YES == cmd->details.history.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\n", (unsigned long long) total, (unsigned long long) cmd->details.history.results_obtained); print_expected (h, total, UINT_MAX); free_history (h, total); fail (is); return; } is->ip++; is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, is); return; } if (GNUNET_OK != check_result (is, cmd->details.history.results_obtained, dir, details)) { GNUNET_break (0); { char *acc; acc = json_dumps (json, JSON_COMPACT); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Result %u was `%s'\n", (unsigned int) cmd->details.history.results_obtained, acc); if (NULL != acc) free (acc); } cmd->details.history.failed = GNUNET_YES; return; } cmd->details.history.results_obtained++; } /** * Run the main interpreter loop that performs bank operations. * * @param cls contains the `struct InterpreterState` */ static void interpreter_run (void *cls) { struct InterpreterState *is = cls; struct TBI_Command *cmd = &is->commands[is->ip]; const struct TBI_Command *ref; struct TALER_Amount amount; const struct GNUNET_SCHEDULER_TaskContext *tc; struct TALER_BANK_AuthenticationData auth; uint64_t rowid; is->task = NULL; tc = GNUNET_SCHEDULER_get_task_context (); if (0 != (tc->reason & GNUNET_SCHEDULER_REASON_SHUTDOWN)) { fprintf (stderr, "Test aborted by shutdown request\n"); fail (is); return; } auth.method = TALER_BANK_AUTH_BASIC; /* or "NONE"? */ auth.details.basic.username = "Exchange"; auth.details.basic.password = "x"; switch (cmd->oc) { case TBI_OC_END: *is->resultp = GNUNET_OK; GNUNET_SCHEDULER_shutdown (); return; case TBI_OC_ADMIN_ADD_INCOMING: if (GNUNET_OK != TALER_string_to_amount (cmd->details.admin_add_incoming.amount, &amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at %u\n", cmd->details.admin_add_incoming.amount, is->ip); fail (is); return; } GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, &cmd->details.admin_add_incoming.wtid, sizeof (cmd->details.admin_add_incoming.wtid)); cmd->details.admin_add_incoming.aih = TALER_BANK_admin_add_incoming (is->ctx, "http://localhost:8080", &auth, cmd->details.admin_add_incoming.exchange_base_url, &cmd->details.admin_add_incoming.wtid, &amount, cmd->details.admin_add_incoming.debit_account_no, cmd->details.admin_add_incoming.credit_account_no, &add_incoming_cb, is); if (NULL == cmd->details.admin_add_incoming.aih) { GNUNET_break (0); fail (is); return; } return; case TBI_OC_HISTORY: if (NULL != cmd->details.history.start_row_ref) { /*In case history is to be found from some other commad's output, like from /admin/add/incoming*/ ref = find_command (is, cmd->details.history.start_row_ref); GNUNET_assert (NULL != ref); } else { ref = NULL; } if (NULL != ref) rowid = ref->details.admin_add_incoming.serial_id; else rowid = UINT64_MAX; cmd->details.history.hh = TALER_BANK_history (is->ctx, "http://localhost:8080", &auth, cmd->details.history.account_number, cmd->details.history.direction, rowid, cmd->details.history.num_results, &history_cb, is); if (NULL == cmd->details.history.hh) { GNUNET_break (0); fail (is); return; } return; case TBI_OC_EXPECT_TRANSFER: ref = find_command (is, cmd->details.expect_transfer.cmd_ref); GNUNET_assert (NULL != ref); GNUNET_assert (GNUNET_OK == TALER_string_to_amount (ref->details.admin_add_incoming.amount, &amount)); { char *subject; char *expect; if (GNUNET_OK != TALER_FAKEBANK_check (is->fakebank, &amount, ref->details.admin_add_incoming.debit_account_no, ref->details.admin_add_incoming.credit_account_no, ref->details.admin_add_incoming.exchange_base_url, &subject)) { GNUNET_break (0); fail (is); return; } expect = GNUNET_STRINGS_data_to_string_alloc (&ref->details.admin_add_incoming.wtid, sizeof (ref->details.admin_add_incoming.wtid)); if (0 != strcmp (subject, expect)) { GNUNET_free (expect); GNUNET_free (subject); GNUNET_break (0); fail (is); return; } GNUNET_free (subject); GNUNET_free (expect); } is->ip++; is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, is); return; case TBI_OC_EXPECT_TRANSFERS_EMPTY: if (GNUNET_OK != TALER_FAKEBANK_check_empty (is->fakebank)) { GNUNET_break (0); fail (is); return; } is->ip++; is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, is); return; default: GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unknown instruction %d at %u (%s)\n", cmd->oc, is->ip, cmd->label); fail (is); return; } } /** * Function run on timeout. * * @param cls the `struct InterpreterState` */ static void do_timeout (void *cls) { struct InterpreterState *is = cls; is->timeout_task = NULL; GNUNET_SCHEDULER_shutdown (); } /** * Function run when the test terminates (good or bad). * Cleans up our state. * * @param cls the interpreter state. */ static void do_shutdown (void *cls) { struct InterpreterState *is = cls; struct TBI_Command *cmd; unsigned int i; if (NULL != is->timeout_task) { GNUNET_SCHEDULER_cancel (is->timeout_task); is->timeout_task = NULL; } for (i=0;TBI_OC_END != (cmd = &is->commands[i])->oc;i++) { switch (cmd->oc) { case TBI_OC_END: GNUNET_assert (0); break; case TBI_OC_ADMIN_ADD_INCOMING: if (NULL != cmd->details.admin_add_incoming.aih) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_BANK_admin_add_incoming_cancel (cmd->details.admin_add_incoming.aih); cmd->details.admin_add_incoming.aih = NULL; } break; case TBI_OC_HISTORY: if (NULL != cmd->details.history.hh) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_BANK_history_cancel (cmd->details.history.hh); cmd->details.history.hh = NULL; } break; case TBI_OC_EXPECT_TRANSFER: break; case TBI_OC_EXPECT_TRANSFERS_EMPTY: break; default: GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unknown instruction %d at %u (%s)\n", cmd->oc, i, cmd->label); break; } } if (NULL != is->task) { GNUNET_SCHEDULER_cancel (is->task); is->task = NULL; } if (NULL != is->fakebank) { TALER_FAKEBANK_stop (is->fakebank); is->fakebank = NULL; } GNUNET_CURL_fini (is->ctx); is->ctx = NULL; GNUNET_CURL_gnunet_rc_destroy (is->rc); GNUNET_free (is); } /** * Entry point to the interpeter. * * @param resultp where to store the final result * @param run_bank #GNUNET_YES to run the fakebank * @param commands list of commands to run */ void TBI_run_interpreter (int *resultp, int run_bank, struct TBI_Command *commands) { struct InterpreterState *is; is = GNUNET_new (struct InterpreterState); if (GNUNET_YES == run_bank) is->fakebank = TALER_FAKEBANK_start (8080); is->resultp = resultp; is->commands = commands; is->ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, &is->rc); GNUNET_assert (NULL != is->ctx); is->rc = GNUNET_CURL_gnunet_rc_create (is->ctx); is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, is); is->timeout_task = GNUNET_SCHEDULER_add_delayed (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 150), &do_timeout, is); GNUNET_SCHEDULER_add_shutdown (&do_shutdown, is); } /* end of test_bank_interpeter.c */