commit 9617dc0e1e8de0550d59c197e822c7e7e5709bae
parent a5919709416909e9c193489cfca56013fa3958f2
Author: Christian Grothoff <grothoff@gnunet.org>
Date: Fri, 23 Jan 2026 23:38:25 +0900
misc minor bugfixes and first test for paivana templates
Diffstat:
7 files changed, 305 insertions(+), 32 deletions(-)
diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.c b/src/backend/taler-merchant-httpd_private-get-orders-ID.c
@@ -904,6 +904,16 @@ phase_check_repurchase (struct GetOrderRequestContext *gorc)
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Found already paid order %s\n",
already_paid_order_id);
+ // FIXME (discuss with Florian): it is a bit wild that we use the session ID of the *client's request* \
+ // (which we usually use to check that the right session got paid!) here to
+ // construct the payment URI when we actually _also_ have a session_id in the
+ // orders table! CG thinks we probably should change this and *ignore* the
+ // client's session ID and when building the taler://pay/ URI only use the
+ // session ID of the order's table (is that one even used right now?).
+ // Right now, test_merchant_templates.sh has to pass the PAIVANA_ID when
+ // calling this endpoint, otherwise the taler://pay/-URI would have no
+ // session ID, despite the templating mechanism having set one explicitly
+ // when instantiating the order.
taler_pay_uri = TMH_make_taler_pay_uri (gorc->sc.con,
hc->infix,
gorc->session_id,
diff --git a/src/include/taler_merchant_util.h b/src/include/taler_merchant_util.h
@@ -271,10 +271,6 @@ struct TALER_MERCHANT_TemplateContractInventory
*/
bool choose_one;
- /**
- * Template allows tips.
- */
- bool request_tip;
};
/**
@@ -333,6 +329,11 @@ struct TALER_MERCHANT_TemplateContract
bool no_amount;
/**
+ * Template allows tips.
+ */
+ bool request_tip;
+
+ /**
* Minimum age required by the template.
*/
uint32_t minimum_age;
diff --git a/src/testing/Makefile.am b/src/testing/Makefile.am
@@ -12,14 +12,15 @@ check_SCRIPTS = \
test_merchant_instance_creation.sh \
test_merchant_instance_response.sh \
test_merchant_instance_purge.sh \
- test_merchant_product_creation.sh \
- test_merchant_order_creation.sh \
test_merchant_kyc.sh \
+ test_merchant_order_creation.sh \
test_merchant_order_autocleanup.sh \
+ test_merchant_product_creation.sh \
test_merchant_statistics.sh \
- test_merchant_wirewatch.sh \
+ test_merchant_templates.sh \
+ test_merchant_transfer_tracking.sh \
test-merchant-walletharness.sh \
- test_merchant_transfer_tracking.sh
+ test_merchant_wirewatch.sh
lib_LTLIBRARIES = \
libtalermerchanttesting.la
diff --git a/src/testing/test_merchant_order_creation.sh b/src/testing/test_merchant_order_creation.sh
@@ -374,12 +374,14 @@ fi
QUANTITY=$(jq -r .contract_terms.products[0].quantity < "$LAST_RESPONSE")
if [ "$QUANTITY" != "1" ]
then
+ cat < "$LAST_RESPONSE" >&2
exit_fail "Expected quantity 1. got: $QUANTITY"
fi
IMAGE=$(jq -r .contract_terms.products[0].image < "$LAST_RESPONSE")
if [ "$IMAGE" != "$RANDOM_IMG" ]
then
+ cat "$LAST_RESPONSE" >&2
exit_fail "Expected $RANDOM_IMG but got something else: $IMAGE"
fi
echo "OK"
@@ -472,7 +474,7 @@ STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}" \
if [ "$STATUS" != "200" ]
then
jq . < "$LAST_RESPONSE"
- exit_fail "Expected 200, getting order info before claming it. got: $STATUS"
+ exit_fail "Expected 200, getting order info. got: $STATUS"
fi
PAY_URL=$(jq -e -r .taler_pay_uri < "$LAST_RESPONSE")
diff --git a/src/testing/test_merchant_templates.sh b/src/testing/test_merchant_templates.sh
@@ -0,0 +1,235 @@
+#!/bin/bash
+# This file is in the public domain.
+
+set -eu
+
+function clean_wallet() {
+ rm -f "${WALLET_DB}"
+ exit_cleanup
+}
+
+
+# Replace with 0 for nexus...
+USE_FAKEBANK=1
+if [ 1 = "$USE_FAKEBANK" ]
+then
+ ACCOUNT="exchange-account-2"
+ BANK_FLAGS="-f -d x-taler-bank -u $ACCOUNT"
+ BANK_URL="http://localhost:8082/"
+else
+ ACCOUNT="exchange-account-1"
+ BANK_FLAGS="-ns -d iban -u $ACCOUNT"
+ BANK_URL="http://localhost:18082/"
+ echo -n "Testing for libeufin-bank"
+ libeufin-bank --help >/dev/null </dev/null || exit_skip " MISSING"
+ echo " FOUND"
+
+fi
+
+. setup.sh
+
+echo -n "Testing for taler-harness"
+taler-harness --help >/dev/null </dev/null || exit_skip " MISSING"
+echo " FOUND"
+
+# Launch exchange, merchant and bank.
+setup -c "test_template.conf" \
+ -r "merchant-exchange-default" \
+ -em \
+ $BANK_FLAGS
+LAST_RESPONSE=$(mktemp -p "${TMPDIR:-/tmp}" test_response.conf-XXXXXX)
+CONF="test_template.conf.edited"
+WALLET_DB=$(mktemp -p "${TMPDIR:-/tmp}" test_wallet.json-XXXXXX)
+EXCHANGE_URL="http://localhost:8081/"
+
+# Install cleanup handler (except for kill -9)
+trap clean_wallet EXIT
+
+echo -n "First prepare wallet with coins ..."
+rm -f "$WALLET_DB"
+taler-wallet-cli \
+ --no-throttle \
+ --wallet-db="$WALLET_DB" \
+ api \
+ --expect-success 'withdrawTestBalance' \
+ "$(jq -n '
+ {
+ amount: "TESTKUDOS:99",
+ corebankApiBaseUrl: $BANK_URL,
+ exchangeBaseUrl: $EXCHANGE_URL
+ }' \
+ --arg BANK_URL "${BANK_URL}" \
+ --arg EXCHANGE_URL "$EXCHANGE_URL"
+ )" 2>wallet-withdraw-1.err >wallet-withdraw-1.out
+echo -n "."
+# FIXME-MS: add logic to have nexus check immediately here.
+# sleep 10
+echo -n "."
+# NOTE: once libeufin can do long-polling, we should
+# be able to reduce the delay here and run wirewatch
+# always in the background via setup
+taler-exchange-wirewatch \
+ -a "$ACCOUNT" \
+ -L "INFO" \
+ -c "$CONF" \
+ -t &> taler-exchange-wirewatch.out
+echo -n "."
+taler-wallet-cli \
+ --wallet-db="$WALLET_DB" \
+ run-until-done \
+ 2>wallet-withdraw-finish-1.err \
+ >wallet-withdraw-finish-1.out
+echo " OK"
+
+CURRENCY_COUNT=$(taler-wallet-cli --wallet-db="$WALLET_DB" balance | jq '.balances|length')
+if [ "$CURRENCY_COUNT" = "0" ]
+then
+ exit_fail "Expected least one currency, withdrawal failed. check log."
+fi
+
+#
+# CREATE INSTANCE FOR TESTING
+#
+
+echo -n "Configuring merchant instance ..."
+
+STATUS=$(curl -H "Content-Type: application/json" -X POST \
+ -H 'Authorization: Bearer secret-token:super_secret' \
+ http://localhost:9966/management/instances \
+ -d '{"auth":{"method":"external"},"id":"admin","name":"default","user_type":"business","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 50000000000},"default_pay_delay":{"d_us": 60000000000}}' \
+ -w "%{http_code}" -s -o /dev/null)
+
+if [ "$STATUS" != "204" ]
+then
+ exit_fail "Expected '204 No content' response. Got instead $STATUS"
+fi
+echo "Ok"
+
+echo -n "Configuring merchant bank account ..."
+
+if [ 1 = "$USE_FAKEBANK" ]
+then
+ FORTYTHREE="payto://x-taler-bank/localhost/fortythree?receiver-name=fortythree"
+else
+ FORTYTHREE=$(get_payto_uri fortythree x)
+fi
+# add bank account address
+STATUS=$(curl -H "Content-Type: application/json" -X POST \
+ -H 'Authorization: Bearer secret-token:super_secret' \
+ http://localhost:9966/private/accounts \
+ -d '{"payto_uri":"'"$FORTYTHREE"'"}' \
+ -w "%{http_code}" -s -o /dev/null)
+
+if [ "$STATUS" != "200" ]
+then
+ exit_fail "Expected '200 OK' response. Got instead $STATUS"
+fi
+echo "Ok"
+
+
+echo -n "Creating Paivana template..."
+TID="paivana"
+STATUS=$(curl 'http://localhost:9966/private/templates' \
+ -d '{"template_id":"paivana","template_description":"A Paivana template","template_contract":{"template_type":"paivana","summary":"The summary","choices":[{"amount":"TESTKUDOS:1"}]}}' \
+ -w "%{http_code}" -s -o "$LAST_RESPONSE")
+
+if [ "$STATUS" != "204" ]
+then
+ cat "$LAST_RESPONSE" >&2
+ exit_fail "Expected 204, template created. got: $STATUS"
+fi
+
+echo "Checking template data ..."
+STATUS=$(curl http://localhost:9966/templates/"$TID" \
+ -w "%{http_code}" -s -o "$LAST_RESPONSE")
+
+AMOUNT=$(jq -r .template_contract.choices[0].amount < "$LAST_RESPONSE")
+if [ "$AMOUNT" != "TESTKUDOS:1" ]
+then
+ cat "$LAST_RESPONSE" >&2
+ exit_fail "Expected TESTKUDOS:1. Got: $AMOUNT"
+fi
+echo " OK"
+
+echo -n "Creating order using template..."
+PAIVANA_ID="4321-4ZAXW7M5WW09QNZDF4MRAXBKS1321QQEBFVSWD68FCXHXS1G04A4ZAXW7M5WW09QNZDF4MRAXBKS1321QQEBFVSWD68FCXHXS1G04A0"
+STATUS=$(curl 'http://localhost:9966/templates/'"$TID" \
+ -d '{"template_type":"paivana","tip":"TESTKUDOS:0.1","website":"https://example.com/","paivana_id":"'"${PAIVANA_ID}"'"}' \
+ -w "%{http_code}" -s -o "$LAST_RESPONSE")
+
+if [ "$STATUS" != "200" ]
+then
+ cat "$LAST_RESPONSE" >&2
+ exit_fail "Expected 200, order created. got: $STATUS"
+fi
+
+ORDER_ID=$(jq -r .order_id < "$LAST_RESPONSE")
+TOKEN=$(jq -r .token < "$LAST_RESPONSE")
+
+if [ "$TOKEN" == "null" ]
+then
+ cat "$LAST_RESPONSE" >&2
+ exit_fail "token should not be null, got: $TOKEN"
+fi
+
+echo "OK"
+
+echo -n "Fetching order details ..."
+STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}?session_id=${PAIVANA_ID}" \
+ -w "%{http_code}" -s -o "$LAST_RESPONSE")
+
+if [ "$STATUS" != "200" ]
+then
+ cat "$LAST_RESPONSE" >&2
+ exit_fail "Expected 200, getting order info. got: $STATUS"
+fi
+
+PRICE=$(jq -r .total_amount < "$LAST_RESPONSE")
+if [ "$PRICE" != "TESTKUDOS:1.1" ]
+then
+ cat "$LAST_RESPONSE" >&2
+ exit_fail "Expected TESTKUDOS:1.1 (with tip) but got: $PRICE"
+fi
+
+PAY_URL=$(jq -e -r .taler_pay_uri < "$LAST_RESPONSE")
+echo "OK: $PAY_URL"
+
+
+NOW=$(date +%s)
+
+echo -n "Pay first order ${PAY_URL} ..."
+# echo "0" to tell wallet to use choice #0
+echo "0" | \
+ taler-wallet-cli \
+ --no-throttle \
+ --wallet-db="$WALLET_DB" \
+ handle-uri "${PAY_URL}" \
+ -y 2> wallet-pay1.err > wallet-pay1.log
+
+taler-wallet-cli \
+ --no-throttle \
+ --wallet-db="$WALLET_DB" \
+ run-until-done 2> wallet-finish-pay1.err > wallet-finish-pay1.log
+NOW2=$(date +%s)
+echo " OK (took $(( NOW2 - NOW )) secs )"
+
+# Check payment status AND binding to session ID.
+STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}?session_id=${PAIVANA_ID}" \
+ -w "%{http_code}" -s -o "$LAST_RESPONSE")
+
+if [ "$STATUS" != "200" ]
+then
+ cat "$LAST_RESPONSE" >&2
+ exit_fail "Expected 200, after pay. got: $STATUS"
+fi
+
+ORDER_STATUS=$(jq -r .order_status < "$LAST_RESPONSE")
+
+if [ "$ORDER_STATUS" != "paid" ]
+then
+ cat "$LAST_RESPONSE" >&2
+ exit_fail "Order status should be 'paid'. got: $ORDER_STATUS"
+fi
+
+echo "TEST PASSED"
+exit 0
diff --git a/src/util/contract_parse.c b/src/util/contract_parse.c
@@ -375,8 +375,8 @@ parse_choices (
for (unsigned int i = 0; i < *choices_len; i++)
{
struct TALER_MERCHANT_ContractChoice *choice = &(*choices)[i];
- const json_t *jinputs;
- const json_t *joutputs;
+ const json_t *jinputs = NULL;
+ const json_t *joutputs = NULL;
struct GNUNET_JSON_Specification spec[] = {
TALER_JSON_spec_amount_any ("amount",
&choice->amount),
@@ -388,12 +388,18 @@ parse_choices (
GNUNET_JSON_spec_object_copy ("description_i18n",
&choice->description_i18n),
NULL),
- TALER_JSON_spec_amount_any ("max_fee",
- &choice->max_fee),
- GNUNET_JSON_spec_array_const ("inputs",
- &jinputs),
- GNUNET_JSON_spec_array_const ("outputs",
- &joutputs),
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_amount_any ("max_fee",
+ &choice->max_fee),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_array_const ("inputs",
+ &jinputs),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_array_const ("outputs",
+ &joutputs),
+ NULL),
GNUNET_JSON_spec_end ()
};
const char *ename;
@@ -414,6 +420,7 @@ parse_choices (
return GNUNET_SYSERR;
}
+ if (NULL != jinputs)
{
const json_t *jinput;
size_t idx;
@@ -450,6 +457,7 @@ parse_choices (
}
}
+ if (NULL != joutputs)
{
const json_t *joutput;
size_t idx;
diff --git a/src/util/template_parse.c b/src/util/template_parse.c
@@ -100,10 +100,6 @@ parse_template_inventory (const json_t *template_contract,
{
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_bool ("request_tip",
- &out->details.inventory.request_tip),
- NULL),
- GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_bool ("selected_all",
&out->details.inventory.selected_all),
NULL),
@@ -122,16 +118,19 @@ parse_template_inventory (const json_t *template_contract,
NULL),
GNUNET_JSON_spec_end ()
};
+ const char *en;
if (GNUNET_OK !=
GNUNET_JSON_parse ((json_t *) template_contract,
spec,
- error_name,
+ &en,
NULL))
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Invalid inventory template_contract for field %s\n",
- *error_name);
+ en);
+ if (NULL != error_name)
+ *error_name = en;
return GNUNET_SYSERR;
}
@@ -148,7 +147,8 @@ parse_template_inventory (const json_t *template_contract,
(0 > json_integer_value (entry)) )
{
GNUNET_break_op (0);
- *error_name = "selected_categories";
+ if (NULL != error_name)
+ *error_name = "selected_categories";
return GNUNET_SYSERR;
}
}
@@ -166,7 +166,8 @@ parse_template_inventory (const json_t *template_contract,
if (! json_is_string (entry))
{
GNUNET_break_op (0);
- *error_name = "selected_products";
+ if (NULL != error_name)
+ *error_name = "selected_products";
return GNUNET_SYSERR;
}
}
@@ -198,16 +199,19 @@ parse_template_paivana (const json_t *template_contract,
&out->details.paivana.choices_len),
GNUNET_JSON_spec_end ()
};
+ const char *en;
if (GNUNET_OK !=
GNUNET_JSON_parse ((json_t *) template_contract,
spec,
- error_name,
+ &en,
NULL))
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Invalid paivana template_contract for field %s\n",
- *error_name);
+ en);
+ if (NULL != error_name)
+ *error_name = en;
return GNUNET_SYSERR;
}
if (NULL != out->details.paivana.website_regex)
@@ -221,6 +225,8 @@ parse_template_paivana (const json_t *template_contract,
GNUNET_break_op (0);
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Invalid paivana website_regex given\n");
+ if (NULL != error_name)
+ *error_name = "Invalid website_regex given";
return GNUNET_SYSERR;
}
regfree (&ex);
@@ -261,24 +267,32 @@ TALER_MERCHANT_template_contract_parse (
GNUNET_JSON_spec_relative_time ("pay_duration",
&out->pay_duration),
NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_bool ("request_tip",
+ &out->request_tip),
+ NULL),
GNUNET_JSON_spec_end ()
};
+ const char *en;
if (NULL == template_contract)
{
- *error_name = "template_contract is NULL";
+ if (NULL != error_name)
+ *error_name = "template_contract is NULL";
return GNUNET_SYSERR;
}
if (GNUNET_OK !=
GNUNET_JSON_parse ((json_t *) template_contract,
spec,
- error_name,
+ &en,
NULL))
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Invalid input for field %s\n",
- *error_name);
+ en);
+ if (NULL != error_name)
+ *error_name = en;
return GNUNET_SYSERR;
}
@@ -288,7 +302,8 @@ TALER_MERCHANT_template_contract_parse (
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Invalid template_type used '%s'\n",
template_type_str);
- *error_name = "Invalid template_type used";
+ if (NULL != error_name)
+ *error_name = "Invalid template_type used";
return GNUNET_SYSERR;
}
@@ -311,7 +326,8 @@ TALER_MERCHANT_template_contract_parse (
/* I think we are never supposed to reach it */
GNUNET_break_op (0);
- *error_name = "template_type";
+ if (NULL != error_name)
+ *error_name = "template_type";
return GNUNET_SYSERR;
}