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 }