merchant

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

commit e643045c5c3217aed5661d8b17d30c3dae80be0f
parent a01547172bb2bff08b2317c85c739da0262955fc
Author: Florian Dold <dold@taler.net>
Date:   Thu,  3 Sep 2026 21:29:36 +0200

merchantdb: test KYC failure persistence and notifications

Issue: https://bugs.taler.net/n/10761

Diffstat:
Msrc/backenddb/test_merchantdb.c | 305+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 290 insertions(+), 15 deletions(-)

diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c @@ -35,6 +35,7 @@ #include "merchant-database/iterate_statistic_bucket_amounts.h" #include "merchant-database/iterate_statistic_bucket_counters.h" #include "merchant-database/iterate_kyc_statuses.h" +#include "merchant-database/insert_kyc_failure.h" #include "merchant-database/insert_kyc_status.h" #include "merchant-database/delete_contract_terms.h" #include "merchant-database/update_to_instance_private_key_deleted.h" @@ -8234,6 +8235,293 @@ kyc_status_fail ( /** + * Expected result for a KYC failure lookup. + */ +struct KycFailureExpected +{ + /** + * Account the result must belong to. + */ + const struct TALER_MERCHANTDB_AccountDetails *account; + + /** + * Exchange the result must belong to. + */ + const char *exchange_url; + + /** + * Expected time of the failed KYC check. + */ + struct GNUNET_TIME_Timestamp timestamp; + + /** + * Expected HTTP status returned by the exchange. + */ + unsigned int exchange_http_status; + + /** + * Number of results received. + */ + unsigned int called; + + /** + * Set if a result did not match expectations. + */ + bool failed; +}; + + +/** + * Check a KYC failure record returned by iterate_kyc_statuses(). + * + * @param cls a `struct KycFailureExpected *` + * @param h_wire hash of the wire account + * @param payto_uri payto URI of the merchant account + * @param exchange_url exchange the status applies to + * @param last_check time of the last KYC check + * @param kyc_ok true if KYC was successful + * @param access_token access token, if available + * @param last_http_status HTTP status of the last check + * @param last_ec Taler error code of the last check + * @param in_aml_review true if the account is under AML review + * @param jlimits account limits, if known + */ +static void +kyc_failure_check ( + void *cls, + const struct TALER_MerchantWireHashP *h_wire, + struct TALER_FullPayto payto_uri, + const char *exchange_url, + struct GNUNET_TIME_Timestamp last_check, + bool kyc_ok, + const struct TALER_AccountAccessTokenP *access_token, + unsigned int last_http_status, + enum TALER_ErrorCode last_ec, + bool in_aml_review, + const json_t *jlimits) +{ + struct KycFailureExpected *expected = cls; + + expected->called++; + if ( (0 != GNUNET_memcmp (&expected->account->h_wire, + h_wire)) || + (0 != strcmp (expected->account->payto_uri.full_payto, + payto_uri.full_payto)) || + (0 != strcmp (expected->exchange_url, + exchange_url)) || + (GNUNET_TIME_timestamp_cmp (expected->timestamp, + !=, + last_check)) || + kyc_ok || + (NULL != access_token) || + (expected->exchange_http_status != last_http_status) || + (TALER_EC_NONE != last_ec) || + in_aml_review || + (NULL != jlimits) ) + { + GNUNET_break (0); + expected->failed = true; + } +} + + +/** + * Count a database notification. + * + * @param cls an `unsigned int *` + * @param extra unused notification payload + * @param extra_size size of @a extra + */ +static void +kyc_event_cb (void *cls, + const void *extra, + size_t extra_size) +{ + unsigned int *fired = cls; + + (void) extra; + (void) extra_size; + (*fired)++; +} + + +/** + * Test inserting and updating a failed KYC check. + * Regression test for #10761, which reported that the operation formerly + * named account_kyc_set_failed had no test coverage. + * + * @param instance instance that owns @a account + * @param account merchant account to use + * @return 0 on success, 1 otherwise + */ +static int +test_insert_kyc_failure ( + const struct InstanceData *instance, + const struct TALER_MERCHANTDB_AccountDetails *account) +{ + static const char exchange_url[] = "https://exchange2.com/"; + struct TALER_MERCHANTDB_MerchantKycStatusChangeEventP account_event = { + .header.size = htons (sizeof (account_event)), + .header.type = htons ( + TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_STATUS_CHANGED), + .h_wire = account->h_wire + }; + struct GNUNET_DB_EventHeaderP general_event = { + .size = htons (sizeof (general_event)), + .type = htons (TALER_DBEVENT_MERCHANT_KYC_STATUS_CHANGED) + }; + struct GNUNET_DB_EventHandler *account_eh = NULL; + struct GNUNET_DB_EventHandler *general_eh = NULL; + unsigned int account_events = 0; + unsigned int general_events = 0; + struct KycFailureExpected expected = { + .account = account, + .exchange_url = exchange_url, + .exchange_http_status = MHD_HTTP_BAD_GATEWAY + }; + int ret = 1; + + TEST_SET_INSTANCE (instance->instance.id, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); + account_eh = TALER_MERCHANTDB_event_listen ( + pg, + &account_event.header, + GNUNET_TIME_UNIT_FOREVER_REL, + &kyc_event_cb, + &account_events); + if (NULL == account_eh) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to listen for account KYC events\n"); + goto cleanup; + } + general_eh = TALER_MERCHANTDB_event_listen ( + pg, + &general_event, + GNUNET_TIME_UNIT_FOREVER_REL, + &kyc_event_cb, + &general_events); + if (NULL == general_eh) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to listen for general KYC events\n"); + goto cleanup; + } + + expected.timestamp = GNUNET_TIME_timestamp_get (); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_MERCHANTDB_insert_kyc_failure ( + pg, + instance->instance.id, + &account->h_wire, + exchange_url, + expected.timestamp, + expected.exchange_http_status, + false)) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Inserting KYC failure failed\n"); + goto cleanup; + } + GNUNET_PQ_event_do_poll (pg->conn); + if ( (1 != account_events) || + (1 != general_events) ) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Inserting KYC failure generated %u account and %u general" + " events, expected one each\n", + account_events, + general_events); + goto cleanup; + } + if (1 != TALER_MERCHANTDB_iterate_kyc_statuses ( + pg, + instance->instance.id, + &account->h_wire, + exchange_url, + &kyc_failure_check, + &expected)) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Looking up inserted KYC failure failed\n"); + goto cleanup; + } + if (expected.failed || + (1 != expected.called)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Inserted KYC failure did not match expectations\n"); + goto cleanup; + } + + expected.timestamp = GNUNET_TIME_timestamp_get (); + expected.exchange_http_status = MHD_HTTP_ACCEPTED; + expected.called = 0; + expected.failed = false; + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_MERCHANTDB_insert_kyc_failure ( + pg, + instance->instance.id, + &account->h_wire, + exchange_url, + expected.timestamp, + expected.exchange_http_status, + false)) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Updating KYC failure failed\n"); + goto cleanup; + } + GNUNET_PQ_event_do_poll (pg->conn); + if ( (2 != account_events) || + (2 != general_events) ) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Updating KYC failure generated %u account and %u general" + " events, expected two each\n", + account_events, + general_events); + goto cleanup; + } + if (1 != TALER_MERCHANTDB_iterate_kyc_statuses ( + pg, + instance->instance.id, + &account->h_wire, + exchange_url, + &kyc_failure_check, + &expected)) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Looking up updated KYC failure failed\n"); + goto cleanup; + } + if (expected.failed || + (1 != expected.called)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Updated KYC failure did not match expectations\n"); + goto cleanup; + } + ret = 0; + +cleanup: + if (NULL != general_eh) + TALER_MERCHANTDB_event_listen_cancel (general_eh); + if (NULL != account_eh) + TALER_MERCHANTDB_event_listen_cancel (account_eh); + return ret; +} + + +/** * Function that tests the KYC table. * * @return 0 on success, 1 otherwise. @@ -8293,21 +8581,8 @@ test_kyc (void) &alerts)); TEST_COND_RET_ON_FAIL (1 == alerts, "KYC status change did not queue an alert report\n"); - TEST_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != - TALER_MERCHANTDB_insert_kyc_status (pg, - instance.instance.id, - &account.h_wire, - "https://exchange2.com/", - now, - GNUNET_TIME_UNIT_FOREVER_ABS, - GNUNET_TIME_UNIT_HOURS, - MHD_HTTP_OK, - TALER_EC_NONE, - 42, - NULL, - NULL, - false, - false)); + TEST_RET_ON_FAIL (test_insert_kyc_failure (&instance, + &account)); TEST_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != TALER_MERCHANTDB_insert_kyc_status (pg, instance.instance.id,