commit 03b6d9a18feec688d7cfd4146eb821c04aaddac6
parent 0717d80bce5c48881adc503f80f7ca4e4ad8c5b4
Author: Florian Dold <dold@taler.net>
Date: Mon, 31 Aug 2026 20:02:35 +0200
backenddb: test order-settled webhook wakeup
Diffstat:
1 file changed, 222 insertions(+), 47 deletions(-)
diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c
@@ -25,6 +25,7 @@
#include <taler/taler_util.h>
#include <taler/taler_json_lib.h>
#include <taler/taler_pq_lib.h>
+#include <taler/taler_dbevents.h>
#include <taler/taler_signatures.h>
#include "taler/taler_merchant_util.h"
#include "merchantdb_lib.h"
@@ -117,6 +118,7 @@
#include "merchant-database/update_webhook.h"
#include "merchant-database/create_tables.h"
#include "merchant-database/drop_tables.h"
+#include "merchant-database/event_listen.h"
#include "merchant-database/preflight.h"
#include "merchant-database/delete_instance.h"
@@ -6415,6 +6417,38 @@ test_insert_transfer_details_settling (
/**
+ * State used to count notifications that wake the webhook helper.
+ */
+struct WebhookPendingEventState
+{
+ /**
+ * Number of notifications received.
+ */
+ unsigned int fired;
+};
+
+
+/**
+ * Count a notification for a newly pending webhook.
+ *
+ * @param cls a `struct WebhookPendingEventState *`
+ * @param extra unused notification payload
+ * @param extra_size size of @a extra
+ */
+static void
+webhook_pending_event_cb (void *cls,
+ const void *extra,
+ size_t extra_size)
+{
+ struct WebhookPendingEventState *state = cls;
+
+ (void) extra;
+ (void) extra_size;
+ state->fired++;
+}
+
+
+/**
* Container for data used when testing transfers.
*/
struct TestTransfers_Closure
@@ -6835,53 +6869,192 @@ run_test_transfers (struct TestTransfers_Closure *cls)
GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
false));
}
- TEST_RET_ON_FAIL (test_insert_transfer_details_settling (
- &cls->instance,
- &cls->account,
- &cls->transfers[3],
- GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
- 2));
- /* A deposit that failed permanently (exchange wired the money to an
- account we do not know) must NOT make the order count as wired,
- and no order_settled notification may be generated for it. */
- TEST_RET_ON_FAIL (test_insert_order (&cls->instance,
+ {
+ static const char *webhook_id = "test-transfer-order-settled";
+ static char webhook_url[]
+ = "https://example.com/order-settled-event-test";
+ struct TALER_MERCHANTDB_WebhookDetails wb = {
+ .event_type = "order_settled",
+ .url = webhook_url,
+ .http_method = "POST",
+ .header_template = NULL,
+ .body_template = "{{order_id}}"
+ };
+ struct GNUNET_DB_EventHeaderP es = {
+ .size = htons (sizeof (es)),
+ .type = htons (TALER_DBEVENT_MERCHANT_WEBHOOK_PENDING)
+ };
+ struct WebhookPendingEventState event_state = { 0 };
+ struct GNUNET_DB_EventHandler *eh;
+ uint64_t pending_count;
+ int webhook_test_result = 1;
+
+ TEST_SET_INSTANCE (cls->instance.instance.id,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT);
+ TEST_COND_RET_ON_FAIL (
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
+ TALER_MERCHANTDB_insert_webhook (pg,
+ cls->instance.instance.id,
+ webhook_id,
+ &wb),
+ "Failed to insert order_settled webhook\n");
+ eh = TALER_MERCHANTDB_event_listen (pg,
+ &es,
+ GNUNET_TIME_UNIT_FOREVER_REL,
+ &webhook_pending_event_cb,
+ &event_state);
+ if (NULL == eh)
+ {
+ GNUNET_break (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to listen for pending webhook events\n");
+ goto webhook_cleanup;
+ }
+
+ TEST_WITH_FAIL_CLAUSE (
+ test_insert_transfer_details_settling (
+ &cls->instance,
+ &cls->account,
+ &cls->transfers[3],
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
+ 2),
+ goto webhook_listener_cleanup;
+ );
+ GNUNET_PQ_event_do_poll (pg->conn);
+ if (1 != event_state.fired)
+ {
+ GNUNET_break (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Settlement generated %u pending webhook events, expected 1\n",
+ event_state.fired);
+ goto webhook_listener_cleanup;
+ }
+ TEST_WITH_FAIL_CLAUSE (
+ query_sql_num (
+ "SELECT COUNT(*) AS num"
+ " FROM merchant.merchant_pending_webhooks"
+ " WHERE url='https://example.com/order-settled-event-test'",
+ &pending_count),
+ goto webhook_listener_cleanup;
+ );
+ if (2 != pending_count)
+ {
+ GNUNET_break (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Settlement queued %llu webhooks, expected 2\n",
+ (unsigned long long) pending_count);
+ goto webhook_listener_cleanup;
+ }
+
+ /* A deposit that failed permanently (exchange wired the money to an
+ account we do not know) must NOT make the order count as wired,
+ queue an order_settled webhook, or wake the webhook helper. */
+ TEST_WITH_FAIL_CLAUSE (
+ test_insert_order (&cls->instance,
+ &cls->order_perm,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT),
+ goto webhook_listener_cleanup;
+ );
+ TEST_WITH_FAIL_CLAUSE (
+ test_insert_contract_terms (&cls->instance,
+ &cls->order_perm,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT),
+ goto webhook_listener_cleanup;
+ );
+ for (unsigned int i = 0; i < 2; i++)
+ TEST_WITH_FAIL_CLAUSE (
+ test_insert_deposit (&cls->instance,
+ &cls->signkey,
+ &cls->deposits_perm[i],
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT),
+ goto webhook_listener_cleanup;
+ );
+ TEST_WITH_FAIL_CLAUSE (
+ test_mark_contract_paid (&cls->instance,
+ &cls->order_perm,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT),
+ goto webhook_listener_cleanup;
+ );
+ TEST_WITH_FAIL_CLAUSE (
+ test_insert_deposit_to_transfer (&cls->instance,
+ &cls->signkey,
&cls->order_perm,
- GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
- TEST_RET_ON_FAIL (test_insert_contract_terms (&cls->instance,
- &cls->order_perm,
- GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
- for (unsigned int i = 0; i < 2; i++)
- TEST_RET_ON_FAIL (test_insert_deposit (&cls->instance,
- &cls->signkey,
- &cls->deposits_perm[i],
- GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
- TEST_RET_ON_FAIL (test_mark_contract_paid (&cls->instance,
- &cls->order_perm,
- GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
- TEST_RET_ON_FAIL (test_insert_deposit_to_transfer (&cls->instance,
- &cls->signkey,
- &cls->order_perm,
- &cls->deposits_perm[0],
- &cls->transfers[4],
- GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
- false));
- TEST_RET_ON_FAIL (test_insert_deposit_to_unknown_account (
- &cls->instance,
- &cls->signkey,
- &cls->order_perm,
- &cls->deposits_perm[1],
- &cls->transfers[4]));
- TEST_RET_ON_FAIL (test_insert_transfer_details_settling (
- &cls->instance,
- &cls->account,
- &cls->transfers[4],
- GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
- 0));
- TEST_RET_ON_FAIL (test_lookup_payment_status (cls->instance.instance.id,
- cls->order_perm.id,
- NULL,
- true,
- false));
+ &cls->deposits_perm[0],
+ &cls->transfers[4],
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
+ false),
+ goto webhook_listener_cleanup;
+ );
+ TEST_WITH_FAIL_CLAUSE (
+ test_insert_deposit_to_unknown_account (
+ &cls->instance,
+ &cls->signkey,
+ &cls->order_perm,
+ &cls->deposits_perm[1],
+ &cls->transfers[4]),
+ goto webhook_listener_cleanup;
+ );
+ TEST_WITH_FAIL_CLAUSE (
+ test_insert_transfer_details_settling (
+ &cls->instance,
+ &cls->account,
+ &cls->transfers[4],
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
+ 0),
+ goto webhook_listener_cleanup;
+ );
+ GNUNET_PQ_event_do_poll (pg->conn);
+ if (1 != event_state.fired)
+ {
+ GNUNET_break (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unsettled order generated a pending webhook event\n");
+ goto webhook_listener_cleanup;
+ }
+ TEST_WITH_FAIL_CLAUSE (
+ query_sql_num (
+ "SELECT COUNT(*) AS num"
+ " FROM merchant.merchant_pending_webhooks"
+ " WHERE url='https://example.com/order-settled-event-test'",
+ &pending_count),
+ goto webhook_listener_cleanup;
+ );
+ if (2 != pending_count)
+ {
+ GNUNET_break (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unsettled order changed the pending webhook count\n");
+ goto webhook_listener_cleanup;
+ }
+ TEST_WITH_FAIL_CLAUSE (
+ test_lookup_payment_status (cls->instance.instance.id,
+ cls->order_perm.id,
+ NULL,
+ true,
+ false),
+ goto webhook_listener_cleanup;
+ );
+ webhook_test_result = 0;
+
+webhook_listener_cleanup:
+ TALER_MERCHANTDB_event_listen_cancel (eh);
+webhook_cleanup:
+ TEST_COND_RET_ON_FAIL (
+ 0 == exec_sql (
+ "DELETE FROM merchant.merchant_pending_webhooks"
+ " WHERE url='https://example.com/order-settled-event-test'"),
+ "Failed to delete pending order_settled webhooks\n");
+ TEST_SET_INSTANCE (cls->instance.instance.id,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT);
+ TEST_COND_RET_ON_FAIL (
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
+ TALER_MERCHANTDB_delete_webhook (pg,
+ cls->instance.instance.id,
+ webhook_id),
+ "Failed to delete order_settled webhook\n");
+ if (0 != webhook_test_result)
+ return 1;
+ }
return 0;
}
@@ -10754,13 +10927,15 @@ run_tests (void)
TEST_RET_ON_FAIL (test_tokens ());
TEST_RET_ON_FAIL (test_orders ());
TEST_RET_ON_FAIL (test_deposits ());
+ /* This test relies on the global pending-webhook identity sequence not
+ having been advanced by an earlier test. */
+ TEST_RET_ON_FAIL (test_pending_webhooks ());
TEST_RET_ON_FAIL (test_transfers ());
TEST_RET_ON_FAIL (test_refunds ());
TEST_RET_ON_FAIL (test_lookup_orders_all_filters ());
TEST_RET_ON_FAIL (test_kyc ());
TEST_RET_ON_FAIL (test_templates ());
TEST_RET_ON_FAIL (test_webhooks ());
- TEST_RET_ON_FAIL (test_pending_webhooks ());
TEST_RET_ON_FAIL (test_inventory_deleted_webhook ());
TEST_RET_ON_FAIL (test_statistics ());
TEST_RET_ON_FAIL (test_mfa_challenges ());