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 exchangedb/test_kyc_targets.c
18 : * @brief tests for the exchangedb functions whose primary table is
19 : * `kyc_targets`
20 : * @author Christian Grothoff
21 : *
22 : * Covers #TALER_EXCHANGEDB_get_aml_file_number(),
23 : * #TALER_EXCHANGEDB_get_h_payto_by_access_token(),
24 : * #TALER_EXCHANGEDB_update_to_aml_locked(),
25 : * #TALER_EXCHANGEDB_update_to_aml_unlocked() and
26 : * #TALER_EXCHANGEDB_iterate_kyc_accounts().
27 : *
28 : * `kyc_targets` is the exchange's list of accounts it knows in a KYC
29 : * sense, keyed by the hash of the *normalized* payto URI. No exported
30 : * function creates a row -- the deposit and credit paths do, as a side
31 : * effect -- so the checks use TDB_account() for that. The access token
32 : * is generated by the database itself, so the checks read it back out.
33 : */
34 : #include "test_common.h"
35 : #include "exchange-database/get_aml_file_number.h"
36 : #include "exchange-database/get_h_payto_by_access_token.h"
37 : #include "exchange-database/iterate_kyc_accounts.h"
38 : #include "exchange-database/update_to_aml_locked.h"
39 : #include "exchange-database/update_to_aml_unlocked.h"
40 :
41 :
42 : /**
43 : * Accounts the checks work on.
44 : */
45 : static struct TDB_Account account;
46 :
47 : /**
48 : * A second account.
49 : */
50 : static struct TDB_Account other;
51 :
52 :
53 : /**
54 : * Set the AML file lifecycle timestamps for an account.
55 : *
56 : * @param pg database context
57 : * @param acc account to update
58 : * @param opened true to set an opening timestamp
59 : * @param closed true to set a closing timestamp
60 : */
61 : static void
62 2 : set_file_lifecycle (struct TALER_EXCHANGEDB_PostgresContext *pg,
63 : const struct TDB_Account *acc,
64 : bool opened,
65 : bool closed)
66 : {
67 2 : struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
68 2 : struct GNUNET_PQ_QueryParam params[] = {
69 2 : GNUNET_PQ_query_param_auto_from_type (&acc->h_normalized),
70 : opened
71 2 : ? GNUNET_PQ_query_param_timestamp (&now)
72 2 : : GNUNET_PQ_query_param_null (),
73 : closed
74 1 : ? GNUNET_PQ_query_param_timestamp (&now)
75 2 : : GNUNET_PQ_query_param_null (),
76 : GNUNET_PQ_query_param_end
77 : };
78 :
79 2 : GNUNET_assert (GNUNET_OK ==
80 : GNUNET_PQ_prepare_anon (pg->conn,
81 : "UPDATE kyc_targets"
82 : " SET open_time=$2, close_time=$3"
83 : " WHERE h_normalized_payto=$1;"));
84 2 : GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
85 : GNUNET_PQ_eval_prepared_non_select (pg->conn,
86 : "",
87 : params));
88 2 : }
89 :
90 :
91 : /**
92 : * Read the access token the database generated for an account.
93 : *
94 : * @param pg the database context
95 : * @param acc account to look up
96 : * @param[out] access_token set to the account's access token
97 : */
98 : static void
99 1 : get_access_token (struct TALER_EXCHANGEDB_PostgresContext *pg,
100 : const struct TDB_Account *acc,
101 : struct TALER_AccountAccessTokenP *access_token)
102 : {
103 1 : struct GNUNET_PQ_QueryParam params[] = {
104 1 : GNUNET_PQ_query_param_auto_from_type (&acc->h_normalized),
105 : GNUNET_PQ_query_param_end
106 : };
107 1 : struct GNUNET_PQ_ResultSpec rs[] = {
108 1 : GNUNET_PQ_result_spec_auto_from_type ("access_token",
109 : access_token),
110 : GNUNET_PQ_result_spec_end
111 : };
112 :
113 1 : GNUNET_assert (GNUNET_OK ==
114 : GNUNET_PQ_prepare_anon (pg->conn,
115 : "SELECT access_token"
116 : " FROM kyc_targets"
117 : " WHERE h_normalized_payto=$1;"));
118 1 : GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
119 : GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
120 : "",
121 : params,
122 : rs));
123 1 : }
124 :
125 :
126 : /**
127 : * Closure for #account_cb().
128 : */
129 : struct AccountContext
130 : {
131 : /**
132 : * How many accounts did the callback see?
133 : */
134 : unsigned int total;
135 :
136 : /**
137 : * Account we are looking for, NULL to match nothing.
138 : */
139 : const struct TALER_NormalizedPaytoHashP *h_payto;
140 :
141 : /**
142 : * How many times did we see it?
143 : */
144 : unsigned int matched;
145 :
146 : /**
147 : * Row of the first account seen.
148 : */
149 : uint64_t first_row;
150 :
151 : /**
152 : * Was the matching account flagged for investigation?
153 : */
154 : bool to_investigate;
155 :
156 : /**
157 : * Was the matching account flagged high risk?
158 : */
159 : bool high_risk;
160 :
161 : /**
162 : * Was a payto URI reported for it?
163 : */
164 : bool have_payto;
165 : };
166 :
167 :
168 : /**
169 : * Callback for #TALER_EXCHANGEDB_iterate_kyc_accounts().
170 : *
171 : * @param cls a `struct AccountContext *`
172 : * @param row_id row of the account
173 : * @param h_payto the account
174 : * @param open_time when the account was opened
175 : * @param close_time when it was closed
176 : * @param comments file note on the account
177 : * @param customer_label human-readable customer label
178 : * @param high_risk whether it is a high-risk relationship
179 : * @param to_investigate whether it is flagged for investigation
180 : * @param payto the account's payto URI
181 : */
182 : static void
183 14 : account_cb (void *cls,
184 : uint64_t row_id,
185 : const struct TALER_NormalizedPaytoHashP *h_payto,
186 : struct GNUNET_TIME_Timestamp open_time,
187 : struct GNUNET_TIME_Timestamp close_time,
188 : const char *comments,
189 : const char *customer_label,
190 : bool high_risk,
191 : bool to_investigate,
192 : struct TALER_FullPayto payto)
193 : {
194 14 : struct AccountContext *ctx = cls;
195 :
196 : (void) open_time;
197 : (void) close_time;
198 : (void) comments;
199 : (void) customer_label;
200 14 : if (0 == ctx->total++)
201 9 : ctx->first_row = row_id;
202 14 : if ( (NULL != ctx->h_payto) &&
203 3 : (0 == GNUNET_memcmp (h_payto,
204 : ctx->h_payto)) )
205 : {
206 2 : ctx->matched++;
207 2 : ctx->to_investigate = to_investigate;
208 2 : ctx->high_risk = high_risk;
209 2 : ctx->have_payto = (NULL != payto.full_payto);
210 : }
211 14 : }
212 :
213 :
214 : /**
215 : * Nothing is known about an account the exchange never saw.
216 : *
217 : * @param pg the database context
218 : * @return 0 on success
219 : */
220 : static int
221 1 : check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
222 : {
223 : struct TALER_NormalizedPaytoHashP h_payto;
224 : struct TALER_AccountAccessTokenP access_token;
225 : struct GNUNET_TIME_Absolute existing_lock;
226 1 : struct AccountContext ctx = { 0 };
227 : uint64_t kyc_target_row;
228 : bool is_wallet;
229 :
230 1 : TDB_FILL (h_payto,
231 : 1);
232 1 : TDB_FILL (access_token,
233 : 1);
234 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
235 : TALER_EXCHANGEDB_get_aml_file_number (pg,
236 : &h_payto,
237 : &kyc_target_row,
238 : &is_wallet));
239 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
240 : TALER_EXCHANGEDB_get_h_payto_by_access_token (pg,
241 : &access_token,
242 : &h_payto,
243 : &is_wallet));
244 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
245 : TALER_EXCHANGEDB_update_to_aml_locked (pg,
246 : &h_payto,
247 : GNUNET_TIME_UNIT_HOURS,
248 : &existing_lock));
249 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
250 : TALER_EXCHANGEDB_update_to_aml_unlocked (pg,
251 : &h_payto));
252 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
253 : TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
254 : NULL,
255 : TALER_EXCHANGE_YNA_ALL,
256 : TALER_EXCHANGE_YNA_ALL,
257 : TALER_EXCHANGE_YNA_ALL,
258 : 0,
259 : 10,
260 : &account_cb,
261 : &ctx));
262 1 : FAILIF (0 != ctx.total);
263 1 : return 0;
264 : }
265 :
266 :
267 : /**
268 : * A known account is found by its hash and by its access token.
269 : *
270 : * @param pg the database context
271 : * @return 0 on success
272 : */
273 : static int
274 1 : check_lookup (struct TALER_EXCHANGEDB_PostgresContext *pg)
275 : {
276 : struct TALER_AccountAccessTokenP access_token;
277 : struct TALER_NormalizedPaytoHashP h_payto;
278 1 : uint64_t kyc_target_row = 0;
279 1 : bool is_wallet = true;
280 :
281 1 : TDB_account (pg,
282 : 10,
283 : &account);
284 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
285 : TALER_EXCHANGEDB_get_aml_file_number (pg,
286 : &account.h_normalized,
287 : &kyc_target_row,
288 : &is_wallet));
289 1 : FAILIF (0 == kyc_target_row);
290 : /* TDB_account() creates bank accounts, not wallet accounts */
291 1 : FAILIF (is_wallet);
292 :
293 1 : get_access_token (pg,
294 : &account,
295 : &access_token);
296 1 : is_wallet = true;
297 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
298 : TALER_EXCHANGEDB_get_h_payto_by_access_token (pg,
299 : &access_token,
300 : &h_payto,
301 : &is_wallet));
302 1 : FAILIF (0 != GNUNET_memcmp (&h_payto,
303 : &account.h_normalized));
304 1 : FAILIF (is_wallet);
305 :
306 : /* an access token nobody was given finds nothing */
307 : {
308 : struct TALER_AccountAccessTokenP other_token;
309 :
310 1 : TDB_FILL (other_token,
311 : 99);
312 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
313 : TALER_EXCHANGEDB_get_h_payto_by_access_token (pg,
314 : &other_token,
315 : &h_payto,
316 : &is_wallet));
317 : }
318 1 : return 0;
319 : }
320 :
321 :
322 : /**
323 : * Locking an account reports the lock to whoever tries next, and
324 : * unlocking clears it.
325 : *
326 : * @param pg the database context
327 : * @return 0 on success
328 : */
329 : static int
330 1 : check_lock (struct TALER_EXCHANGEDB_PostgresContext *pg)
331 : {
332 : struct GNUNET_TIME_Absolute existing_lock;
333 :
334 : /* no lock yet */
335 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
336 : TALER_EXCHANGEDB_update_to_aml_locked (pg,
337 : &account.h_normalized,
338 : GNUNET_TIME_UNIT_HOURS,
339 : &existing_lock));
340 1 : FAILIF (! GNUNET_TIME_absolute_is_zero (existing_lock));
341 :
342 : /* a second attempt is told when the first lock runs out */
343 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
344 : TALER_EXCHANGEDB_update_to_aml_locked (pg,
345 : &account.h_normalized,
346 : GNUNET_TIME_UNIT_HOURS,
347 : &existing_lock));
348 1 : FAILIF (GNUNET_TIME_absolute_is_zero (existing_lock));
349 1 : FAILIF (GNUNET_TIME_absolute_is_past (existing_lock));
350 :
351 : /* unlocking clears it */
352 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
353 : TALER_EXCHANGEDB_update_to_aml_unlocked (pg,
354 : &account.h_normalized));
355 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
356 : TALER_EXCHANGEDB_update_to_aml_locked (pg,
357 : &account.h_normalized,
358 : GNUNET_TIME_UNIT_HOURS,
359 : &existing_lock));
360 1 : FAILIF (! GNUNET_TIME_absolute_is_zero (existing_lock));
361 : /* unlocking an account that is not locked is fine */
362 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
363 : TALER_EXCHANGEDB_update_to_aml_unlocked (pg,
364 : &account.h_normalized));
365 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
366 : TALER_EXCHANGEDB_update_to_aml_unlocked (pg,
367 : &account.h_normalized));
368 1 : return 0;
369 : }
370 :
371 :
372 : /**
373 : * The account listing reports the accounts, with the filters applied.
374 : *
375 : * @param pg the database context
376 : * @return 0 on success
377 : */
378 : static int
379 1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
380 : {
381 : struct AccountContext ctx;
382 : struct TALER_NormalizedPaytoHashP unknown;
383 : uint64_t lowest;
384 :
385 1 : TDB_account (pg,
386 : 11,
387 : &other);
388 1 : memset (&ctx,
389 : 0,
390 : sizeof (ctx));
391 1 : ctx.h_payto = &account.h_normalized;
392 1 : FAILIF (2 !=
393 : TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
394 : NULL,
395 : TALER_EXCHANGE_YNA_ALL,
396 : TALER_EXCHANGE_YNA_ALL,
397 : TALER_EXCHANGE_YNA_ALL,
398 : 0,
399 : 10,
400 : &account_cb,
401 : &ctx));
402 1 : FAILIF (1 != ctx.matched);
403 1 : FAILIF (! ctx.have_payto);
404 : /* no AML decision has been taken, so neither flag is set */
405 1 : FAILIF (ctx.to_investigate);
406 1 : FAILIF (ctx.high_risk);
407 1 : lowest = ctx.first_row;
408 :
409 : /* An exact account filter returns only the requested account. */
410 1 : memset (&ctx,
411 : 0,
412 : sizeof (ctx));
413 1 : ctx.h_payto = &account.h_normalized;
414 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
415 : TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
416 : &account.h_normalized,
417 : TALER_EXCHANGE_YNA_ALL,
418 : TALER_EXCHANGE_YNA_ALL,
419 : TALER_EXCHANGE_YNA_ALL,
420 : 0,
421 : 10,
422 : &account_cb,
423 : &ctx));
424 1 : FAILIF (1 != ctx.total);
425 1 : FAILIF (1 != ctx.matched);
426 :
427 : /* An unknown exact account filter returns no result. */
428 1 : TDB_FILL (unknown,
429 : 99);
430 1 : memset (&ctx,
431 : 0,
432 : sizeof (ctx));
433 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
434 : TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
435 : &unknown,
436 : TALER_EXCHANGE_YNA_ALL,
437 : TALER_EXCHANGE_YNA_ALL,
438 : TALER_EXCHANGE_YNA_ALL,
439 : 0,
440 : 10,
441 : &account_cb,
442 : &ctx));
443 1 : FAILIF (0 != ctx.total);
444 :
445 : /* the limit caps the result set */
446 1 : memset (&ctx,
447 : 0,
448 : sizeof (ctx));
449 1 : FAILIF (1 !=
450 : TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
451 : NULL,
452 : TALER_EXCHANGE_YNA_ALL,
453 : TALER_EXCHANGE_YNA_ALL,
454 : TALER_EXCHANGE_YNA_ALL,
455 : 0,
456 : 1,
457 : &account_cb,
458 : &ctx));
459 1 : FAILIF (lowest != ctx.first_row);
460 :
461 : /* a negative limit walks the list the other way */
462 1 : memset (&ctx,
463 : 0,
464 : sizeof (ctx));
465 1 : FAILIF (2 !=
466 : TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
467 : NULL,
468 : TALER_EXCHANGE_YNA_ALL,
469 : TALER_EXCHANGE_YNA_ALL,
470 : TALER_EXCHANGE_YNA_ALL,
471 : 1000,
472 : -10,
473 : &account_cb,
474 : &ctx));
475 1 : FAILIF (lowest == ctx.first_row);
476 :
477 : /* nothing is under investigation... */
478 1 : memset (&ctx,
479 : 0,
480 : sizeof (ctx));
481 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
482 : TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
483 : NULL,
484 : TALER_EXCHANGE_YNA_YES,
485 : TALER_EXCHANGE_YNA_ALL,
486 : TALER_EXCHANGE_YNA_ALL,
487 : 0,
488 : 10,
489 : &account_cb,
490 : &ctx));
491 1 : FAILIF (0 != ctx.total);
492 : /* ...so everything comes back under "not under investigation" */
493 1 : memset (&ctx,
494 : 0,
495 : sizeof (ctx));
496 1 : FAILIF (2 !=
497 : TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
498 : NULL,
499 : TALER_EXCHANGE_YNA_NO,
500 : TALER_EXCHANGE_YNA_ALL,
501 : TALER_EXCHANGE_YNA_ALL,
502 : 0,
503 : 10,
504 : &account_cb,
505 : &ctx));
506 :
507 : /* nothing is high risk either */
508 1 : memset (&ctx,
509 : 0,
510 : sizeof (ctx));
511 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
512 : TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
513 : NULL,
514 : TALER_EXCHANGE_YNA_ALL,
515 : TALER_EXCHANGE_YNA_ALL,
516 : TALER_EXCHANGE_YNA_YES,
517 : 0,
518 : 10,
519 : &account_cb,
520 : &ctx));
521 1 : FAILIF (0 != ctx.total);
522 :
523 : /* Neither AML file has been opened yet. */
524 1 : memset (&ctx,
525 : 0,
526 : sizeof (ctx));
527 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
528 : TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
529 : NULL,
530 : TALER_EXCHANGE_YNA_ALL,
531 : TALER_EXCHANGE_YNA_YES,
532 : TALER_EXCHANGE_YNA_ALL,
533 : 0,
534 : 10,
535 : &account_cb,
536 : &ctx));
537 1 : memset (&ctx,
538 : 0,
539 : sizeof (ctx));
540 1 : FAILIF (2 !=
541 : TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
542 : NULL,
543 : TALER_EXCHANGE_YNA_ALL,
544 : TALER_EXCHANGE_YNA_NO,
545 : TALER_EXCHANGE_YNA_ALL,
546 : 0,
547 : 10,
548 : &account_cb,
549 : &ctx));
550 :
551 : /* Opening one AML file splits the YES and NO result sets. */
552 1 : set_file_lifecycle (pg,
553 : &account,
554 : true,
555 : false);
556 1 : memset (&ctx,
557 : 0,
558 : sizeof (ctx));
559 1 : FAILIF (1 !=
560 : TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
561 : NULL,
562 : TALER_EXCHANGE_YNA_ALL,
563 : TALER_EXCHANGE_YNA_YES,
564 : TALER_EXCHANGE_YNA_ALL,
565 : 0,
566 : 10,
567 : &account_cb,
568 : &ctx));
569 1 : memset (&ctx,
570 : 0,
571 : sizeof (ctx));
572 1 : FAILIF (1 !=
573 : TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
574 : NULL,
575 : TALER_EXCHANGE_YNA_ALL,
576 : TALER_EXCHANGE_YNA_NO,
577 : TALER_EXCHANGE_YNA_ALL,
578 : 0,
579 : 10,
580 : &account_cb,
581 : &ctx));
582 :
583 : /* Closing it moves the file back into the NO result set. */
584 1 : set_file_lifecycle (pg,
585 : &account,
586 : true,
587 : true);
588 1 : memset (&ctx,
589 : 0,
590 : sizeof (ctx));
591 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
592 : TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
593 : NULL,
594 : TALER_EXCHANGE_YNA_ALL,
595 : TALER_EXCHANGE_YNA_YES,
596 : TALER_EXCHANGE_YNA_ALL,
597 : 0,
598 : 10,
599 : &account_cb,
600 : &ctx));
601 1 : memset (&ctx,
602 : 0,
603 : sizeof (ctx));
604 1 : FAILIF (2 !=
605 : TALER_EXCHANGEDB_iterate_kyc_accounts (pg,
606 : NULL,
607 : TALER_EXCHANGE_YNA_ALL,
608 : TALER_EXCHANGE_YNA_NO,
609 : TALER_EXCHANGE_YNA_ALL,
610 : 0,
611 : 10,
612 : &account_cb,
613 : &ctx));
614 1 : return 0;
615 : }
616 :
617 :
618 : /**
619 : * The checks to run, in order.
620 : */
621 : static const struct TDB_Test tests[] = {
622 : { "kyc-targets-empty",
623 : &check_empty },
624 : { "kyc-targets-lookup",
625 : &check_lookup },
626 : { "kyc-targets-lock",
627 : &check_lock },
628 : { "kyc-targets-iterate",
629 : &check_iterate },
630 : { NULL, NULL }
631 : };
632 :
633 :
634 : int
635 1 : main (int argc,
636 : char *const *argv)
637 : {
638 : int ret;
639 :
640 1 : ret = TDB_main (argc,
641 : argv,
642 : "test-kyc-targets",
643 : "Tests for the exchangedb `kyc_targets' table",
644 : tests);
645 1 : TDB_account_free (&account);
646 1 : TDB_account_free (&other);
647 1 : return ret;
648 : }
649 :
650 :
651 : /* end of test_kyc_targets.c */
|