merchant

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

test_merchant_kyccheck.c (9709B)


      1 /*
      2    This file is part of TALER
      3    Copyright (C) 2026 Taler Systems SA
      4 
      5    TALER is free software; you can redistribute it and/or modify it under the
      6    terms of the GNU General Public License as published by the Free Software
      7    Foundation; either version 3, or (at your option) any later version.
      8 
      9    TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10    WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11    A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12 
     13    You should have received a copy of the GNU General Public License along with
     14    TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15  */
     16 /**
     17  * @file backend/test_merchant_kyccheck.c
     18  * @brief Exercise refresh scheduling and database-backed status transitions.
     19  */
     20 /* Inject successive /keys snapshots while using the real database helpers. */
     21 #define TALER_MERCHANTDB_get_exchange_keys test_get_exchange_keys
     22 #define main kyccheck_main
     23 #include "taler-merchant-kyccheck.c"
     24 #undef main
     25 #undef TALER_MERCHANTDB_get_exchange_keys
     26 
     27 static struct TALER_EXCHANGE_Keys *next_keys;
     28 
     29 
     30 enum GNUNET_DB_QueryStatus
     31 test_get_exchange_keys (struct TALER_MERCHANTDB_PostgresContext *db,
     32                         const char *exchange_url,
     33                         struct GNUNET_TIME_Absolute *first_retry,
     34                         struct TALER_EXCHANGE_Keys **keys)
     35 {
     36   (void) db;
     37   GNUNET_assert (0 == strcmp (exchange_url, next_keys->exchange_url));
     38   *first_retry = GNUNET_TIME_UNIT_ZERO_ABS;
     39   *keys = TALER_EXCHANGE_keys_incref (next_keys);
     40   return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
     41 }
     42 
     43 
     44 /* Called by the HTTP fixture with its disposable database configuration. */
     45 static void
     46 check_database_transition (void *cls)
     47 {
     48   const char *mode = cls;
     49   struct TALER_EXCHANGE_WireAccount wa = {
     50     .fpayto_uri = {
     51       .full_payto = "payto://x-taler-bank/localhost/exchange?receiver-name=Exchange"
     52     }
     53   };
     54   struct TALER_EXCHANGE_Keys eligible = {
     55     .exchange_url = "http://localhost:1/",
     56     .accounts = &wa,
     57     .accounts_len = 1,
     58     .rc = 1
     59   };
     60   struct TALER_EXCHANGE_Keys ineligible = {
     61     .exchange_url = eligible.exchange_url,
     62     .rc = 1
     63   };
     64   struct Account a = {
     65     .instance_id = "test-1",
     66     .merchant_account_uri = {
     67       .full_payto = "payto://x-taler-bank/localhost/account-1?receiver-name=Test"
     68     }
     69   };
     70 
     71   pg = TALER_MERCHANTDB_connect (cfg);
     72   GNUNET_assert (NULL != pg);
     73   memset (&a.h_wire, 42, sizeof (a.h_wire));
     74   if (0 == strcmp (mode, "tos-conflict"))
     75   {
     76     struct Exchange e = { .keys = &eligible };
     77     struct Inquiry i = {
     78       .a = &a,
     79       .e = &e,
     80       .tos_etag = GNUNET_strdup ("v2")
     81     };
     82     const struct TALER_EXCHANGE_PostKycUploadResponse response = {
     83       .hr.http_status = MHD_HTTP_CONFLICT
     84     };
     85 
     86     active_inquiries = 1;
     87     tos_upload_cb (&i, &response);
     88     GNUNET_assert (0 == active_inquiries);
     89     GNUNET_assert (NULL != i.task);
     90     GNUNET_SCHEDULER_cancel (i.task);
     91   }
     92   else
     93   {
     94     struct Account inactive = {
     95       .instance_id = "test-2",
     96       .merchant_account_uri = a.merchant_account_uri,
     97       .h_wire = a.h_wire,
     98       .account_gen = 1 /* differs from database_gen; discovery removed it */
     99     };
    100 
    101     GNUNET_assert (0 == strcmp (mode, "eligibility"));
    102     inquiry_map = GNUNET_CONTAINER_multihashmap_create (8, GNUNET_YES);
    103     ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, &rc);
    104     rc = GNUNET_CURL_gnunet_rc_create (ctx);
    105     a_head = a_tail = &a;
    106     GNUNET_CONTAINER_DLL_insert (a_head, a_tail, &inactive);
    107     /* Pending discovery must not turn an unindexed account ineligible. */
    108     force_check_now (NULL, "test-2", eligible.exchange_url, &a.h_wire);
    109     next_keys = &eligible;
    110     find_keys (eligible.exchange_url);
    111     GNUNET_assert (NULL != a.i_head);
    112     GNUNET_assert (a.i_head->kyc_ok);
    113     next_keys = &ineligible;
    114     find_keys (eligible.exchange_url);
    115     GNUNET_assert (NULL == a.i_head);
    116     GNUNET_assert (0 == GNUNET_CONTAINER_multihashmap_size (inquiry_map));
    117     /* Repeated refreshes while ineligible must not recreate an inquiry. */
    118     force_check_now (NULL, a.instance_id, eligible.exchange_url, &a.h_wire);
    119     GNUNET_assert (NULL == a.i_head);
    120     next_keys = &eligible;
    121     find_keys (eligible.exchange_url);
    122     GNUNET_assert (NULL != a.i_head);
    123     GNUNET_assert (TALER_EC_MERCHANT_PRIVATE_ACCOUNT_NOT_ELIGIBLE_FOR_EXCHANGE
    124                    == a.i_head->last_ec);
    125     GNUNET_assert (0 == a.i_head->backoff.rel_value_us);
    126     GNUNET_assert (NULL != a.i_head->kyc);
    127     GNUNET_assert (! a.i_head->not_first_time);
    128     /* Cancel before performing network I/O; keep the persisted status for
    129        the fixture's HTTP assertion. */
    130     stop_inquiries (&a);
    131     TALER_EXCHANGE_keys_decref (e_head->keys);
    132     GNUNET_free (e_head);
    133     e_tail = NULL;
    134     a_head = a_tail = NULL;
    135     GNUNET_CONTAINER_multihashmap_destroy (inquiry_map);
    136     GNUNET_CURL_gnunet_rc_destroy (rc);
    137     GNUNET_CURL_fini (ctx);
    138   }
    139   GNUNET_assert (EXIT_SUCCESS == global_ret);
    140   TALER_MERCHANTDB_disconnect (pg);
    141   GNUNET_SCHEDULER_shutdown ();
    142 }
    143 
    144 
    145 static void
    146 check_scheduling (void *cls)
    147 {
    148   struct TALER_EXCHANGE_Keys keys = {
    149     .exchange_url = "http://localhost:1/"
    150   };
    151   struct Exchange e = { .keys = &keys };
    152   struct Account a = {
    153     .instance_id = "test",
    154     .merchant_account_uri = { .full_payto = "payto://x-taler-bank/localhost/test" }
    155   };
    156   struct Inquiry *i = GNUNET_new (struct Inquiry);
    157   struct GNUNET_HashCode other;
    158   uint64_t serial = GNUNET_htonll (42);
    159   uint64_t serial2 = GNUNET_htonll (43);
    160 
    161   (void) cls;
    162   inquiry_map = GNUNET_CONTAINER_multihashmap_create (8, GNUNET_YES);
    163   refresh_map = GNUNET_CONTAINER_multihashmap_create (8, GNUNET_YES);
    164   i->a = &a;
    165   i->e = &e;
    166   inquiry_key (a.instance_id, &a.h_wire, keys.exchange_url, &i->key);
    167   inquiry_key ("different-instance", &a.h_wire, keys.exchange_url, &other);
    168   GNUNET_assert (0 != GNUNET_memcmp (&i->key, &other));
    169   GNUNET_CONTAINER_DLL_insert (a.i_head, a.i_tail, i);
    170   GNUNET_assert (GNUNET_OK == GNUNET_CONTAINER_multihashmap_put (
    171                    inquiry_map, &i->key, i,
    172                    GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY));
    173 
    174   /* Repeated notifications for an instance are processed once per pending
    175      batch, and distinct instances retain their own requests. */
    176   update_forced (NULL, &serial, sizeof (serial));
    177   update_forced (NULL, &serial, sizeof (serial));
    178   update_forced (NULL, &serial2, sizeof (serial2));
    179   GNUNET_assert (2 == GNUNET_CONTAINER_multihashmap_size (refresh_map));
    180   GNUNET_assert (42 == refresh_head->merchant_serial);
    181   GNUNET_assert (43 == refresh_tail->merchant_serial);
    182   update_forced (NULL, NULL, 0);
    183   update_forced (NULL, &serial, 1);
    184   serial = 0;
    185   update_forced (NULL, &serial, sizeof (serial));
    186   GNUNET_assert (2 == GNUNET_CONTAINER_multihashmap_size (refresh_map));
    187   GNUNET_assert (EXIT_SUCCESS == global_ret);
    188   GNUNET_SCHEDULER_cancel (refresh_task);
    189   refresh_task = NULL;
    190 
    191   /* Indexed lookup affects only the selected inquiry, with one pending task. */
    192   force_check_now (NULL, "different-instance", keys.exchange_url, &a.h_wire);
    193   GNUNET_assert (NULL == i->task);
    194   force_check_now (NULL, a.instance_id, keys.exchange_url, &a.h_wire);
    195   GNUNET_assert (NULL != i->task);
    196   force_check_now (NULL, a.instance_id, keys.exchange_url, &a.h_wire);
    197   GNUNET_assert (NULL != i->task);
    198   GNUNET_SCHEDULER_cancel (i->task);
    199   i->task = NULL;
    200 
    201   /* All stages of an active exchange interaction share the same slot. The
    202      sentinels are never dereferenced and are cleared before cleanup. */
    203   i->kyc = (void *) i;
    204   request_inquiry (i);
    205   GNUNET_assert (NULL == i->task);
    206   i->kyc = NULL;
    207   i->kyc_info = (void *) i;
    208   request_inquiry (i);
    209   GNUNET_assert (NULL == i->task);
    210   i->kyc_info = NULL;
    211   i->tos_upload = (void *) i;
    212   request_inquiry (i);
    213   GNUNET_assert (NULL == i->task);
    214   i->tos_upload = NULL;
    215 
    216   active_inquiries = OPEN_INQUIRY_LIMIT;
    217   inquiry_work (i);
    218   GNUNET_assert (i->limited && at_limit);
    219   request_inquiry (i);
    220   GNUNET_assert (NULL == i->task);
    221   GNUNET_assert (i->limited);
    222 
    223   /* Releasing slots resumes the limited inquiry exactly once. Cancel the
    224      newly created HTTP operation before the scheduler performs network I/O. */
    225   ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, &rc);
    226   rc = GNUNET_CURL_gnunet_rc_create (ctx);
    227   a_head = &a;
    228   a_tail = &a;
    229   active_inquiries = OPEN_INQUIRY_LIMIT / 2;
    230   end_inquiry ();
    231   GNUNET_assert (! i->limited);
    232   GNUNET_assert (NULL != i->kyc);
    233   GNUNET_assert (OPEN_INQUIRY_LIMIT / 2 == active_inquiries);
    234   request_inquiry (i);
    235   GNUNET_assert (NULL == i->task);
    236   stop_inquiry (i);
    237   GNUNET_assert (0 == GNUNET_CONTAINER_multihashmap_size (inquiry_map));
    238   GNUNET_CURL_gnunet_rc_destroy (rc);
    239   GNUNET_CURL_fini (ctx);
    240   while (NULL != refresh_head)
    241   {
    242     struct Refresh *r = refresh_head;
    243 
    244     GNUNET_CONTAINER_DLL_remove (refresh_head, refresh_tail, r);
    245     GNUNET_free (r);
    246   }
    247   GNUNET_CONTAINER_multihashmap_destroy (refresh_map);
    248   GNUNET_CONTAINER_multihashmap_destroy (inquiry_map);
    249   a_head = a_tail = NULL;
    250   GNUNET_SCHEDULER_shutdown ();
    251 }
    252 
    253 
    254 int
    255 main (int argc, char *const argv[])
    256 {
    257   GNUNET_log_setup (argv[0], "WARNING", NULL);
    258   if (3 == argc)
    259   {
    260     struct GNUNET_CONFIGURATION_Handle *config =
    261       GNUNET_CONFIGURATION_create (TALER_MERCHANT_project_data ());
    262 
    263     GNUNET_assert (GNUNET_OK == GNUNET_CONFIGURATION_load (config, argv[1]));
    264     cfg = config;
    265     GNUNET_SCHEDULER_run (&check_database_transition, (void *) argv[2]);
    266     GNUNET_CONFIGURATION_destroy (config);
    267     return global_ret;
    268   }
    269   GNUNET_assert (1 == argc);
    270   GNUNET_SCHEDULER_run (&check_scheduling, NULL);
    271   return 0;
    272 }