/*
This file is part of TALER
Copyright (C) 2014, 2015, 2016 GNUnet e.V. and INRIA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free Software
Foundation; either version 2.1, 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 Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with
TALER; see the file COPYING.LGPL. If not, see
*/
/**
* @file merchant/test_merchant_api.c
* @brief testcase to test merchant's HTTP API interface
* @author Christian Grothoff
*/
#include "platform.h"
#include
#include
#include
#include
#include
#include "taler_merchant_service.h"
#include "taler_merchantdb_lib.h"
#include
#include
#include
/**
* Shortcut
*/
#define LOG_INFO(...) GNUNET_log(GNUNET_ERROR_TYPE_INFO, __VA_ARGS__)
/**
* URI under which the merchant is reachable during the testcase.
*/
#define MERCHANT_URI "http://localhost:8082"
/**
* URI under which the exchange is reachable during the testcase.
*/
#define EXCHANGE_URI "http://localhost:8081/"
/**
* URI of the bank.
*/
#define BANK_URI "http://localhost:8083/"
/**
* On which port do we run the (fake) bank?
*/
#define BANK_PORT 8083
/**
* Max size allowed for a contract
*/
#define CONTRACT_MAX_SIZE 500
/**
* Handle to access the exchange.
*/
static struct TALER_EXCHANGE_Handle *exchange;
/**
* Main execution context for the main loop of the exchange.
*/
static struct GNUNET_CURL_Context *ctx;
/**
* Array of receivers to test against
*/
static char **instances;
/**
* How many merchant instances this test runs
*/
unsigned int nreceiver = 0;
/**
* Current receiver
*/
static char *receiver;
/**
* Current receiver being tested
*/
unsigned int receiver_idx = 0;
/**
* Task run on timeout.
*/
static struct GNUNET_SCHEDULER_Task *timeout_task;
/**
* Context for running the #ctx's event loop.
*/
static struct GNUNET_CURL_RescheduleContext *rc;
/**
* Handle to the fake bank service we run for the
* aggregator.
*/
static struct TALER_FAKEBANK_Handle *fakebank;
/**
* Result of the testcases, #GNUNET_OK on success
*/
static int result;
/**
* Opcodes for the interpreter.
*/
enum OpCode
{
/**
* Termination code, stops the interpreter loop (with success).
*/
OC_END = 0,
/**
* Add funds to a reserve by (faking) incoming wire transfer.
*/
OC_ADMIN_ADD_INCOMING,
/**
* Check status of a reserve.
*/
OC_WITHDRAW_STATUS,
/**
* Withdraw a coin from a reserve.
*/
OC_WITHDRAW_SIGN,
/**
* Get backend to sign a contract.
*/
OC_CONTRACT,
/**
* Pay with coins.
*/
OC_PAY,
/**
* Run the aggregator to execute deposits.
*/
OC_RUN_AGGREGATOR,
/**
* Check that the fakebank has received a certain transaction.
*/
OC_CHECK_BANK_TRANSFER,
/**
* Check that the fakebank has not received any other transactions.
*/
OC_CHECK_BANK_TRANSFERS_EMPTY,
/**
* Retrieve deposit details for a given wire transfer
*/
OC_TRACK_TRANSFER,
/**
* Retrieve wire transfer details for a given transaction
*/
OC_TRACK_TRANSACTION,
/**
* Test getting transactions based on timestamp
*/
OC_HISTORY
};
/**
* Structure specifying details about a coin to be melted.
* Used in a NULL-terminated array as part of command
* specification.
*/
struct MeltDetails
{
/**
* Amount to melt (including fee).
*/
const char *amount;
/**
* Reference to reserve_withdraw operations for coin to
* be used for the /refresh/melt operation.
*/
const char *coin_ref;
};
/**
* Information about a fresh coin generated by the refresh operation.
*/
struct FreshCoin
{
/**
* If @e amount is NULL, this specifies the denomination key to
* use. Otherwise, this will be set (by the interpreter) to the
* denomination PK matching @e amount.
*/
const struct TALER_EXCHANGE_DenomPublicKey *pk;
/**
* Set (by the interpreter) to the exchange's signature over the
* coin's public key.
*/
struct TALER_DenominationSignature sig;
/**
* Set (by the interpreter) to the coin's private key.
*/
struct TALER_CoinSpendPrivateKeyP coin_priv;
};
/**
* Details for a exchange operation to execute.
*/
struct Command
{
/**
* Opcode of the command.
*/
enum OpCode oc;
/**
* Label for the command, can be NULL.
*/
const char *label;
/**
* Which response code do we expect for this command?
*/
unsigned int expected_response_code;
/**
* Details about the command.
*/
union
{
/**
* Information for a #OC_ADMIN_ADD_INCOMING command.
*/
struct
{
/**
* Label to another admin_add_incoming command if we
* should deposit into an existing reserve, NULL if
* a fresh reserve should be created.
*/
const char *reserve_reference;
/**
* String describing the amount to add to the reserve.
*/
const char *amount;
/**
* Sender's bank account details (JSON).
*/
const char *sender_details;
/**
* Transfer details (JSON)
*/
const char *transfer_details;
/**
* Set (by the interpreter) to the reserve's private key
* we used to fill the reserve.
*/
struct TALER_ReservePrivateKeyP reserve_priv;
/**
* Set to the API's handle during the operation.
*/
struct TALER_EXCHANGE_AdminAddIncomingHandle *aih;
} admin_add_incoming;
/**
* Information for a #OC_WITHDRAW_STATUS command.
*/
struct
{
/**
* Label to the #OC_ADMIN_ADD_INCOMING command which
* created the reserve.
*/
const char *reserve_reference;
/**
* Set to the API's handle during the operation.
*/
struct TALER_EXCHANGE_ReserveStatusHandle *wsh;
/**
* Expected reserve balance.
*/
const char *expected_balance;
} reserve_status;
/**
* Information for a #OC_WITHDRAW_SIGN command.
*/
struct
{
/**
* Which reserve should we withdraw from?
*/
const char *reserve_reference;
/**
* String describing the denomination value we should withdraw.
* A corresponding denomination key must exist in the exchange's
* offerings. Can be NULL if @e pk is set instead.
*/
const char *amount;
/**
* If @e amount is NULL, this specifies the denomination key to
* use. Otherwise, this will be set (by the interpreter) to the
* denomination PK matching @e amount.
*/
const struct TALER_EXCHANGE_DenomPublicKey *pk;
/**
* Set (by the interpreter) to the exchange's signature over the
* coin's public key.
*/
struct TALER_DenominationSignature sig;
/**
* Set (by the interpreter) to the coin's private key.
*/
struct TALER_CoinSpendPrivateKeyP coin_priv;
/**
* Blinding key used for the operation.
*/
struct TALER_DenominationBlindingKeyP blinding_key;
/**
* Withdraw handle (while operation is running).
*/
struct TALER_EXCHANGE_ReserveWithdrawHandle *wsh;
} reserve_withdraw;
/**
* Information for an #OC_CONTRACT command.
*/
struct
{
/**
* Contract proposal (without merchant_pub, exchanges or H_wire).
* It's dynamically generated because we need different transaction_id
* for different merchant instances.
*/
char proposal[CONTRACT_MAX_SIZE];
/**
* Handle to the active /contract operation, or NULL.
*/
struct TALER_MERCHANT_ContractOperation *co;
/**
* Full contract in JSON, set by the /contract operation.
*/
json_t *contract;
/**
* Signature, set by the /contract operation.
*/
struct TALER_MerchantSignatureP merchant_sig;
/**
* Hash of the full contract, set by the /contract operation.
*/
struct GNUNET_HashCode h_contract;
} contract;
/**
* Information for a #OC_PAY command.
* FIXME: support tests where we pay with multiple coins at once.
*/
struct
{
/**
* Reference to the contract.
*/
const char *contract_ref;
/**
* Reference to a reserve_withdraw operation for a coin to
* be used for the /deposit operation.
*/
const char *coin_ref;
/**
* If this @e coin_ref refers to an operation that generated
* an array of coins, this value determines which coin to use.
*/
unsigned int coin_idx;
/**
* Amount to pay (from the coin, including fee).
*/
const char *amount_with_fee;
/**
* Amount to pay (from the coin, excluding fee). The sum of the
* deltas between all @e amount_with_fee and the @e
* amount_without_fee must be less than max_fee, and the sum of
* the @e amount_with_fee must be larger than the @e
* total_amount.
*/
const char *amount_without_fee;
/**
* Deposit handle while operation is running.
*/
struct TALER_MERCHANT_Pay *ph;
/**
* Set to the transaction ID of the respective contract.
*/
uint64_t transaction_id;
/**
* Merchant's public key
*/
struct TALER_MerchantPublicKeyP merchant_pub;
} pay;
struct {
/**
* Process for the aggregator.
*/
struct GNUNET_OS_Process *aggregator_proc;
/**
* ID of task called whenever we get a SIGCHILD.
*/
struct GNUNET_SCHEDULER_Task *child_death_task;
} run_aggregator;
struct {
/**
* Which amount do we expect to see transferred?
*/
const char *amount;
/**
* Which account do we expect to be debited?
*/
uint64_t account_debit;
/**
* Which account do we expect to be credited?
*/
uint64_t account_credit;
/**
* Set (!) to the wire transfer identifier observed.
*/
struct TALER_WireTransferIdentifierRawP wtid;
} check_bank_transfer;
struct {
/**
* #OC_CHECK_BANK_TRANSFER command from which we should grab
* the WTID.
*/
char *check_bank_ref;
/**
* #OC_PAY command which we expect in the result.
*/
char *expected_pay_ref;
/**
* Handle to a /track/transfer operation
*/
struct TALER_MERCHANT_TrackTransferHandle *tdo;
} track_transfer;
struct {
/**
* #OC_PAY command from which we should grab
* the WTID.
*/
char *pay_ref;
/**
* #OC_CHECK_BANK_TRANSFER command which we expect in the result.
*/
char *expected_transfer_ref;
/**
* Handle to a /track/transaction operation
*/
struct TALER_MERCHANT_TrackTransactionHandle *tth;
} track_transaction;
struct {
/**
* Date we want retrieved transactions younger than
*/
struct GNUNET_TIME_Absolute date;
/**
* How many "rows" we expect in the result
*/
unsigned int nresult;
/**
* Handle to the merchant
*/
/**
* Handle to /history request
*/
struct TALER_MERCHANT_HistoryOperation *ho;
} history;
} details;
};
/**
* State of the interpreter loop.
*/
struct InterpreterState
{
/**
* Keys from the exchange.
*/
const struct TALER_EXCHANGE_Keys *keys;
/**
* Commands the interpreter will run.
*/
struct Command *commands;
/**
* Interpreter task (if one is scheduled).
*/
struct GNUNET_SCHEDULER_Task *task;
/**
* Instruction pointer. Tells #interpreter_run() which
* instruction to run next.
*/
unsigned int ip;
};
/**
* Pipe used to communicate child death via signal.
*/
static struct GNUNET_DISK_PipeHandle *sigpipe;
/**
* The testcase failed, return with an error code.
*
* @param is interpreter state to clean up
*/
static void
fail (struct InterpreterState *is)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Interpreter failed at step %s (#%u)\n",
is->commands[is->ip].label,
is->ip);
result = GNUNET_SYSERR;
GNUNET_SCHEDULER_shutdown ();
}
/**
* Get a new contract proposal for each OC_CONTRACT
* in the list of command. It's used when we run multiple
* instances beacuse we can't have the same transaction_id
* for two instances.
*
* @param cmds the list of commands
*/
void
get_new_contracts (struct Command *cmds)
{
unsigned int i;
unsigned int d = 0;
struct Command *cmd;
#define DELTA 1000
for (i=0;OC_END != (cmd = &cmds[i])->oc;i++)
if ( (NULL != cmd->label) &&
(OC_CONTRACT == cmd->oc) )
{
if (0 == strcmp (cmd->label, "create-contract-2"))
d = DELTA;
snprintf (cmd->details.contract.proposal,
CONTRACT_MAX_SIZE,
"{\
\"max_fee\":\
{\"currency\":\"EUR\", \"value\":0, \"fraction\":500000},\
\"transaction_id\":%d,\
\"timestamp\":\"\\/Date(42)\\/\",\
\"refund_deadline\":\"\\/Date(0)\\/\",\
\"expiry\":\"\\/Date(999999999)\\/\",\
\"amount\":{\"currency\":\"EUR\", \"value\":5, \"fraction\":0},\
\"products\":\
[ {\"description\":\"ice cream\", \"value\":\"{EUR:5}\"} ]\
}",
receiver_idx + d);
}
}
/**
* 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 Command *
find_command (const struct InterpreterState *is,
const char *label)
{
unsigned int i;
const struct Command *cmd;
if (NULL == label)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Attempt to lookup command for empty label\n");
return NULL;
}
for (i=0;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;
}
/**
* Run the main interpreter loop that performs exchange operations.
*
* @param cls contains the `struct InterpreterState`
*/
static void
interpreter_run (void *cls);
/**
* Run the next command with the interpreter.
*
* @param is current interpeter state.
*/
static void
next_command (struct InterpreterState *is)
{
is->ip++;
is->task = GNUNET_SCHEDULER_add_now (&interpreter_run,
is);
}
/**
* 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 exchange's reply is bogus (fails to follow the protocol)
* @param full_response full response from the exchange (for logging, in case of errors)
*/
static void
add_incoming_cb (void *cls,
unsigned int http_status,
const json_t *full_response)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
cmd->details.admin_add_incoming.aih = NULL;
if (MHD_HTTP_OK != http_status)
{
GNUNET_break (0);
fail (is);
return;
}
next_command (is);
}
/**
* Callback for a /history request. It's up to this function how
* to render the array containing transactions details (FIXME link to
* documentation)
*
* @param cls closure
* @param http_status HTTP status returned by the merchant backend
* @param json actual body containing history
*/
void
history_cb (void *cls,
unsigned int http_status,
const json_t *json)
{
struct InterpreterState *is = cls;
if (MHD_HTTP_OK != http_status)
{
fail (is);
return;
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Got 200 OK from /history!\n");
}
/**
* Check if the given historic event @a h corresponds to the given
* command @a cmd.
*
* @param h event in history
* @param cmd an #OC_ADMIN_ADD_INCOMING command
* @return #GNUNET_OK if they match, #GNUNET_SYSERR if not
*/
static int
compare_admin_add_incoming_history (const struct TALER_EXCHANGE_ReserveHistory *h,
const struct Command *cmd)
{
struct TALER_Amount amount;
if (TALER_EXCHANGE_RTT_DEPOSIT != h->type)
{
GNUNET_break_op (0);
return GNUNET_SYSERR;
}
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (cmd->details.admin_add_incoming.amount,
&amount));
if (0 != TALER_amount_cmp (&amount,
&h->amount))
{
GNUNET_break_op (0);
return GNUNET_SYSERR;
}
return GNUNET_OK;
}
/**
* Check if the given historic event @a h corresponds to the given
* command @a cmd.
*
* @param h event in history
* @param cmd an #OC_WITHDRAW_SIGN command
* @return #GNUNET_OK if they match, #GNUNET_SYSERR if not
*/
static int
compare_reserve_withdraw_history (const struct TALER_EXCHANGE_ReserveHistory *h,
const struct Command *cmd)
{
struct TALER_Amount amount;
struct TALER_Amount amount_with_fee;
if (TALER_EXCHANGE_RTT_WITHDRAWAL != h->type)
{
GNUNET_break_op (0);
return GNUNET_SYSERR;
}
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (cmd->details.reserve_withdraw.amount,
&amount));
GNUNET_assert (GNUNET_OK ==
TALER_amount_add (&amount_with_fee,
&amount,
&cmd->details.reserve_withdraw.pk->fee_withdraw));
if (0 != TALER_amount_cmp (&amount_with_fee,
&h->amount))
{
GNUNET_break_op (0);
return GNUNET_SYSERR;
}
return GNUNET_OK;
}
/**
* Function called with the result of a /reserve/status 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 exchange's reply is bogus (fails to follow the protocol)
* @param[in] json original response in JSON format (useful only for diagnostics)
* @param balance current balance in the reserve, NULL on error
* @param history_length number of entries in the transaction history, 0 on error
* @param history detailed transaction history, NULL on error
*/
static void
reserve_status_cb (void *cls,
unsigned int http_status,
const json_t *json,
const struct TALER_Amount *balance,
unsigned int history_length,
const struct TALER_EXCHANGE_ReserveHistory *history)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
struct Command *rel;
unsigned int i;
unsigned int j;
struct TALER_Amount amount;
cmd->details.reserve_status.wsh = NULL;
if (cmd->expected_response_code != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u to command %s\n",
http_status,
cmd->label);
GNUNET_break (0);
json_dumpf (json, stderr, 0);
fail (is);
return;
}
switch (http_status)
{
case MHD_HTTP_OK:
j = 0;
for (i=0;iip;i++)
{
switch ((rel = &is->commands[i])->oc)
{
case OC_ADMIN_ADD_INCOMING:
if ( ( (NULL != rel->label) &&
(0 == strcmp (cmd->details.reserve_status.reserve_reference,
rel->label) ) ) ||
( (NULL != rel->details.admin_add_incoming.reserve_reference) &&
(0 == strcmp (cmd->details.reserve_status.reserve_reference,
rel->details.admin_add_incoming.reserve_reference) ) ) )
{
if (GNUNET_OK !=
compare_admin_add_incoming_history (&history[j],
rel))
{
GNUNET_break (0);
fail (is);
return;
}
j++;
}
break;
case OC_WITHDRAW_SIGN:
if (0 == strcmp (cmd->details.reserve_status.reserve_reference,
rel->details.reserve_withdraw.reserve_reference))
{
if (GNUNET_OK !=
compare_reserve_withdraw_history (&history[j],
rel))
{
GNUNET_break (0);
fail (is);
return;
}
j++;
}
break;
default:
/* unreleated, just skip */
break;
}
}
if (j != history_length)
{
GNUNET_break (0);
fail (is);
return;
}
if (NULL != cmd->details.reserve_status.expected_balance)
{
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (cmd->details.reserve_status.expected_balance,
&amount));
if (0 != TALER_amount_cmp (&amount,
balance))
{
GNUNET_break (0);
fail (is);
return;
}
}
break;
default:
/* Unsupported status code (by test harness) */
GNUNET_break (0);
break;
}
next_command (is);
}
/**
* Function called upon completion of our /reserve/withdraw 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 exchange's reply is bogus (fails to follow the protocol)
* @param sig signature over the coin, NULL on error
* @param full_response full response from the exchange (for logging, in case of errors)
*/
static void
reserve_withdraw_cb (void *cls,
unsigned int http_status,
const struct TALER_DenominationSignature *sig,
const json_t *full_response)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
cmd->details.reserve_withdraw.wsh = NULL;
if (cmd->expected_response_code != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u to command %s\n",
http_status,
cmd->label);
json_dumpf (full_response, stderr, 0);
GNUNET_break (0);
fail (is);
return;
}
switch (http_status)
{
case MHD_HTTP_OK:
if (NULL == sig)
{
GNUNET_break (0);
fail (is);
return;
}
cmd->details.reserve_withdraw.sig.rsa_signature
= GNUNET_CRYPTO_rsa_signature_dup (sig->rsa_signature);
break;
case MHD_HTTP_PAYMENT_REQUIRED:
/* nothing to check */
break;
default:
/* Unsupported status code (by test harness) */
GNUNET_break (0);
break;
}
next_command (is);
}
/**
* Callbacks of this type are used to serve the result of submitting a
* /contract request to a merchant.
*
* @param cls closure
* @param http_status HTTP response code, 200 indicates success;
* 0 if the backend's reply is bogus (fails to follow the protocol)
* @param obj the full received JSON reply, or
* error details if the request failed
* @param contract completed contract, NULL on error
* @param sig merchant's signature over the contract, NULL on error
* @param h_contract hash of the contract, NULL on error
*/
static void
contract_cb (void *cls,
unsigned int http_status,
const json_t *obj,
const json_t *contract,
const struct TALER_MerchantSignatureP *sig,
const struct GNUNET_HashCode *h_contract)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
cmd->details.contract.co = NULL;
switch (http_status)
{
case MHD_HTTP_OK:
cmd->details.contract.contract = json_incref ((json_t *) contract);
cmd->details.contract.merchant_sig = *sig;
cmd->details.contract.h_contract = *h_contract;
break;
default:
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"/contract responded with unexpected status code %u in step %u\n",
http_status,
is->ip);
json_dumpf (obj, stderr, 0);
GNUNET_break (0);
fail (is);
return;
}
next_command (is);
}
/**
* Function called with the result of a /pay operation.
*
* @param cls closure with the interpreter state
* @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful deposit;
* 0 if the exchange's reply is bogus (fails to follow the protocol)
* @param obj the received JSON reply, should be kept as proof (and, in case of errors,
* be forwarded to the customer)
*/
static void
pay_cb (void *cls,
unsigned int http_status,
const json_t *obj)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
struct PaymentResponsePS mr;
struct GNUNET_CRYPTO_EddsaSignature sig;
json_t *jsig;
cmd->details.pay.ph = NULL;
if (cmd->expected_response_code != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u to command %s\n",
http_status,
cmd->label);
json_dumpf (obj, stderr, 0);
fail (is);
return;
}
if (MHD_HTTP_OK == http_status)
{
/* Check signature */
GNUNET_break (NULL !=
(jsig = json_object_get (obj, "merchant_sig")));
mr.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_PAYMENT_OK);
mr.purpose.size = htonl (sizeof (mr));
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Got DP: %s\n",
json_dumps (obj,
JSON_INDENT (2)));
GNUNET_assert (GNUNET_OK ==
GNUNET_STRINGS_string_to_data (json_string_value (jsig),
strlen (json_string_value (jsig)),
&sig,
sizeof (sig)));
if (GNUNET_OK !=
GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_MERCHANT_PAYMENT_OK,
&mr.purpose,
&sig,
&cmd->details.pay.merchant_pub.eddsa_pub))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Merchant signature given in response to /pay invalid\n");
fail (is);
return;
}
}
next_command (is);
}
/**
* Task triggered whenever we receive a SIGCHLD (child
* process died).
*
* @param cls closure, NULL if we need to self-restart
*/
static void
maint_child_death (void *cls)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
const struct GNUNET_DISK_FileHandle *pr;
char c[16];
cmd->details.run_aggregator.child_death_task = NULL;
pr = GNUNET_DISK_pipe_handle (sigpipe, GNUNET_DISK_PIPE_END_READ);
GNUNET_break (0 < GNUNET_DISK_file_read (pr, &c, sizeof (c)));
GNUNET_OS_process_wait (cmd->details.run_aggregator.aggregator_proc);
GNUNET_OS_process_destroy (cmd->details.run_aggregator.aggregator_proc);
cmd->details.run_aggregator.aggregator_proc = NULL;
next_command (is);
}
/**
* Callback for a /track/transfer operation
*
* @param cls closure for this function
* @param http_status HTTP response code returned by the server
* @param sign_key exchange key used to sign @a json, or NULL
* @param json original json reply (may include signatures, those have then been
* validated already)
* @param h_wire hash of the wire transfer address the transfer went to, or NULL on error
* @param total_amount total amount of the wire transfer, or NULL if the exchange could
* not provide any @a wtid (set only if @a http_status is #MHD_HTTP_OK)
* @param details_length length of the @a details array
* @param details array with details about the combined transactions
*/
static void
track_transfer_cb (void *cls,
unsigned int http_status,
const struct TALER_ExchangePublicKeyP *sign_key,
const json_t *json,
const struct GNUNET_HashCode *h_wire,
const struct TALER_Amount *total_amount,
unsigned int details_length,
const struct TALER_TrackTransferDetails *details)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
cmd->details.track_transfer.tdo = NULL;
if (cmd->expected_response_code != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u to command %s\n",
http_status,
cmd->label);
json_dumpf (json, stderr, 0);
fail (is);
return;
}
switch (http_status)
{
case MHD_HTTP_OK:
{
const struct Command *ref;
unsigned int i;
int found;
ref = find_command (is,
cmd->details.track_transfer.expected_pay_ref);
GNUNET_assert (NULL != ref);
found = GNUNET_NO;
for (i=0;idetails.pay.amount_without_fee,
&amount_without_fee));
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (ref->details.pay.amount_with_fee,
&amount_with_fee));
GNUNET_assert (GNUNET_OK ==
TALER_amount_subtract (&deposit_fee,
&amount_with_fee,
&amount_without_fee));
cref = find_command (is,
ref->details.pay.coin_ref);
GNUNET_assert (NULL != cref);
switch (cref->oc)
{
case OC_WITHDRAW_SIGN:
GNUNET_CRYPTO_eddsa_key_get_public (&cref->details.reserve_withdraw.coin_priv.eddsa_priv,
&coin_pub.eddsa_pub);
break;
default:
GNUNET_assert (0);
}
if ( (details[i].transaction_id == ref->details.pay.transaction_id) &&
(0 == TALER_amount_cmp (&details[i].coin_value,
&amount_with_fee)) &&
(0 == TALER_amount_cmp (&details[i].coin_fee,
&deposit_fee)) &&
(0 == memcmp (&details[i].coin_pub,
&coin_pub,
sizeof (struct TALER_CoinSpendPublicKeyP))) )
found = GNUNET_YES;
}
if (GNUNET_NO == found)
{
GNUNET_break (0);
json_dumpf (json, stderr, 0);
fail (is);
return;
}
break;
}
default:
break;
}
next_command (is);
}
/**
* Function called with detailed wire transfer data.
*
* @param cls closure
* @param http_status HTTP status code we got, 0 on exchange protocol violation
* @param json original json reply
* @param num_transfers number of wire transfers involved in setting the transaction
* @param transfers detailed list of transfers involved and their coins
*/
static void
track_transaction_cb (void *cls,
unsigned int http_status,
const json_t *json,
unsigned int num_transfers,
const struct TALER_MERCHANT_TransactionWireTransfer *transfers)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
cmd->details.track_transaction.tth = NULL;
if (cmd->expected_response_code != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u to command %s\n",
http_status,
cmd->label);
json_dumpf (json, stderr, 0);
fail (is);
return;
}
/* Test result vs. expecations... */
switch (http_status)
{
case MHD_HTTP_OK:
{
const struct Command *ref;
struct TALER_Amount ea;
struct TALER_Amount coin_contribution;
if (1 != num_transfers)
{
GNUNET_break (0);
json_dumpf (json, stderr, 0);
fail (is);
return;
}
ref = find_command (is,
cmd->details.track_transaction.expected_transfer_ref);
GNUNET_assert (NULL != ref);
if (0 != memcmp (&ref->details.check_bank_transfer.wtid,
&transfers[0].wtid,
sizeof (struct TALER_WireTransferIdentifierRawP)))
{
GNUNET_break (0);
json_dumpf (json, stderr, 0);
fail (is);
return;
}
/* NOTE: this assumes that the wire transfer corresponds to a
single coin involved in a pay/deposit. Thus, this invariant
may not always hold in the future depending no how the
testcases evolve. */
if (1 != transfers[0].num_coins)
{
GNUNET_break (0);
json_dumpf (json, stderr, 0);
fail (is);
return;
}
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (ref->details.check_bank_transfer.amount,
&ea));
GNUNET_assert (GNUNET_OK ==
TALER_amount_subtract (&coin_contribution,
&transfers[0].coins[0].amount_with_fee,
&transfers[0].coins[0].deposit_fee));
if (0 !=
TALER_amount_cmp (&ea,
&coin_contribution))
{
GNUNET_break (0);
json_dumpf (json, stderr, 0);
fail (is);
return;
}
break;
}
default:
break;
}
next_command (is);
}
/**
* Find denomination key matching the given amount.
*
* @param keys array of keys to search
* @param amount coin value to look for
* @return NULL if no matching key was found
*/
static const struct TALER_EXCHANGE_DenomPublicKey *
find_pk (const struct TALER_EXCHANGE_Keys *keys,
const struct TALER_Amount *amount)
{
unsigned int i;
struct GNUNET_TIME_Absolute now;
struct TALER_EXCHANGE_DenomPublicKey *pk;
char *str;
now = GNUNET_TIME_absolute_get ();
for (i=0;inum_denom_keys;i++)
{
pk = &keys->denom_keys[i];
if ( (0 == TALER_amount_cmp (amount,
&pk->value)) &&
(now.abs_value_us >= pk->valid_from.abs_value_us) &&
(now.abs_value_us < pk->withdraw_valid_until.abs_value_us) )
return pk;
}
/* do 2nd pass to check if expiration times are to blame for failure */
str = TALER_amount_to_string (amount);
for (i=0;inum_denom_keys;i++)
{
pk = &keys->denom_keys[i];
if ( (0 == TALER_amount_cmp (amount,
&pk->value)) &&
( (now.abs_value_us < pk->valid_from.abs_value_us) ||
(now.abs_value_us > pk->withdraw_valid_until.abs_value_us) ) )
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Have denomination key for `%s', but with wrong expiration range %llu vs [%llu,%llu)\n",
str,
(unsigned long long) now.abs_value_us,
(unsigned long long) pk->valid_from.abs_value_us,
(unsigned long long) pk->withdraw_valid_until.abs_value_us);
GNUNET_free (str);
return NULL;
}
}
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"No denomination key for amount %s found\n",
str);
GNUNET_free (str);
return NULL;
}
/**
* Run the main interpreter loop that performs exchange operations.
*
* @param cls contains the `struct InterpreterState`
*/
static void
interpreter_run (void *cls)
{
const struct GNUNET_SCHEDULER_TaskContext *tc;
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
const struct Command *ref;
struct TALER_ReservePublicKeyP reserve_pub;
struct TALER_CoinSpendPublicKeyP coin_pub;
struct TALER_Amount amount;
struct GNUNET_TIME_Absolute execution_date;
json_t *sender_details;
json_t *transfer_details;
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;
}
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Interpreter runs command %u/%s(%u)\n",
is->ip,
cmd->label,
cmd->oc);
switch (cmd->oc)
{
case OC_END:
result = GNUNET_OK;
if (receiver_idx + 1 == nreceiver)
{
GNUNET_SCHEDULER_shutdown ();
return;
}
is->ip = 0;
receiver_idx++;
receiver = instances[receiver_idx];
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Switching instance: '%s'",
receiver);
get_new_contracts(is->commands);
is->task = GNUNET_SCHEDULER_add_now (interpreter_run,
is);
return;
case OC_ADMIN_ADD_INCOMING:
if (NULL !=
cmd->details.admin_add_incoming.reserve_reference)
{
ref = find_command (is,
cmd->details.admin_add_incoming.reserve_reference);
GNUNET_assert (NULL != ref);
GNUNET_assert (OC_ADMIN_ADD_INCOMING == ref->oc);
cmd->details.admin_add_incoming.reserve_priv
= ref->details.admin_add_incoming.reserve_priv;
}
else
{
struct GNUNET_CRYPTO_EddsaPrivateKey *priv;
priv = GNUNET_CRYPTO_eddsa_key_create ();
cmd->details.admin_add_incoming.reserve_priv.eddsa_priv = *priv;
GNUNET_free (priv);
}
GNUNET_CRYPTO_eddsa_key_get_public (&cmd->details.admin_add_incoming.reserve_priv.eddsa_priv,
&reserve_pub.eddsa_pub);
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;
}
execution_date = GNUNET_TIME_absolute_get ();
GNUNET_TIME_round_abs (&execution_date);
sender_details = json_loads (cmd->details.admin_add_incoming.sender_details,
JSON_REJECT_DUPLICATES,
NULL);
if (NULL == sender_details)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to parse sender details `%s' at %u\n",
cmd->details.admin_add_incoming.sender_details,
is->ip);
fail (is);
return;
}
transfer_details = json_loads (cmd->details.admin_add_incoming.transfer_details,
JSON_REJECT_DUPLICATES,
NULL);
if (NULL == transfer_details)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to parse transfer details `%s' at %u\n",
cmd->details.admin_add_incoming.transfer_details,
is->ip);
fail (is);
return;
}
cmd->details.admin_add_incoming.aih
= TALER_EXCHANGE_admin_add_incoming (exchange,
"http://localhost:18080/",
&reserve_pub,
&amount,
execution_date,
sender_details,
transfer_details,
&add_incoming_cb,
is);
json_decref (sender_details);
json_decref (transfer_details);
if (NULL == cmd->details.admin_add_incoming.aih)
{
GNUNET_break (0);
fail (is);
return;
}
return;
case OC_WITHDRAW_STATUS:
GNUNET_assert (NULL !=
cmd->details.reserve_status.reserve_reference);
ref = find_command (is,
cmd->details.reserve_status.reserve_reference);
GNUNET_assert (NULL != ref);
GNUNET_assert (OC_ADMIN_ADD_INCOMING == ref->oc);
GNUNET_CRYPTO_eddsa_key_get_public (&ref->details.admin_add_incoming.reserve_priv.eddsa_priv,
&reserve_pub.eddsa_pub);
cmd->details.reserve_status.wsh
= TALER_EXCHANGE_reserve_status (exchange,
&reserve_pub,
&reserve_status_cb,
is);
return;
case OC_WITHDRAW_SIGN:
GNUNET_assert (NULL !=
cmd->details.reserve_withdraw.reserve_reference);
ref = find_command (is,
cmd->details.reserve_withdraw.reserve_reference);
GNUNET_assert (NULL != ref);
GNUNET_assert (OC_ADMIN_ADD_INCOMING == ref->oc);
if (NULL != cmd->details.reserve_withdraw.amount)
{
if (GNUNET_OK !=
TALER_string_to_amount (cmd->details.reserve_withdraw.amount,
&amount))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to parse amount `%s' at %u\n",
cmd->details.reserve_withdraw.amount,
is->ip);
fail (is);
return;
}
cmd->details.reserve_withdraw.pk = find_pk (is->keys,
&amount);
}
if (NULL == cmd->details.reserve_withdraw.pk)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to determine denomination key at %u\n",
is->ip);
fail (is);
return;
}
/* create coin's private key */
{
struct GNUNET_CRYPTO_EddsaPrivateKey *priv;
priv = GNUNET_CRYPTO_eddsa_key_create ();
cmd->details.reserve_withdraw.coin_priv.eddsa_priv = *priv;
GNUNET_free (priv);
}
GNUNET_CRYPTO_eddsa_key_get_public (&cmd->details.reserve_withdraw.coin_priv.eddsa_priv,
&coin_pub.eddsa_pub);
GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK,
&cmd->details.reserve_withdraw.blinding_key,
sizeof (cmd->details.reserve_withdraw.blinding_key));
cmd->details.reserve_withdraw.wsh
= TALER_EXCHANGE_reserve_withdraw (exchange,
cmd->details.reserve_withdraw.pk,
&ref->details.admin_add_incoming.reserve_priv,
&cmd->details.reserve_withdraw.coin_priv,
&cmd->details.reserve_withdraw.blinding_key,
&reserve_withdraw_cb,
is);
if (NULL == cmd->details.reserve_withdraw.wsh)
{
GNUNET_break (0);
fail (is);
return;
}
return;
case OC_CONTRACT:
{
json_t *proposal;
json_error_t error;
proposal = json_loads (cmd->details.contract.proposal,
JSON_REJECT_DUPLICATES,
&error);
if (NULL != receiver)
json_object_set_new (proposal,
"receiver",
json_string (receiver));
if (NULL == proposal)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to parse proposal `%s' at command #%u: %s at %u\n",
cmd->details.contract.proposal,
is->ip,
error.text,
(unsigned int) error.column);
fail (is);
return;
}
cmd->details.contract.co
= TALER_MERCHANT_contract_sign (ctx,
MERCHANT_URI,
proposal,
&contract_cb,
is);
if (NULL == cmd->details.contract.co)
{
GNUNET_break (0);
json_decref (proposal);
fail (is);
return;
}
json_decref (proposal);
return;
}
case OC_PAY:
{
struct TALER_MERCHANT_PayCoin pc;
uint64_t transaction_id;
struct GNUNET_TIME_Absolute refund_deadline;
struct GNUNET_TIME_Absolute timestamp;
struct GNUNET_HashCode h_wire;
struct TALER_MerchantPublicKeyP merchant_pub;
struct TALER_MerchantSignatureP merchant_sig;
struct GNUNET_HashCode h_contract;
struct TALER_Amount total_amount;
struct TALER_Amount max_fee;
const char *error_name;
unsigned int error_line;
/* get amount */
ref = find_command (is,
cmd->details.pay.contract_ref);
GNUNET_assert (NULL != ref);
merchant_sig = ref->details.contract.merchant_sig;
GNUNET_assert (NULL != ref->details.contract.contract);
{
/* Get information that need to be replied in the deposit permission */
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_uint64 ("transaction_id", &transaction_id),
GNUNET_JSON_spec_absolute_time ("refund_deadline", &refund_deadline),
GNUNET_JSON_spec_absolute_time ("timestamp", ×tamp),
GNUNET_JSON_spec_fixed_auto ("merchant_pub", &merchant_pub),
GNUNET_JSON_spec_fixed_auto ("H_wire", &h_wire),
TALER_JSON_spec_amount ("amount", &total_amount),
TALER_JSON_spec_amount ("max_fee", &max_fee),
GNUNET_JSON_spec_end()
};
if (GNUNET_OK !=
GNUNET_JSON_parse (ref->details.contract.contract,
spec,
&error_name,
&error_line))
{
GNUNET_break_op (0);
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Parser failed on %s:%u\n",
error_name,
error_line);
fail (is);
return;
}
cmd->details.pay.transaction_id = transaction_id;
cmd->details.pay.merchant_pub = merchant_pub;
}
TALER_JSON_hash (ref->details.contract.contract,
&h_contract);
/* initialize 'pc' (FIXME: to do in a loop later...) */
{
memset (&pc, 0, sizeof (pc));
ref = find_command (is,
cmd->details.pay.coin_ref);
GNUNET_assert (NULL != ref);
switch (ref->oc)
{
case OC_WITHDRAW_SIGN:
pc.coin_priv = ref->details.reserve_withdraw.coin_priv;
pc.denom_pub = ref->details.reserve_withdraw.pk->key;
pc.denom_sig = ref->details.reserve_withdraw.sig;
pc.denom_value = ref->details.reserve_withdraw.pk->value;
break;
default:
GNUNET_assert (0);
}
if (GNUNET_OK !=
TALER_string_to_amount (cmd->details.pay.amount_without_fee,
&pc.amount_without_fee))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to parse amount `%s' at %u\n",
cmd->details.pay.amount_without_fee,
is->ip);
fail (is);
return;
}
if (GNUNET_OK !=
TALER_string_to_amount (cmd->details.pay.amount_with_fee,
&pc.amount_with_fee))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to parse amount `%s' at %u\n",
cmd->details.pay.amount_with_fee,
is->ip);
fail (is);
return;
}
}
cmd->details.pay.ph
= TALER_MERCHANT_pay_wallet (ctx,
MERCHANT_URI,
receiver,
&h_contract,
transaction_id,
&total_amount,
&max_fee,
&merchant_pub,
&merchant_sig,
timestamp,
refund_deadline,
&h_wire,
EXCHANGE_URI,
1 /* num_coins */,
&pc /* coins */,
&pay_cb,
is);
}
if (NULL == cmd->details.pay.ph)
{
GNUNET_break (0);
fail (is);
return;
}
return;
case OC_RUN_AGGREGATOR:
{
const struct GNUNET_DISK_FileHandle *pr;
cmd->details.run_aggregator.aggregator_proc
= GNUNET_OS_start_process (GNUNET_NO,
GNUNET_OS_INHERIT_STD_ALL,
NULL, NULL, NULL,
"taler-exchange-aggregator",
"taler-exchange-aggregator",
"-c", "test_merchant_api.conf",
"-t", /* exit when done */
NULL);
if (NULL == cmd->details.run_aggregator.aggregator_proc)
{
GNUNET_break (0);
fail (is);
return;
}
pr = GNUNET_DISK_pipe_handle (sigpipe, GNUNET_DISK_PIPE_END_READ);
cmd->details.run_aggregator.child_death_task
= GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL,
pr,
&maint_child_death, is);
return;
}
case OC_CHECK_BANK_TRANSFER:
{
if (GNUNET_OK !=
TALER_string_to_amount (cmd->details.check_bank_transfer.amount,
&amount))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to parse amount `%s' at %u\n",
cmd->details.reserve_withdraw.amount,
is->ip);
fail (is);
return;
}
if (GNUNET_OK !=
TALER_FAKEBANK_check (fakebank,
&amount,
cmd->details.check_bank_transfer.account_debit,
cmd->details.check_bank_transfer.account_credit,
&cmd->details.check_bank_transfer.wtid))
{
GNUNET_break (0);
fail (is);
return;
}
next_command (is);
return;
}
case OC_CHECK_BANK_TRANSFERS_EMPTY:
{
if (GNUNET_OK !=
TALER_FAKEBANK_check_empty (fakebank))
{
GNUNET_break (0);
fail (is);
return;
}
next_command (is);
return;
}
case OC_TRACK_TRANSFER:
ref = find_command (is,
cmd->details.track_transfer.check_bank_ref);
GNUNET_assert (NULL != ref);
cmd->details.track_transfer.tdo =
TALER_MERCHANT_track_transfer (ctx,
MERCHANT_URI,
receiver,
&ref->details.check_bank_transfer.wtid,
EXCHANGE_URI,
&track_transfer_cb,
is);
return;
case OC_TRACK_TRANSACTION:
ref = find_command (is,
cmd->details.track_transaction.pay_ref);
GNUNET_assert (NULL != ref);
/*FIXME check/assert return code */
cmd->details.track_transaction.tth =
TALER_MERCHANT_track_transaction (ctx,
MERCHANT_URI,
receiver, /* got it NULL, right now */
ref->details.pay.transaction_id,
&track_transaction_cb,
is);
return;
case OC_HISTORY:
LOG_INFO("Processing history\n");
return;
if (NULL ==
(cmd->details.history.ho = TALER_MERCHANT_history (ctx,
MERCHANT_URI,
cmd->details.history.date,
history_cb,
is)))
{
fail (is);
return;
}
break;
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 when the test times out.
*
* @param cls NULL
*/
static void
do_timeout (void *cls)
{
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 Command *cmd;
unsigned int i;
if (NULL != timeout_task)
{
GNUNET_SCHEDULER_cancel (timeout_task);
timeout_task = NULL;
}
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Shutdown executing\n");
for (i=0;OC_END != (cmd = &is->commands[i])->oc;i++)
{
switch (cmd->oc)
{
case OC_END:
GNUNET_assert (0);
break;
case 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_EXCHANGE_admin_add_incoming_cancel (cmd->details.admin_add_incoming.aih);
cmd->details.admin_add_incoming.aih = NULL;
}
break;
case OC_WITHDRAW_STATUS:
if (NULL != cmd->details.reserve_status.wsh)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete\n",
i,
cmd->label);
TALER_EXCHANGE_reserve_status_cancel (cmd->details.reserve_status.wsh);
cmd->details.reserve_status.wsh = NULL;
}
break;
case OC_WITHDRAW_SIGN:
if (NULL != cmd->details.reserve_withdraw.wsh)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete\n",
i,
cmd->label);
TALER_EXCHANGE_reserve_withdraw_cancel (cmd->details.reserve_withdraw.wsh);
cmd->details.reserve_withdraw.wsh = NULL;
}
if (NULL != cmd->details.reserve_withdraw.sig.rsa_signature)
{
GNUNET_CRYPTO_rsa_signature_free (cmd->details.reserve_withdraw.sig.rsa_signature);
cmd->details.reserve_withdraw.sig.rsa_signature = NULL;
}
break;
case OC_CONTRACT:
if (NULL != cmd->details.contract.co)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete\n",
i,
cmd->label);
TALER_MERCHANT_contract_sign_cancel (cmd->details.contract.co);
cmd->details.contract.co = NULL;
}
if (NULL != cmd->details.contract.contract)
{
json_decref (cmd->details.contract.contract);
cmd->details.contract.contract = NULL;
}
break;
case OC_PAY:
if (NULL != cmd->details.pay.ph)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete\n",
i,
cmd->label);
TALER_MERCHANT_pay_cancel (cmd->details.pay.ph);
cmd->details.pay.ph = NULL;
}
break;
case OC_RUN_AGGREGATOR:
if (NULL != cmd->details.run_aggregator.aggregator_proc)
{
GNUNET_break (0 ==
GNUNET_OS_process_kill (cmd->details.run_aggregator.aggregator_proc,
SIGKILL));
GNUNET_OS_process_wait (cmd->details.run_aggregator.aggregator_proc);
GNUNET_OS_process_destroy (cmd->details.run_aggregator.aggregator_proc);
cmd->details.run_aggregator.aggregator_proc = NULL;
}
if (NULL != cmd->details.run_aggregator.child_death_task)
{
GNUNET_SCHEDULER_cancel (cmd->details.run_aggregator.child_death_task);
cmd->details.run_aggregator.child_death_task = NULL;
}
break;
case OC_CHECK_BANK_TRANSFER:
break;
case OC_CHECK_BANK_TRANSFERS_EMPTY:
break;
case OC_TRACK_TRANSFER:
if (NULL != cmd->details.track_transfer.tdo)
{
TALER_MERCHANT_track_transfer_cancel (cmd->details.track_transfer.tdo);
cmd->details.track_transfer.tdo = NULL;
}
break;
case OC_TRACK_TRANSACTION:
if (NULL != cmd->details.track_transaction.tth)
{
TALER_MERCHANT_track_transaction_cancel (cmd->details.track_transaction.tth);
cmd->details.track_transaction.tth = NULL;
}
break;
case OC_HISTORY:
if (NULL != cmd->details.history.ho)
{
TALER_MERCHANT_history_cancel (cmd->details.history.ho);
cmd->details.history.ho = NULL;
}
break;
default:
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Shutdown: 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;
}
GNUNET_free (is);
GNUNET_free_non_null (instances);
if (NULL != exchange)
{
TALER_EXCHANGE_disconnect (exchange);
exchange = NULL;
}
if (NULL != ctx)
{
GNUNET_CURL_fini (ctx);
ctx = NULL;
}
if (NULL != rc)
{
GNUNET_CURL_gnunet_rc_destroy (rc);
rc = NULL;
}
TALER_FAKEBANK_stop (fakebank);
fakebank = NULL;
}
/**
* Functions of this type are called to provide the retrieved signing and
* denomination keys of the exchange. No TALER_EXCHANGE_*() functions should be called
* in this callback.
*
* @param cls closure
* @param keys information about keys of the exchange
*/
static void
cert_cb (void *cls,
const struct TALER_EXCHANGE_Keys *keys)
{
struct InterpreterState *is = cls;
/* check that keys is OK */
#define ERR(cond) do { if(!(cond)) break; GNUNET_break (0); GNUNET_SCHEDULER_shutdown(); return; } while (0)
ERR (NULL == keys);
ERR (0 == keys->num_sign_keys);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Read %u signing keys\n",
keys->num_sign_keys);
ERR (0 == keys->num_denom_keys);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Read %u denomination keys\n",
keys->num_denom_keys);
#undef ERR
/* run actual tests via interpreter-loop */
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Certificate callback invoked, starting interpreter\n");
is->keys = keys;
is->task = GNUNET_SCHEDULER_add_now (&interpreter_run,
is);
}
/**
* Signal handler called for SIGCHLD. Triggers the
* respective handler by writing to the trigger pipe.
*/
static void
sighandler_child_death ()
{
static char c;
int old_errno = errno; /* back-up errno */
GNUNET_break (1 ==
GNUNET_DISK_file_write (GNUNET_DISK_pipe_handle
(sigpipe, GNUNET_DISK_PIPE_END_WRITE),
&c, sizeof (c)));
errno = old_errno; /* restore errno */
}
/**
* Main function that will be run by the scheduler.
*
* @param cls closure
*/
static void
run (void *cls)
{
struct InterpreterState *is;
static struct Command commands[] =
{
/* Fill reserve with EUR:5.01, as withdraw fee is 1 ct per config */
{ .oc = OC_ADMIN_ADD_INCOMING,
.label = "create-reserve-1",
.expected_response_code = MHD_HTTP_OK,
.details.admin_add_incoming.sender_details = "{ \"type\":\"test\", \"bank_uri\":\"" BANK_URI "\", \"account_number\":62, \"uuid\":1 }",
.details.admin_add_incoming.transfer_details = "{ \"uuid\": 1}",
.details.admin_add_incoming.amount = "EUR:5.01" },
/* Withdraw a 5 EUR coin, at fee of 1 ct */
{ .oc = OC_WITHDRAW_SIGN,
.label = "withdraw-coin-1",
.expected_response_code = MHD_HTTP_OK,
.details.reserve_withdraw.reserve_reference = "create-reserve-1",
.details.reserve_withdraw.amount = "EUR:5" },
/* Check that deposit and withdraw operation are in history, and
that the balance is now at zero */
{ .oc = OC_WITHDRAW_STATUS,
.label = "withdraw-status-1",
.expected_response_code = MHD_HTTP_OK,
.details.reserve_status.reserve_reference = "create-reserve-1",
.details.reserve_status.expected_balance = "EUR:0" },
/* Create contract */
{ .oc = OC_CONTRACT,
.label = "create-contract-1",
.expected_response_code = MHD_HTTP_OK },
{ .oc = OC_PAY,
.label = "deposit-simple",
.expected_response_code = MHD_HTTP_OK,
.details.pay.contract_ref = "create-contract-1",
.details.pay.coin_ref = "withdraw-coin-1",
.details.pay.amount_with_fee = "EUR:5",
.details.pay.amount_without_fee = "EUR:4.99" },
/* Create another contract */
{ .oc = OC_CONTRACT,
.label = "create-contract-2",
.expected_response_code = MHD_HTTP_OK },
/* Try to double-spend the 5 EUR coin at the same merchant (but different
transaction ID) */
{ .oc = OC_PAY,
.label = "deposit-double-2",
.expected_response_code = MHD_HTTP_FORBIDDEN,
.details.pay.contract_ref = "create-contract-2",
.details.pay.coin_ref = "withdraw-coin-1",
.details.pay.amount_with_fee = "EUR:5",
.details.pay.amount_without_fee = "EUR:4.99" },
/* Fill second reserve with EUR:1 */
{ .oc = OC_ADMIN_ADD_INCOMING,
.label = "create-reserve-2",
.expected_response_code = MHD_HTTP_OK,
.details.admin_add_incoming.sender_details = "{ \"type\":\"test\", \"bank_uri\":\"" BANK_URI "\", \"account_number\":63, \"uuid\":2 }",
.details.admin_add_incoming.transfer_details = "{ \"uuid\": 2}",
.details.admin_add_incoming.amount = "EUR:1" },
/* Add another 4.01 EUR to reserve #2 */
{ .oc = OC_ADMIN_ADD_INCOMING,
.label = "create-reserve-2b",
.expected_response_code = MHD_HTTP_OK,
.details.admin_add_incoming.reserve_reference = "create-reserve-2",
.details.admin_add_incoming.sender_details = "{ \"type\":\"test\", \"bank_uri\":\"" BANK_URI "\", \"account_number\":63, \"uuid\":3 }",
.details.admin_add_incoming.transfer_details = "{ \"uuid\": 3}",
.details.admin_add_incoming.amount = "EUR:4.01" },
/* Withdraw a 5 EUR coin, at fee of 1 ct */
{ .oc = OC_WITHDRAW_SIGN,
.label = "withdraw-coin-2",
.expected_response_code = MHD_HTTP_OK,
.details.reserve_withdraw.reserve_reference = "create-reserve-2",
.details.reserve_withdraw.amount = "EUR:5" },
/* Check nothing happened on the bank side so far */
{ .oc = OC_CHECK_BANK_TRANSFERS_EMPTY,
.label = "check_bank_empty" },
/* Run transfers. */
{ .oc = OC_RUN_AGGREGATOR,
.label = "run-aggregator" },
/* Obtain WTID of the transfer */
{ .oc = OC_CHECK_BANK_TRANSFER,
.label = "check_bank_transfer-499c",
.details.check_bank_transfer.amount = "EUR:4.99",
.details.check_bank_transfer.account_debit = 2, /* exchange-outgoing */
.details.check_bank_transfer.account_credit = 62 /* merchant */
},
/* Check that there are no other unusual transfers */
{ .oc = OC_CHECK_BANK_TRANSFERS_EMPTY,
.label = "check_bank_empty" },
/* Trace the WTID back to the original transaction */
{ .oc = OC_TRACK_TRANSFER,
.label = "track-transfer-1",
.expected_response_code = MHD_HTTP_OK,
.details.track_transfer.check_bank_ref = "check_bank_transfer-499c",
.details.track_transfer.expected_pay_ref = "deposit-simple"
},
{ .oc = OC_TRACK_TRANSFER,
.label = "track-transfer-1-again",
.expected_response_code = MHD_HTTP_OK,
.details.track_transfer.check_bank_ref = "check_bank_transfer-499c",
.details.track_transfer.expected_pay_ref = "deposit-simple"
},
/* Trace transaction to WTID */
{ .oc = OC_TRACK_TRANSACTION,
.label = "track-transaction-1",
.expected_response_code = MHD_HTTP_OK,
.details.track_transaction.pay_ref = "deposit-simple",
.details.track_transaction.expected_transfer_ref = "check_bank_transfer-499c"
},
{ .oc = OC_TRACK_TRANSACTION,
.label = "track-transaction-1-again",
.expected_response_code = MHD_HTTP_OK,
.details.track_transaction.pay_ref = "deposit-simple",
.details.track_transaction.expected_transfer_ref = "check_bank_transfer-499c"
},
/* Pay again successfully on 2nd contract */
{ .oc = OC_PAY,
.label = "deposit-simple-2",
.expected_response_code = MHD_HTTP_OK,
.details.pay.contract_ref = "create-contract-2",
.details.pay.coin_ref = "withdraw-coin-2",
.details.pay.amount_with_fee = "EUR:5",
.details.pay.amount_without_fee = "EUR:4.99" },
/* Check "failure" to trace transaction to WTID before aggregator */
{ .oc = OC_TRACK_TRANSACTION,
.label = "track-transaction-2-found",
.expected_response_code = MHD_HTTP_ACCEPTED,
.details.track_transaction.pay_ref = "deposit-simple-2"
},
{ .oc = OC_TRACK_TRANSACTION,
.label = "track-transaction-2-found-again",
.expected_response_code = MHD_HTTP_ACCEPTED,
.details.track_transaction.pay_ref = "deposit-simple-2"
},
/* Run transfers. */
{ .oc = OC_RUN_AGGREGATOR,
.label = "run-aggregator-2" },
/* Obtain WTID of the transfer */
{ .oc = OC_CHECK_BANK_TRANSFER,
.label = "check_bank_transfer-499c-2",
.details.check_bank_transfer.amount = "EUR:4.99",
.details.check_bank_transfer.account_debit = 2, /* exchange-outgoing */
.details.check_bank_transfer.account_credit = 62 /* merchant */
},
/* Check that there are no other unusual transfers */
{ .oc = OC_CHECK_BANK_TRANSFERS_EMPTY,
.label = "check_bank_empty" },
/* This time, invert the order in which we do the tracing */
/* Trace transaction to WTID */
{ .oc = OC_TRACK_TRANSACTION,
.label = "track-transaction-2",
.expected_response_code = MHD_HTTP_OK,
.details.track_transaction.pay_ref = "deposit-simple-2",
.details.track_transaction.expected_transfer_ref = "check_bank_transfer-499c-2"
},
{ .oc = OC_TRACK_TRANSACTION,
.label = "track-transaction-2-again", .expected_response_code = MHD_HTTP_OK,
.details.track_transaction.pay_ref = "deposit-simple-2",
.details.track_transaction.expected_transfer_ref = "check_bank_transfer-499c-2"
},
/* Trace the WTID back to the original transaction */
{ .oc = OC_TRACK_TRANSFER,
.label = "track-transfer-2",
.expected_response_code = MHD_HTTP_OK,
.details.track_transfer.check_bank_ref = "check_bank_transfer-499c-2",
.details.track_transfer.expected_pay_ref = "deposit-simple-2"
},
{ .oc = OC_TRACK_TRANSFER,
.label = "track-transfer-2-again",
.expected_response_code = MHD_HTTP_OK,
.details.track_transfer.check_bank_ref = "check_bank_transfer-499c-2",
.details.track_transfer.expected_pay_ref = "deposit-simple-2"
},
/**
* NOTE: could NOT initialize timestamps by calling GNUNET_TIME_xy ()
* because that used to give a 'Initializer element is not constant'
* error at compile time.
*/
{ .oc = OC_HISTORY,
.label = "history-1",
.expected_response_code = MHD_HTTP_OK,
.details.history.date.abs_value_us = 0,
.details.history.nresult = 2
},
{ .oc = OC_HISTORY,
.label = "history-2",
.expected_response_code = MHD_HTTP_OK,
.details.history.date.abs_value_us = 2000000000 * 1000LL *1000LL,
.details.history.nresult = 0
},
/* end of testcase */
{ .oc = OC_END }
};
get_new_contracts (commands);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Interpreter initializing\n");
fakebank = TALER_FAKEBANK_start (BANK_PORT);
if (NULL == fakebank)
{
fprintf (stderr,
"\nFailed to start fake bank service\n");
result = 77;
return;
}
is = GNUNET_new (struct InterpreterState);
is->commands = commands;
ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
&rc);
GNUNET_assert (NULL != ctx);
rc = GNUNET_CURL_gnunet_rc_create (ctx);
exchange = TALER_EXCHANGE_connect (ctx,
EXCHANGE_URI,
&cert_cb, is,
TALER_EXCHANGE_OPTION_END);
GNUNET_assert (NULL != exchange);
timeout_task
= GNUNET_SCHEDULER_add_delayed (GNUNET_TIME_relative_multiply
(GNUNET_TIME_UNIT_SECONDS, 150),
&do_timeout, NULL);
GNUNET_SCHEDULER_add_shutdown (&do_shutdown, is);
}
/**
* Main function for the testcase for the exchange API.
*
* @param argc expected to be 1
* @param argv expected to only contain the program name
*/
int
main (int argc,
char * const *argv)
{
char *_instances;
char *token;
struct GNUNET_OS_Process *proc;
struct GNUNET_OS_Process *exchanged;
struct GNUNET_OS_Process *merchantd;
struct TALER_MERCHANTDB_Plugin *db;
struct GNUNET_CONFIGURATION_Handle *cfg;
unsigned int cnt;
struct GNUNET_SIGNAL_Context *shc_chld;
unsetenv ("XDG_DATA_HOME");
unsetenv ("XDG_CONFIG_HOME");
GNUNET_log_setup ("test-merchant-api",
"WARNING",
NULL);
cfg = GNUNET_CONFIGURATION_create ();
GNUNET_assert (GNUNET_OK ==
GNUNET_CONFIGURATION_load (cfg,
"test_merchant_api.conf"));
GNUNET_break (GNUNET_CONFIGURATION_get_value_string (cfg,
"merchant",
"INSTANCES",
&_instances));
GNUNET_break (NULL != (token = strtok (_instances, " ")));
GNUNET_array_append (instances, nreceiver, token);
while (NULL != (token = strtok (NULL, " ")))
GNUNET_array_append(instances, nreceiver, token);
receiver = instances[receiver_idx];
db = TALER_MERCHANTDB_plugin_load (cfg);
if (NULL == db)
{
GNUNET_CONFIGURATION_destroy (cfg);
return 77;
}
(void) db->drop_tables (db->cls);
if (GNUNET_OK != db->initialize (db->cls))
{
TALER_MERCHANTDB_plugin_unload (db);
GNUNET_CONFIGURATION_destroy (cfg);
return 77;
}
TALER_MERCHANTDB_plugin_unload (db);
GNUNET_CONFIGURATION_destroy (cfg);
proc = GNUNET_OS_start_process (GNUNET_NO,
GNUNET_OS_INHERIT_STD_ALL,
NULL, NULL, NULL,
"taler-exchange-keyup",
"taler-exchange-keyup",
"-c", "test_merchant_api.conf",
NULL);
if (NULL == proc)
{
fprintf (stderr,
"Failed to run taler-exchange-keyup. Check your PATH.\n");
return 77;
}
GNUNET_OS_process_wait (proc);
GNUNET_OS_process_destroy (proc);
proc = GNUNET_OS_start_process (GNUNET_NO,
GNUNET_OS_INHERIT_STD_ALL,
NULL, NULL, NULL,
"taler-exchange-dbinit",
"taler-exchange-dbinit",
"-c", "test_merchant_api.conf",
"-r",
NULL);
if (NULL == proc)
{
fprintf (stderr,
"Failed to run taler-exchange-dbinit. Check your PATH.\n");
return 77;
}
GNUNET_OS_process_wait (proc);
GNUNET_OS_process_destroy (proc);
exchanged = GNUNET_OS_start_process (GNUNET_NO,
GNUNET_OS_INHERIT_STD_ALL,
NULL, NULL, NULL,
"taler-exchange-httpd",
"taler-exchange-httpd",
"-c", "test_merchant_api.conf",
NULL);
if (NULL == exchanged)
{
fprintf (stderr,
"Failed to run taler-exchange-httpd. Check your PATH.\n");
return 77;
}
/* give child time to start and bind against the socket */
fprintf (stderr,
"Waiting for taler-exchange-httpd to be ready\n");
cnt = 0;
do
{
fprintf (stderr, ".");
sleep (1);
cnt++;
if (cnt > 60)
{
fprintf (stderr,
"\nFailed to start taler-exchange-httpd\n");
GNUNET_OS_process_kill (exchanged,
SIGKILL);
GNUNET_OS_process_wait (exchanged);
GNUNET_OS_process_destroy (exchanged);
return 77;
}
}
while (0 != system ("wget -q -t 1 -T 1 " EXCHANGE_URI "keys -o /dev/null -O /dev/null"));
fprintf (stderr, "\n");
merchantd = GNUNET_OS_start_process (GNUNET_NO,
GNUNET_OS_INHERIT_STD_ALL,
NULL, NULL, NULL,
"taler-merchant-httpd",
"taler-merchant-httpd",
"-c", "test_merchant_api.conf",
NULL);
if (NULL == merchantd)
{
fprintf (stderr,
"Failed to run taler-merchant-httpd. Check your PATH.\n");
GNUNET_OS_process_kill (exchanged,
SIGKILL);
GNUNET_OS_process_wait (exchanged);
GNUNET_OS_process_destroy (exchanged);
return 77;
}
/* give child time to start and bind against the socket */
fprintf (stderr,
"Waiting for taler-merchant-httpd to be ready\n");
cnt = 0;
do
{
fprintf (stderr, ".");
sleep (1);
cnt++;
if (cnt > 60)
{
fprintf (stderr,
"\nFailed to start taler-merchant-httpd\n");
GNUNET_OS_process_kill (merchantd,
SIGKILL);
GNUNET_OS_process_wait (merchantd);
GNUNET_OS_process_destroy (merchantd);
GNUNET_OS_process_kill (exchanged,
SIGKILL);
GNUNET_OS_process_wait (exchanged);
GNUNET_OS_process_destroy (exchanged);
return 77;
}
}
while (0 != system ("wget -q -t 1 -T 1 " MERCHANT_URI " -o /dev/null -O /dev/null"));
fprintf (stderr, "\n");
result = GNUNET_SYSERR;
sigpipe = GNUNET_DISK_pipe (GNUNET_NO, GNUNET_NO, GNUNET_NO, GNUNET_NO);
GNUNET_assert (NULL != sigpipe);
shc_chld = GNUNET_SIGNAL_handler_install (GNUNET_SIGCHLD,
&sighandler_child_death);
GNUNET_SCHEDULER_run (&run, NULL);
GNUNET_SIGNAL_handler_uninstall (shc_chld);
shc_chld = NULL;
GNUNET_DISK_pipe_close (sigpipe);
GNUNET_OS_process_kill (merchantd,
SIGTERM);
GNUNET_OS_process_wait (merchantd);
GNUNET_OS_process_destroy (merchantd);
GNUNET_OS_process_kill (exchanged,
SIGTERM);
GNUNET_OS_process_wait (exchanged);
GNUNET_OS_process_destroy (exchanged);
if (77 == result)
return 77;
return (GNUNET_OK == result) ? 0 : 1;
}
/* end of test_merchant_api.c */