merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

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:
Msrc/backenddb/test_merchantdb.c | 269+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
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 ());