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_kycauths_in.c
18 : * @brief tests for the exchangedb functions whose primary table is
19 : * `kycauths_in`
20 : * @author Christian Grothoff
21 : *
22 : * Covers #TALER_EXCHANGEDB_do_import_credits() (for its KYC-auth half; the
23 : * reserve half is in test_reserves_in.c),
24 : * #TALER_EXCHANGEDB_iterate_kycauth_in_above_serial_id_by_account() and
25 : * #TALER_EXCHANGEDB_iterate_exchange_kycauth_transfers().
26 : *
27 : * A KYC-auth transfer carries an account public key in its wire subject.
28 : * The import creates the `kyc_targets` and `wire_targets` rows for the
29 : * debited account on first sight and only sets `target_pub` afterwards, so
30 : * the checks import from the same account twice. The primary key is
31 : * (account, wire reference), which is what makes a re-import a no-op.
32 : */
33 : #include "test_common.h"
34 : #include "exchange-database/begin_shard.h"
35 : #include "exchange-database/do_import_credits.h"
36 : #include "exchange-database/iterate_exchange_kycauth_transfers.h"
37 : #include "exchange-database/iterate_kycauth_in_above_serial_id_by_account.h"
38 :
39 :
40 : /**
41 : * Account the first two transfers come from.
42 : */
43 : #define PAYTO_A "payto://x-taler-bank/localhost/ka-a?receiver-name=A"
44 :
45 : /**
46 : * Account the third transfer comes from.
47 : */
48 : #define PAYTO_B "payto://x-taler-bank/localhost/ka-b?receiver-name=B"
49 :
50 :
51 : /**
52 : * Closure for #kycauth_cb().
53 : */
54 : struct AuthContext
55 : {
56 : /**
57 : * How many rows did the callback see?
58 : */
59 : unsigned int total;
60 :
61 : /**
62 : * Stop after this many rows; 0 for no limit.
63 : */
64 : unsigned int stop_after;
65 :
66 : /**
67 : * Account key we are looking for, NULL to match nothing.
68 : */
69 : const union TALER_AccountPublicKeyP *account_pub;
70 :
71 : /**
72 : * How many times did we see @e account_pub?
73 : */
74 : unsigned int matched;
75 :
76 : /**
77 : * Credit reported for @e account_pub.
78 : */
79 : struct TALER_Amount credit;
80 :
81 : /**
82 : * Sender account reported for @e account_pub, owned by this struct.
83 : */
84 : char *sender;
85 : };
86 :
87 :
88 : /**
89 : * Callback for #TALER_EXCHANGEDB_iterate_kycauth_in_above_serial_id_by_account().
90 : *
91 : * @param cls a `struct AuthContext *`
92 : * @param rowid row of the transfer
93 : * @param account_pub key the transfer associated with the account
94 : * @param credit amount that was transferred
95 : * @param sender_account_details account the money came from
96 : * @param wire_reference bank's identifier for the transfer
97 : * @param execution_date when the transfer was made
98 : * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
99 : */
100 : static enum GNUNET_GenericReturnValue
101 5 : kycauth_cb (void *cls,
102 : uint64_t rowid,
103 : const union TALER_AccountPublicKeyP *account_pub,
104 : const struct TALER_Amount *credit,
105 : const struct TALER_FullPayto sender_account_details,
106 : uint64_t wire_reference,
107 : struct GNUNET_TIME_Timestamp execution_date)
108 : {
109 5 : struct AuthContext *ctx = cls;
110 :
111 : (void) rowid;
112 : (void) wire_reference;
113 : (void) execution_date;
114 5 : ctx->total++;
115 5 : if ( (NULL != ctx->account_pub) &&
116 4 : (0 == GNUNET_memcmp (account_pub,
117 : ctx->account_pub)) )
118 : {
119 2 : ctx->matched++;
120 2 : ctx->credit = *credit;
121 2 : GNUNET_free (ctx->sender);
122 2 : ctx->sender = (NULL == sender_account_details.full_payto)
123 : ? NULL
124 2 : : GNUNET_strdup (sender_account_details.full_payto);
125 : }
126 5 : if ( (0 != ctx->stop_after) &&
127 1 : (ctx->total >= ctx->stop_after) )
128 1 : return GNUNET_SYSERR;
129 4 : return GNUNET_OK;
130 : }
131 :
132 :
133 : /**
134 : * Closure for #transfer_cb().
135 : */
136 : struct TransferContext
137 : {
138 : /**
139 : * How many transfers did the callback see?
140 : */
141 : unsigned int total;
142 :
143 : /**
144 : * Row of the first transfer seen.
145 : */
146 : uint64_t first_row;
147 :
148 : /**
149 : * Sum of the whole-unit parts of the amounts seen.
150 : */
151 : uint64_t value_sum;
152 : };
153 :
154 :
155 : /**
156 : * Callback for #TALER_EXCHANGEDB_iterate_exchange_kycauth_transfers().
157 : *
158 : * @param cls a `struct TransferContext *`
159 : * @param row_id row of the transfer
160 : * @param payto_uri account the money came from
161 : * @param execution_time when the transfer was made
162 : * @param amount amount that was transferred
163 : */
164 : static void
165 11 : transfer_cb (void *cls,
166 : uint64_t row_id,
167 : const char *payto_uri,
168 : struct GNUNET_TIME_Absolute execution_time,
169 : const struct TALER_Amount *amount)
170 : {
171 11 : struct TransferContext *ctx = cls;
172 :
173 : (void) payto_uri;
174 : (void) execution_time;
175 11 : if (0 == ctx->total++)
176 5 : ctx->first_row = row_id;
177 11 : ctx->value_sum += amount->value;
178 11 : }
179 :
180 :
181 : /**
182 : * Import one KYC authentication transfer, advancing a work shard of its
183 : * own along with it.
184 : *
185 : * @param pg the database context
186 : * @param job_name job whose shard to open and advance
187 : * @param account_name exchange bank account the transfer arrived at
188 : * @param payto account the money came from
189 : * @param seed seed for the account public key
190 : * @param wire_reference bank's identifier for the transfer
191 : * @param amount amount that was transferred, e.g. "10"
192 : * @param[out] account_pub set to the key the transfer carried
193 : * @return transaction status of the import
194 : */
195 : static enum GNUNET_DB_QueryStatus
196 4 : import_one (struct TALER_EXCHANGEDB_PostgresContext *pg,
197 : const char *job_name,
198 : const char *account_name,
199 : const char *payto,
200 : uint32_t seed,
201 : uint64_t wire_reference,
202 : const char *amount,
203 : union TALER_AccountPublicKeyP *account_pub)
204 : {
205 4 : struct TALER_Amount balance = TDB_amount (amount);
206 : struct TALER_EXCHANGEDB_KycauthInInfo info;
207 : struct TALER_EXCHANGEDB_CreditBatch batch;
208 : uint64_t start;
209 : uint64_t end;
210 : uint64_t progress;
211 :
212 4 : GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
213 : TALER_EXCHANGEDB_begin_shard (pg,
214 : job_name,
215 : GNUNET_TIME_UNIT_HOURS,
216 : 1024,
217 : &start,
218 : &end,
219 : &progress));
220 4 : TDB_fill (account_pub,
221 : sizeof (*account_pub),
222 : seed);
223 4 : memset (&info,
224 : 0,
225 : sizeof (info));
226 4 : info.account_pub = account_pub;
227 4 : info.balance = &balance;
228 4 : info.execution_time = GNUNET_TIME_timestamp_get ();
229 4 : info.sender_account_details.full_payto = (char *) payto;
230 4 : info.wire_reference = wire_reference;
231 4 : memset (&batch,
232 : 0,
233 : sizeof (batch));
234 4 : batch.exchange_account_name = account_name;
235 4 : batch.kycauths = &info;
236 4 : batch.kycauths_length = 1;
237 4 : batch.job_name = job_name;
238 4 : batch.shard_start = start;
239 4 : batch.shard_end = end;
240 4 : batch.progress_row = end;
241 4 : batch.lease = GNUNET_TIME_UNIT_HOURS;
242 4 : return TALER_EXCHANGEDB_do_import_credits (pg,
243 : &batch,
244 : NULL);
245 : }
246 :
247 :
248 : /**
249 : * Nothing is known while the table is empty.
250 : *
251 : * @param pg the database context
252 : * @return 0 on success
253 : */
254 : static int
255 1 : check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
256 : {
257 1 : struct AuthContext ctx = { 0 };
258 1 : struct TransferContext tctx = { 0 };
259 1 : struct TALER_Amount zero = TDB_amount ("0");
260 :
261 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
262 : TALER_EXCHANGEDB_iterate_kycauth_in_above_serial_id_by_account (
263 : pg,
264 : "exchange-account-1",
265 : 0,
266 : &kycauth_cb,
267 : &ctx));
268 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
269 : TALER_EXCHANGEDB_iterate_exchange_kycauth_transfers (pg,
270 : &zero,
271 : 0,
272 : 10,
273 : NULL,
274 : &transfer_cb,
275 : &tctx));
276 1 : FAILIF (0 != ctx.total);
277 1 : FAILIF (0 != tctx.total);
278 1 : FAILIF (0 != TDB_count (pg,
279 : "FROM kycauths_in"));
280 1 : return 0;
281 : }
282 :
283 :
284 : /**
285 : * Importing a transfer registers the account key and creates the account
286 : * rows; importing it again is a no-op.
287 : *
288 : * @param pg the database context
289 : * @return 0 on success
290 : */
291 : static int
292 1 : check_import (struct TALER_EXCHANGEDB_PostgresContext *pg)
293 : {
294 : union TALER_AccountPublicKeyP account_pub;
295 : struct AuthContext ctx;
296 1 : struct TALER_Amount expect = TDB_amount ("10");
297 : char *hex;
298 :
299 1 : FAILIF (0 >
300 : import_one (pg,
301 : "ka-job-a",
302 : "exchange-account-1",
303 : PAYTO_A,
304 : 10,
305 : 1,
306 : "10",
307 : &account_pub));
308 1 : FAILIF (1 != TDB_count (pg,
309 : "FROM kycauths_in"));
310 : /* the account is now known and carries the key from the wire subject */
311 1 : hex = TDB_hex (&account_pub,
312 : sizeof (account_pub));
313 1 : FAILIF_C (1 != TDB_count (pg,
314 : "FROM kyc_targets"
315 : " WHERE target_pub=decode('%s','hex')",
316 : hex),
317 : GNUNET_free (hex));
318 1 : GNUNET_free (hex);
319 1 : FAILIF (1 != TDB_count (pg,
320 : "FROM wire_targets"));
321 :
322 : /* the same transfer again: the primary key is (account, wire reference) */
323 1 : FAILIF (0 >
324 : import_one (pg,
325 : "ka-job-a",
326 : "exchange-account-1",
327 : PAYTO_A,
328 : 10,
329 : 1,
330 : "10",
331 : &account_pub));
332 1 : FAILIF (1 != TDB_count (pg,
333 : "FROM kycauths_in"));
334 :
335 1 : memset (&ctx,
336 : 0,
337 : sizeof (ctx));
338 1 : ctx.account_pub = &account_pub;
339 1 : FAILIF (0 >=
340 : TALER_EXCHANGEDB_iterate_kycauth_in_above_serial_id_by_account (
341 : pg,
342 : "exchange-account-1",
343 : 0,
344 : &kycauth_cb,
345 : &ctx));
346 1 : FAILIF_C (1 != ctx.matched,
347 : GNUNET_free (ctx.sender));
348 1 : FAILIF_C (0 != TALER_amount_cmp (&ctx.credit,
349 : &expect),
350 : GNUNET_free (ctx.sender));
351 1 : FAILIF_C (0 != strcmp (ctx.sender,
352 : PAYTO_A),
353 : GNUNET_free (ctx.sender));
354 1 : GNUNET_free (ctx.sender);
355 1 : return 0;
356 : }
357 :
358 :
359 : /**
360 : * With several transfers on file the by-account iterator filters by the
361 : * exchange bank account and honours an aborting callback.
362 : *
363 : * @param pg the database context
364 : * @return 0 on success
365 : */
366 : static int
367 1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
368 : {
369 : union TALER_AccountPublicKeyP a2;
370 : union TALER_AccountPublicKeyP a3;
371 : struct AuthContext ctx;
372 :
373 : /* a second transfer from the same account, replacing the key */
374 1 : FAILIF (0 >
375 : import_one (pg,
376 : "ka-job-a",
377 : "exchange-account-1",
378 : PAYTO_A,
379 : 11,
380 : 2,
381 : "2",
382 : &a2));
383 : /* and one from a different account to a different exchange account */
384 1 : FAILIF (0 >
385 : import_one (pg,
386 : "ka-job-b",
387 : "exchange-account-2",
388 : PAYTO_B,
389 : 12,
390 : 3,
391 : "5",
392 : &a3));
393 1 : FAILIF (3 != TDB_count (pg,
394 : "FROM kycauths_in"));
395 : /* the second transfer from PAYTO_A took over its target_pub */
396 : {
397 1 : char *hex = TDB_hex (&a2,
398 : sizeof (a2));
399 :
400 1 : FAILIF_C (1 != TDB_count (pg,
401 : "FROM kyc_targets"
402 : " WHERE target_pub=decode('%s','hex')",
403 : hex),
404 : GNUNET_free (hex));
405 1 : GNUNET_free (hex);
406 : }
407 :
408 1 : memset (&ctx,
409 : 0,
410 : sizeof (ctx));
411 1 : ctx.account_pub = &a3;
412 1 : FAILIF (2 !=
413 : TALER_EXCHANGEDB_iterate_kycauth_in_above_serial_id_by_account (
414 : pg,
415 : "exchange-account-1",
416 : 0,
417 : &kycauth_cb,
418 : &ctx));
419 1 : FAILIF_C (0 != ctx.matched,
420 : GNUNET_free (ctx.sender));
421 1 : GNUNET_free (ctx.sender);
422 :
423 1 : memset (&ctx,
424 : 0,
425 : sizeof (ctx));
426 1 : ctx.account_pub = &a3;
427 1 : FAILIF (1 !=
428 : TALER_EXCHANGEDB_iterate_kycauth_in_above_serial_id_by_account (
429 : pg,
430 : "exchange-account-2",
431 : 0,
432 : &kycauth_cb,
433 : &ctx));
434 1 : FAILIF_C (1 != ctx.matched,
435 : GNUNET_free (ctx.sender));
436 1 : FAILIF_C (0 != strcmp (ctx.sender,
437 : PAYTO_B),
438 : GNUNET_free (ctx.sender));
439 1 : GNUNET_free (ctx.sender);
440 :
441 : /* an exchange account nobody wired to */
442 1 : memset (&ctx,
443 : 0,
444 : sizeof (ctx));
445 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
446 : TALER_EXCHANGEDB_iterate_kycauth_in_above_serial_id_by_account (
447 : pg,
448 : "exchange-account-nowhere",
449 : 0,
450 : &kycauth_cb,
451 : &ctx));
452 1 : FAILIF (0 != ctx.total);
453 :
454 : /* the serial bound is inclusive, so a bound past the end finds nothing */
455 1 : memset (&ctx,
456 : 0,
457 : sizeof (ctx));
458 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
459 : TALER_EXCHANGEDB_iterate_kycauth_in_above_serial_id_by_account (
460 : pg,
461 : "exchange-account-1",
462 : 1000,
463 : &kycauth_cb,
464 : &ctx));
465 1 : FAILIF (0 != ctx.total);
466 :
467 : /* a callback that gives up stops the iteration without erroring out */
468 1 : memset (&ctx,
469 : 0,
470 : sizeof (ctx));
471 1 : ctx.stop_after = 1;
472 1 : FAILIF (2 !=
473 : TALER_EXCHANGEDB_iterate_kycauth_in_above_serial_id_by_account (
474 : pg,
475 : "exchange-account-1",
476 : 0,
477 : &kycauth_cb,
478 : &ctx));
479 1 : FAILIF (1 != ctx.total);
480 1 : GNUNET_free (ctx.sender);
481 1 : return 0;
482 : }
483 :
484 :
485 : /**
486 : * The AML view of the same rows filters by amount and by account, and can
487 : * walk the table backwards.
488 : *
489 : * @param pg the database context
490 : * @return 0 on success
491 : */
492 : static int
493 1 : check_kycauth_transfers (struct TALER_EXCHANGEDB_PostgresContext *pg)
494 : {
495 : struct TALER_NormalizedPayto npayto;
496 : struct TALER_NormalizedPaytoHashP h_payto;
497 1 : struct TALER_FullPayto payto_a = {
498 : .full_payto = (char *) PAYTO_A
499 : };
500 1 : struct TALER_Amount zero = TDB_amount ("0");
501 1 : struct TALER_Amount five = TDB_amount ("5");
502 1 : struct TALER_Amount huge = TDB_amount ("1000");
503 : struct TransferContext ctx;
504 : uint64_t lowest;
505 :
506 1 : memset (&ctx,
507 : 0,
508 : sizeof (ctx));
509 1 : FAILIF (3 !=
510 : TALER_EXCHANGEDB_iterate_exchange_kycauth_transfers (pg,
511 : &zero,
512 : 0,
513 : 10,
514 : NULL,
515 : &transfer_cb,
516 : &ctx));
517 1 : FAILIF (10 + 2 + 5 != ctx.value_sum);
518 1 : lowest = ctx.first_row;
519 :
520 1 : memset (&ctx,
521 : 0,
522 : sizeof (ctx));
523 1 : FAILIF (2 !=
524 : TALER_EXCHANGEDB_iterate_exchange_kycauth_transfers (pg,
525 : &five,
526 : 0,
527 : 10,
528 : NULL,
529 : &transfer_cb,
530 : &ctx));
531 1 : FAILIF (10 + 5 != ctx.value_sum);
532 :
533 1 : memset (&ctx,
534 : 0,
535 : sizeof (ctx));
536 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
537 : TALER_EXCHANGEDB_iterate_exchange_kycauth_transfers (pg,
538 : &huge,
539 : 0,
540 : 10,
541 : NULL,
542 : &transfer_cb,
543 : &ctx));
544 1 : FAILIF (0 != ctx.total);
545 :
546 1 : npayto = TALER_payto_normalize (payto_a);
547 1 : GNUNET_assert (NULL != npayto.normalized_payto);
548 1 : TALER_normalized_payto_hash (npayto,
549 : &h_payto);
550 1 : GNUNET_free (npayto.normalized_payto);
551 1 : memset (&ctx,
552 : 0,
553 : sizeof (ctx));
554 1 : FAILIF (2 !=
555 : TALER_EXCHANGEDB_iterate_exchange_kycauth_transfers (pg,
556 : &zero,
557 : 0,
558 : 10,
559 : &h_payto,
560 : &transfer_cb,
561 : &ctx));
562 1 : FAILIF (10 + 2 != ctx.value_sum);
563 :
564 1 : memset (&ctx,
565 : 0,
566 : sizeof (ctx));
567 1 : FAILIF (1 !=
568 : TALER_EXCHANGEDB_iterate_exchange_kycauth_transfers (pg,
569 : &zero,
570 : 0,
571 : 1,
572 : NULL,
573 : &transfer_cb,
574 : &ctx));
575 1 : FAILIF (lowest != ctx.first_row);
576 :
577 1 : memset (&ctx,
578 : 0,
579 : sizeof (ctx));
580 1 : FAILIF (3 !=
581 : TALER_EXCHANGEDB_iterate_exchange_kycauth_transfers (pg,
582 : &zero,
583 : 1000,
584 : -10,
585 : NULL,
586 : &transfer_cb,
587 : &ctx));
588 1 : FAILIF (lowest == ctx.first_row);
589 1 : FAILIF (10 + 2 + 5 != ctx.value_sum);
590 1 : return 0;
591 : }
592 :
593 :
594 : /**
595 : * The checks to run, in order.
596 : */
597 : static const struct TDB_Test tests[] = {
598 : { "kycauths-in-empty",
599 : &check_empty },
600 : { "kycauths-in-import",
601 : &check_import },
602 : { "kycauths-in-iterate",
603 : &check_iterate },
604 : { "kycauths-in-transfers",
605 : &check_kycauth_transfers },
606 : { NULL, NULL }
607 : };
608 :
609 :
610 : int
611 1 : main (int argc,
612 : char *const *argv)
613 : {
614 1 : return TDB_main (argc,
615 : argv,
616 : "test-kycauths-in",
617 : "Tests for the exchangedb `kycauths_in' table",
618 : tests);
619 : }
620 :
621 :
622 : /* end of test_kycauths_in.c */
|