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:
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,