Line data Source code
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 0 : 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 0 : GNUNET_assert (0 == strcmp (exchange_url, next_keys->exchange_url));
38 0 : *first_retry = GNUNET_TIME_UNIT_ZERO_ABS;
39 0 : *keys = TALER_EXCHANGE_keys_incref (next_keys);
40 0 : 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 0 : check_database_transition (void *cls)
47 : {
48 0 : const char *mode = cls;
49 0 : struct TALER_EXCHANGE_WireAccount wa = {
50 : .fpayto_uri = {
51 : .full_payto = "payto://x-taler-bank/localhost/exchange?receiver-name=Exchange"
52 : }
53 : };
54 0 : struct TALER_EXCHANGE_Keys eligible = {
55 : .exchange_url = "http://localhost:1/",
56 : .accounts = &wa,
57 : .accounts_len = 1,
58 : .rc = 1
59 : };
60 0 : struct TALER_EXCHANGE_Keys ineligible = {
61 0 : .exchange_url = eligible.exchange_url,
62 : .rc = 1
63 : };
64 0 : 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 0 : pg = TALER_MERCHANTDB_connect (cfg);
72 0 : GNUNET_assert (NULL != pg);
73 0 : memset (&a.h_wire, 42, sizeof (a.h_wire));
74 0 : if (0 == strcmp (mode, "tos-conflict"))
75 : {
76 0 : struct Exchange e = { .keys = &eligible };
77 0 : struct Inquiry i = {
78 : .a = &a,
79 : .e = &e,
80 0 : .tos_etag = GNUNET_strdup ("v2")
81 : };
82 0 : const struct TALER_EXCHANGE_PostKycUploadResponse response = {
83 : .hr.http_status = MHD_HTTP_CONFLICT
84 : };
85 :
86 0 : active_inquiries = 1;
87 0 : tos_upload_cb (&i, &response);
88 0 : GNUNET_assert (0 == active_inquiries);
89 0 : GNUNET_assert (NULL != i.task);
90 0 : GNUNET_SCHEDULER_cancel (i.task);
91 : }
92 : else
93 : {
94 0 : 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 0 : GNUNET_assert (0 == strcmp (mode, "eligibility"));
102 0 : inquiry_map = GNUNET_CONTAINER_multihashmap_create (8, GNUNET_YES);
103 0 : ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, &rc);
104 0 : rc = GNUNET_CURL_gnunet_rc_create (ctx);
105 0 : a_head = a_tail = &a;
106 0 : GNUNET_CONTAINER_DLL_insert (a_head, a_tail, &inactive);
107 : /* Pending discovery must not turn an unindexed account ineligible. */
108 0 : force_check_now (NULL, "test-2", eligible.exchange_url, &a.h_wire);
109 0 : next_keys = &eligible;
110 0 : find_keys (eligible.exchange_url);
111 0 : GNUNET_assert (NULL != a.i_head);
112 0 : GNUNET_assert (a.i_head->kyc_ok);
113 0 : next_keys = &ineligible;
114 0 : find_keys (eligible.exchange_url);
115 0 : GNUNET_assert (NULL == a.i_head);
116 0 : GNUNET_assert (0 == GNUNET_CONTAINER_multihashmap_size (inquiry_map));
117 : /* Repeated refreshes while ineligible must not recreate an inquiry. */
118 0 : force_check_now (NULL, a.instance_id, eligible.exchange_url, &a.h_wire);
119 0 : GNUNET_assert (NULL == a.i_head);
120 0 : next_keys = &eligible;
121 0 : find_keys (eligible.exchange_url);
122 0 : GNUNET_assert (NULL != a.i_head);
123 0 : GNUNET_assert (TALER_EC_MERCHANT_PRIVATE_ACCOUNT_NOT_ELIGIBLE_FOR_EXCHANGE
124 : == a.i_head->last_ec);
125 0 : GNUNET_assert (0 == a.i_head->backoff.rel_value_us);
126 0 : GNUNET_assert (NULL != a.i_head->kyc);
127 0 : 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 0 : stop_inquiries (&a);
131 0 : TALER_EXCHANGE_keys_decref (e_head->keys);
132 0 : GNUNET_free (e_head);
133 0 : e_tail = NULL;
134 0 : a_head = a_tail = NULL;
135 0 : GNUNET_CONTAINER_multihashmap_destroy (inquiry_map);
136 0 : GNUNET_CURL_gnunet_rc_destroy (rc);
137 0 : GNUNET_CURL_fini (ctx);
138 : }
139 0 : GNUNET_assert (EXIT_SUCCESS == global_ret);
140 0 : TALER_MERCHANTDB_disconnect (pg);
141 0 : GNUNET_SCHEDULER_shutdown ();
142 0 : }
143 :
144 :
145 : static void
146 1 : check_scheduling (void *cls)
147 : {
148 1 : struct TALER_EXCHANGE_Keys keys = {
149 : .exchange_url = "http://localhost:1/"
150 : };
151 1 : struct Exchange e = { .keys = &keys };
152 1 : struct Account a = {
153 : .instance_id = "test",
154 : .merchant_account_uri = { .full_payto = "payto://x-taler-bank/localhost/test" }
155 : };
156 1 : struct Inquiry *i = GNUNET_new (struct Inquiry);
157 : struct GNUNET_HashCode other;
158 1 : uint64_t serial = GNUNET_htonll (42);
159 1 : uint64_t serial2 = GNUNET_htonll (43);
160 :
161 : (void) cls;
162 1 : inquiry_map = GNUNET_CONTAINER_multihashmap_create (8, GNUNET_YES);
163 1 : refresh_map = GNUNET_CONTAINER_multihashmap_create (8, GNUNET_YES);
164 1 : i->a = &a;
165 1 : i->e = &e;
166 1 : inquiry_key (a.instance_id, &a.h_wire, keys.exchange_url, &i->key);
167 1 : inquiry_key ("different-instance", &a.h_wire, keys.exchange_url, &other);
168 1 : GNUNET_assert (0 != GNUNET_memcmp (&i->key, &other));
169 1 : GNUNET_CONTAINER_DLL_insert (a.i_head, a.i_tail, i);
170 1 : 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 1 : update_forced (NULL, &serial, sizeof (serial));
177 1 : update_forced (NULL, &serial, sizeof (serial));
178 1 : update_forced (NULL, &serial2, sizeof (serial2));
179 1 : GNUNET_assert (2 == GNUNET_CONTAINER_multihashmap_size (refresh_map));
180 1 : GNUNET_assert (42 == refresh_head->merchant_serial);
181 1 : GNUNET_assert (43 == refresh_tail->merchant_serial);
182 1 : update_forced (NULL, NULL, 0);
183 1 : update_forced (NULL, &serial, 1);
184 1 : serial = 0;
185 1 : update_forced (NULL, &serial, sizeof (serial));
186 1 : GNUNET_assert (2 == GNUNET_CONTAINER_multihashmap_size (refresh_map));
187 1 : GNUNET_assert (EXIT_SUCCESS == global_ret);
188 1 : GNUNET_SCHEDULER_cancel (refresh_task);
189 1 : refresh_task = NULL;
190 :
191 : /* Indexed lookup affects only the selected inquiry, with one pending task. */
192 1 : force_check_now (NULL, "different-instance", keys.exchange_url, &a.h_wire);
193 1 : GNUNET_assert (NULL == i->task);
194 1 : force_check_now (NULL, a.instance_id, keys.exchange_url, &a.h_wire);
195 1 : GNUNET_assert (NULL != i->task);
196 1 : force_check_now (NULL, a.instance_id, keys.exchange_url, &a.h_wire);
197 1 : GNUNET_assert (NULL != i->task);
198 1 : GNUNET_SCHEDULER_cancel (i->task);
199 1 : 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 1 : i->kyc = (void *) i;
204 1 : request_inquiry (i);
205 1 : GNUNET_assert (NULL == i->task);
206 1 : i->kyc = NULL;
207 1 : i->kyc_info = (void *) i;
208 1 : request_inquiry (i);
209 1 : GNUNET_assert (NULL == i->task);
210 1 : i->kyc_info = NULL;
211 1 : i->tos_upload = (void *) i;
212 1 : request_inquiry (i);
213 1 : GNUNET_assert (NULL == i->task);
214 1 : i->tos_upload = NULL;
215 :
216 1 : active_inquiries = OPEN_INQUIRY_LIMIT;
217 1 : inquiry_work (i);
218 1 : GNUNET_assert (i->limited && at_limit);
219 1 : request_inquiry (i);
220 1 : GNUNET_assert (NULL == i->task);
221 1 : 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 1 : ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, &rc);
226 1 : rc = GNUNET_CURL_gnunet_rc_create (ctx);
227 1 : a_head = &a;
228 1 : a_tail = &a;
229 1 : active_inquiries = OPEN_INQUIRY_LIMIT / 2;
230 1 : end_inquiry ();
231 1 : GNUNET_assert (! i->limited);
232 1 : GNUNET_assert (NULL != i->kyc);
233 1 : GNUNET_assert (OPEN_INQUIRY_LIMIT / 2 == active_inquiries);
234 1 : request_inquiry (i);
235 1 : GNUNET_assert (NULL == i->task);
236 1 : stop_inquiry (i);
237 1 : GNUNET_assert (0 == GNUNET_CONTAINER_multihashmap_size (inquiry_map));
238 1 : GNUNET_CURL_gnunet_rc_destroy (rc);
239 1 : GNUNET_CURL_fini (ctx);
240 3 : while (NULL != refresh_head)
241 : {
242 2 : struct Refresh *r = refresh_head;
243 :
244 2 : GNUNET_CONTAINER_DLL_remove (refresh_head, refresh_tail, r);
245 2 : GNUNET_free (r);
246 : }
247 1 : GNUNET_CONTAINER_multihashmap_destroy (refresh_map);
248 1 : GNUNET_CONTAINER_multihashmap_destroy (inquiry_map);
249 1 : a_head = a_tail = NULL;
250 1 : GNUNET_SCHEDULER_shutdown ();
251 1 : }
252 :
253 :
254 : int
255 1 : main (int argc, char *const argv[])
256 : {
257 1 : GNUNET_log_setup (argv[0], "WARNING", NULL);
258 1 : if (3 == argc)
259 : {
260 : struct GNUNET_CONFIGURATION_Handle *config =
261 0 : GNUNET_CONFIGURATION_create (TALER_MERCHANT_project_data ());
262 :
263 0 : GNUNET_assert (GNUNET_OK == GNUNET_CONFIGURATION_load (config, argv[1]));
264 0 : cfg = config;
265 0 : GNUNET_SCHEDULER_run (&check_database_transition, (void *) argv[2]);
266 0 : GNUNET_CONFIGURATION_destroy (config);
267 0 : return global_ret;
268 : }
269 1 : GNUNET_assert (1 == argc);
270 1 : GNUNET_SCHEDULER_run (&check_scheduling, NULL);
271 1 : return 0;
272 : }
|