test_kycauths_in.c (19569B)
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 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 struct AuthContext *ctx = cls; 110 111 (void) rowid; 112 (void) wire_reference; 113 (void) execution_date; 114 ctx->total++; 115 if ( (NULL != ctx->account_pub) && 116 (0 == GNUNET_memcmp (account_pub, 117 ctx->account_pub)) ) 118 { 119 ctx->matched++; 120 ctx->credit = *credit; 121 GNUNET_free (ctx->sender); 122 ctx->sender = (NULL == sender_account_details.full_payto) 123 ? NULL 124 : GNUNET_strdup (sender_account_details.full_payto); 125 } 126 if ( (0 != ctx->stop_after) && 127 (ctx->total >= ctx->stop_after) ) 128 return GNUNET_SYSERR; 129 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 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 struct TransferContext *ctx = cls; 172 173 (void) payto_uri; 174 (void) execution_time; 175 if (0 == ctx->total++) 176 ctx->first_row = row_id; 177 ctx->value_sum += amount->value; 178 } 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 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 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 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 TDB_fill (account_pub, 221 sizeof (*account_pub), 222 seed); 223 memset (&info, 224 0, 225 sizeof (info)); 226 info.account_pub = account_pub; 227 info.balance = &balance; 228 info.execution_time = GNUNET_TIME_timestamp_get (); 229 info.sender_account_details.full_payto = (char *) payto; 230 info.wire_reference = wire_reference; 231 memset (&batch, 232 0, 233 sizeof (batch)); 234 batch.exchange_account_name = account_name; 235 batch.kycauths = &info; 236 batch.kycauths_length = 1; 237 batch.job_name = job_name; 238 batch.shard_start = start; 239 batch.shard_end = end; 240 batch.progress_row = end; 241 batch.lease = GNUNET_TIME_UNIT_HOURS; 242 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 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 256 { 257 struct AuthContext ctx = { 0 }; 258 struct TransferContext tctx = { 0 }; 259 struct TALER_Amount zero = TDB_amount ("0"); 260 261 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 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 FAILIF (0 != ctx.total); 277 FAILIF (0 != tctx.total); 278 FAILIF (0 != TDB_count (pg, 279 "FROM kycauths_in")); 280 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 check_import (struct TALER_EXCHANGEDB_PostgresContext *pg) 293 { 294 union TALER_AccountPublicKeyP account_pub; 295 struct AuthContext ctx; 296 struct TALER_Amount expect = TDB_amount ("10"); 297 char *hex; 298 299 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 FAILIF (1 != TDB_count (pg, 309 "FROM kycauths_in")); 310 /* the account is now known and carries the key from the wire subject */ 311 hex = TDB_hex (&account_pub, 312 sizeof (account_pub)); 313 FAILIF_C (1 != TDB_count (pg, 314 "FROM kyc_targets" 315 " WHERE target_pub=decode('%s','hex')", 316 hex), 317 GNUNET_free (hex)); 318 GNUNET_free (hex); 319 FAILIF (1 != TDB_count (pg, 320 "FROM wire_targets")); 321 322 /* the same transfer again: the primary key is (account, wire reference) */ 323 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 FAILIF (1 != TDB_count (pg, 333 "FROM kycauths_in")); 334 335 memset (&ctx, 336 0, 337 sizeof (ctx)); 338 ctx.account_pub = &account_pub; 339 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 FAILIF_C (1 != ctx.matched, 347 GNUNET_free (ctx.sender)); 348 FAILIF_C (0 != TALER_amount_cmp (&ctx.credit, 349 &expect), 350 GNUNET_free (ctx.sender)); 351 FAILIF_C (0 != strcmp (ctx.sender, 352 PAYTO_A), 353 GNUNET_free (ctx.sender)); 354 GNUNET_free (ctx.sender); 355 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 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 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 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 FAILIF (3 != TDB_count (pg, 394 "FROM kycauths_in")); 395 /* the second transfer from PAYTO_A took over its target_pub */ 396 { 397 char *hex = TDB_hex (&a2, 398 sizeof (a2)); 399 400 FAILIF_C (1 != TDB_count (pg, 401 "FROM kyc_targets" 402 " WHERE target_pub=decode('%s','hex')", 403 hex), 404 GNUNET_free (hex)); 405 GNUNET_free (hex); 406 } 407 408 memset (&ctx, 409 0, 410 sizeof (ctx)); 411 ctx.account_pub = &a3; 412 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 FAILIF_C (0 != ctx.matched, 420 GNUNET_free (ctx.sender)); 421 GNUNET_free (ctx.sender); 422 423 memset (&ctx, 424 0, 425 sizeof (ctx)); 426 ctx.account_pub = &a3; 427 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 FAILIF_C (1 != ctx.matched, 435 GNUNET_free (ctx.sender)); 436 FAILIF_C (0 != strcmp (ctx.sender, 437 PAYTO_B), 438 GNUNET_free (ctx.sender)); 439 GNUNET_free (ctx.sender); 440 441 /* an exchange account nobody wired to */ 442 memset (&ctx, 443 0, 444 sizeof (ctx)); 445 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 FAILIF (0 != ctx.total); 453 454 /* the serial bound is inclusive, so a bound past the end finds nothing */ 455 memset (&ctx, 456 0, 457 sizeof (ctx)); 458 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 FAILIF (0 != ctx.total); 466 467 /* a callback that gives up stops the iteration without erroring out */ 468 memset (&ctx, 469 0, 470 sizeof (ctx)); 471 ctx.stop_after = 1; 472 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 FAILIF (1 != ctx.total); 480 GNUNET_free (ctx.sender); 481 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 check_kycauth_transfers (struct TALER_EXCHANGEDB_PostgresContext *pg) 494 { 495 struct TALER_NormalizedPayto npayto; 496 struct TALER_NormalizedPaytoHashP h_payto; 497 struct TALER_FullPayto payto_a = { 498 .full_payto = (char *) PAYTO_A 499 }; 500 struct TALER_Amount zero = TDB_amount ("0"); 501 struct TALER_Amount five = TDB_amount ("5"); 502 struct TALER_Amount huge = TDB_amount ("1000"); 503 struct TransferContext ctx; 504 uint64_t lowest; 505 506 memset (&ctx, 507 0, 508 sizeof (ctx)); 509 FAILIF (3 != 510 TALER_EXCHANGEDB_iterate_exchange_kycauth_transfers (pg, 511 &zero, 512 0, 513 10, 514 NULL, 515 &transfer_cb, 516 &ctx)); 517 FAILIF (10 + 2 + 5 != ctx.value_sum); 518 lowest = ctx.first_row; 519 520 memset (&ctx, 521 0, 522 sizeof (ctx)); 523 FAILIF (2 != 524 TALER_EXCHANGEDB_iterate_exchange_kycauth_transfers (pg, 525 &five, 526 0, 527 10, 528 NULL, 529 &transfer_cb, 530 &ctx)); 531 FAILIF (10 + 5 != ctx.value_sum); 532 533 memset (&ctx, 534 0, 535 sizeof (ctx)); 536 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 FAILIF (0 != ctx.total); 545 546 npayto = TALER_payto_normalize (payto_a); 547 GNUNET_assert (NULL != npayto.normalized_payto); 548 TALER_normalized_payto_hash (npayto, 549 &h_payto); 550 GNUNET_free (npayto.normalized_payto); 551 memset (&ctx, 552 0, 553 sizeof (ctx)); 554 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 FAILIF (10 + 2 != ctx.value_sum); 563 564 memset (&ctx, 565 0, 566 sizeof (ctx)); 567 FAILIF (1 != 568 TALER_EXCHANGEDB_iterate_exchange_kycauth_transfers (pg, 569 &zero, 570 0, 571 1, 572 NULL, 573 &transfer_cb, 574 &ctx)); 575 FAILIF (lowest != ctx.first_row); 576 577 memset (&ctx, 578 0, 579 sizeof (ctx)); 580 FAILIF (3 != 581 TALER_EXCHANGEDB_iterate_exchange_kycauth_transfers (pg, 582 &zero, 583 1000, 584 -10, 585 NULL, 586 &transfer_cb, 587 &ctx)); 588 FAILIF (lowest == ctx.first_row); 589 FAILIF (10 + 2 + 5 != ctx.value_sum); 590 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 main (int argc, 612 char *const *argv) 613 { 614 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 */