test_aml_history.c (22962B)
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_aml_history.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `aml_history` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_insert_aml_decision(), 23 * #TALER_EXCHANGEDB_iterate_aml_history(), 24 * #TALER_EXCHANGEDB_iterate_aml_history_above_serial_id(), 25 * #TALER_EXCHANGEDB_iterate_aml_decisions() and 26 * #TALER_EXCHANGEDB_aml_history_builder(). 27 * 28 * `aml_history` is what an AML officer signed: one row per decision, each 29 * pointing at the `legitimization_outcomes` row it produced. The insert 30 * refuses an officer who may not decide and a decision older than the one 31 * already on file, and both of those are checked here. 32 */ 33 #include "test_common.h" 34 #include "exchange-database/account_history.h" 35 #include "exchange-database/insert_aml_decision.h" 36 #include "exchange-database/insert_aml_officer.h" 37 #include "exchange-database/iterate_aml_decisions.h" 38 #include "exchange-database/iterate_aml_history.h" 39 #include "exchange-database/iterate_aml_history_above_serial_id.h" 40 41 42 /** 43 * Account the checks decide about. 44 */ 45 static struct TDB_Account account; 46 47 48 /** 49 * Officer who takes the decisions. 50 */ 51 static struct TALER_AmlOfficerPublicKeyP officer_pub; 52 53 54 /** 55 * Build a timestamp from a number of seconds since the epoch. 56 * 57 * @param secs seconds since the epoch 58 * @return the timestamp 59 */ 60 static struct GNUNET_TIME_Timestamp 61 ts (uint64_t secs) 62 { 63 struct GNUNET_TIME_Absolute abs = { 64 .abs_value_us = secs * 1000LLU * 1000LLU 65 }; 66 67 return GNUNET_TIME_absolute_to_timestamp (abs); 68 } 69 70 71 /** 72 * Build a rule set naming @a name as its only rule. 73 * 74 * @param name name of the rule 75 * @return the rule set, to be freed with json_decref() 76 */ 77 static json_t * 78 make_rules (const char *name) 79 { 80 json_t *j; 81 82 j = GNUNET_JSON_PACK ( 83 GNUNET_JSON_pack_string ("rule_name", 84 name), 85 GNUNET_JSON_pack_array_steal ("rules", 86 json_array ())); 87 GNUNET_assert (NULL != j); 88 return j; 89 } 90 91 92 /** 93 * Closure for #history_cb() and #decision_cb(). 94 */ 95 struct HistoryContext 96 { 97 /** 98 * How many rows did the callback see? 99 */ 100 unsigned int total; 101 102 /** 103 * Stop after this many rows; 0 for no limit. 104 */ 105 unsigned int stop_after; 106 107 /** 108 * Justification of the last row seen, owned by this struct. 109 */ 110 char *justification; 111 112 /** 113 * Whether the last row asked for an investigation. 114 */ 115 bool to_investigate; 116 117 /** 118 * Whether the last row is the active decision. 119 */ 120 bool is_active; 121 122 /** 123 * Whether the last row named an officer. 124 */ 125 bool have_officer; 126 }; 127 128 129 /** 130 * Callback for #TALER_EXCHANGEDB_iterate_aml_history(). 131 * 132 * @param cls a `struct HistoryContext *` 133 * @param outcome_serial_id row of the outcome the decision produced 134 * @param decision_time when the decision was taken 135 * @param justification why it was taken 136 * @param decider_pub which officer took it 137 * @param jproperties new account properties 138 * @param jnew_rules new account rules 139 * @param to_investigate whether staff should investigate 140 * @param is_active whether this is the active decision 141 */ 142 static void 143 history_cb (void *cls, 144 uint64_t outcome_serial_id, 145 struct GNUNET_TIME_Timestamp decision_time, 146 const char *justification, 147 const struct TALER_AmlOfficerPublicKeyP *decider_pub, 148 const json_t *jproperties, 149 const json_t *jnew_rules, 150 bool to_investigate, 151 bool is_active) 152 { 153 struct HistoryContext *ctx = cls; 154 155 (void) outcome_serial_id; 156 (void) decision_time; 157 (void) jproperties; 158 (void) jnew_rules; 159 ctx->total++; 160 GNUNET_free (ctx->justification); 161 ctx->justification = (NULL == justification) 162 ? NULL 163 : GNUNET_strdup (justification); 164 ctx->to_investigate = to_investigate; 165 ctx->is_active = is_active; 166 ctx->have_officer = (NULL != decider_pub); 167 } 168 169 170 /** 171 * Callback for #TALER_EXCHANGEDB_iterate_aml_decisions(). 172 * 173 * @param cls a `struct HistoryContext *` 174 * @param row_id row of the decision 175 * @param justification why it was taken 176 * @param h_payto account it is about 177 * @param decision_time when it was taken 178 * @param expiration_time when the rules expire 179 * @param jproperties new account properties 180 * @param to_investigate whether staff should investigate 181 * @param is_active whether this is the active decision 182 * @param is_wallet whether the account is a wallet 183 * @param payto the account's payto URI 184 * @param account_rules the account's rules 185 */ 186 static void 187 decision_cb (void *cls, 188 uint64_t row_id, 189 const char *justification, 190 const struct TALER_NormalizedPaytoHashP *h_payto, 191 struct GNUNET_TIME_Timestamp decision_time, 192 struct GNUNET_TIME_Absolute expiration_time, 193 const json_t *jproperties, 194 bool to_investigate, 195 bool is_active, 196 bool is_wallet, 197 struct TALER_FullPayto payto, 198 const json_t *account_rules) 199 { 200 struct HistoryContext *ctx = cls; 201 202 (void) row_id; 203 (void) h_payto; 204 (void) decision_time; 205 (void) expiration_time; 206 (void) jproperties; 207 (void) is_wallet; 208 (void) payto; 209 (void) account_rules; 210 ctx->total++; 211 GNUNET_free (ctx->justification); 212 ctx->justification = (NULL == justification) 213 ? NULL 214 : GNUNET_strdup (justification); 215 ctx->to_investigate = to_investigate; 216 ctx->is_active = is_active; 217 } 218 219 220 /** 221 * Callback for 222 * #TALER_EXCHANGEDB_iterate_aml_history_above_serial_id(). 223 * 224 * @param cls a `struct HistoryContext *` 225 * @param rowid row of the decision 226 * @param h_payto account it is about 227 * @param justification why it was taken 228 * @param decider_pub officer who took it 229 * @param decider_sig the officer's signature 230 * @param decision_time when it was taken 231 * @param jproperties new account properties 232 * @param jnew_rules new account rules 233 * @param new_measure_name measure to apply immediately 234 * @param to_investigate whether staff should investigate 235 * @param attributes_expiration when the attributes expire 236 * @param h_attributes hash of the attributes 237 * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop 238 */ 239 static enum GNUNET_GenericReturnValue 240 serial_cb (void *cls, 241 uint64_t rowid, 242 const struct TALER_NormalizedPaytoHashP *h_payto, 243 const char *justification, 244 const struct TALER_AmlOfficerPublicKeyP *decider_pub, 245 const struct TALER_AmlOfficerSignatureP *decider_sig, 246 struct GNUNET_TIME_Timestamp decision_time, 247 const json_t *jproperties, 248 const json_t *jnew_rules, 249 const char *new_measure_name, 250 bool to_investigate, 251 struct GNUNET_TIME_Timestamp attributes_expiration, 252 const struct GNUNET_HashCode *h_attributes) 253 { 254 struct HistoryContext *ctx = cls; 255 256 (void) rowid; 257 (void) h_payto; 258 (void) decider_sig; 259 (void) decision_time; 260 (void) jproperties; 261 (void) jnew_rules; 262 (void) new_measure_name; 263 (void) attributes_expiration; 264 (void) h_attributes; 265 ctx->total++; 266 GNUNET_free (ctx->justification); 267 ctx->justification = (NULL == justification) 268 ? NULL 269 : GNUNET_strdup (justification); 270 ctx->to_investigate = to_investigate; 271 ctx->have_officer = (NULL != decider_pub); 272 if ( (0 != ctx->stop_after) && 273 (ctx->total >= ctx->stop_after) ) 274 return GNUNET_SYSERR; 275 return GNUNET_OK; 276 } 277 278 279 /** 280 * Outcome of an AML decision. 281 */ 282 struct DecisionStatus 283 { 284 /** 285 * May the officer not decide right now? 286 */ 287 bool invalid_officer; 288 289 /** 290 * Is the account unknown? 291 */ 292 bool unknown_account; 293 294 /** 295 * Time of the decision that was already on file. 296 */ 297 struct GNUNET_TIME_Timestamp last_date; 298 299 /** 300 * Row of the measures the decision put in place. 301 */ 302 uint64_t legitimization_measure_serial_id; 303 304 /** 305 * Is the account a wallet? 306 */ 307 bool is_wallet; 308 }; 309 310 311 /** 312 * Take an AML decision about our account. 313 * 314 * @param pg the database context 315 * @param decider officer taking the decision, NULL for none 316 * @param seed seed for the officer's signature 317 * @param when when the decision is taken, in seconds since the epoch 318 * @param justification why 319 * @param to_investigate whether staff should investigate 320 * @param[out] st set to the outcome 321 * @return transaction status 322 */ 323 static enum GNUNET_DB_QueryStatus 324 decide (struct TALER_EXCHANGEDB_PostgresContext *pg, 325 const struct TALER_AmlOfficerPublicKeyP *decider, 326 uint32_t seed, 327 uint64_t when, 328 const char *justification, 329 bool to_investigate, 330 struct DecisionStatus *st) 331 { 332 struct TALER_AmlOfficerSignatureP decider_sig; 333 struct TALER_FullPayto null_payto = { NULL }; 334 json_t *new_rules = make_rules ("decided"); 335 /* the argument is declared `const char *[static 0]', so it must not 336 be NULL even though no event is to be triggered */ 337 const char *no_events[1] = { NULL }; 338 enum GNUNET_DB_QueryStatus qs; 339 340 TDB_fill (&decider_sig, 341 sizeof (decider_sig), 342 seed); 343 memset (st, 344 0, 345 sizeof (*st)); 346 qs = TALER_EXCHANGEDB_insert_aml_decision ( 347 pg, 348 null_payto, 349 &account.h_normalized, 350 ts (when), 351 GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS), 352 NULL, 353 new_rules, 354 to_investigate, 355 NULL, 356 NULL, 357 justification, 358 decider, 359 (NULL == decider) ? NULL : &decider_sig, 360 0, 361 no_events, 362 NULL, 363 0, 364 NULL, 365 NULL, 366 GNUNET_TIME_UNIT_ZERO_TS, 367 &st->invalid_officer, 368 &st->unknown_account, 369 &st->last_date, 370 &st->legitimization_measure_serial_id, 371 &st->is_wallet); 372 json_decref (new_rules); 373 return qs; 374 } 375 376 377 /** 378 * Nothing is known while the table is empty. 379 * 380 * @param pg the database context 381 * @return 0 on success 382 */ 383 static int 384 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 385 { 386 struct HistoryContext ctx = { 0 }; 387 388 TDB_account (pg, 389 10, 390 &account); 391 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 392 TALER_EXCHANGEDB_iterate_aml_history (pg, 393 &account.h_normalized, 394 0, 395 10, 396 &history_cb, 397 &ctx)); 398 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 399 TALER_EXCHANGEDB_iterate_aml_history_above_serial_id ( 400 pg, 401 0, 402 &serial_cb, 403 &ctx)); 404 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 405 TALER_EXCHANGEDB_iterate_aml_decisions (pg, 406 &account.h_normalized, 407 TALER_EXCHANGE_YNA_ALL, 408 TALER_EXCHANGE_YNA_ALL, 409 0, 410 10, 411 &decision_cb, 412 &ctx)); 413 FAILIF (0 != ctx.total); 414 return 0; 415 } 416 417 418 /** 419 * An officer who is not on file may not decide. 420 * 421 * @param pg the database context 422 * @return 0 on success 423 */ 424 static int 425 check_invalid_officer (struct TALER_EXCHANGEDB_PostgresContext *pg) 426 { 427 struct DecisionStatus st; 428 429 TDB_FILL (officer_pub, 430 20); 431 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 432 decide (pg, 433 &officer_pub, 434 20, 435 1600000000, 436 "no such officer", 437 false, 438 &st)); 439 FAILIF (! st.invalid_officer); 440 FAILIF (0 != TDB_count (pg, 441 "FROM aml_history")); 442 return 0; 443 } 444 445 446 /** 447 * An appointed officer's decision is recorded and becomes the active 448 * outcome. 449 * 450 * @param pg the database context 451 * @return 0 on success 452 */ 453 static int 454 check_decide (struct TALER_EXCHANGEDB_PostgresContext *pg) 455 { 456 struct TALER_MasterSignatureP master_sig; 457 struct GNUNET_TIME_Timestamp previous_change; 458 struct DecisionStatus st; 459 struct HistoryContext ctx; 460 461 TDB_FILL (master_sig, 462 20); 463 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 464 TALER_EXCHANGEDB_insert_aml_officer (pg, 465 &officer_pub, 466 &master_sig, 467 "Alex Officer", 468 true, 469 false, 470 ts (1500000000), 471 &previous_change)); 472 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 473 decide (pg, 474 &officer_pub, 475 20, 476 1600000000, 477 "looks fine", 478 false, 479 &st)); 480 FAILIF (st.invalid_officer); 481 FAILIF (st.unknown_account); 482 FAILIF (1 != TDB_count (pg, 483 "FROM aml_history")); 484 FAILIF (1 != TDB_count (pg, 485 "FROM legitimization_outcomes")); 486 487 memset (&ctx, 488 0, 489 sizeof (ctx)); 490 FAILIF (0 >= 491 TALER_EXCHANGEDB_iterate_aml_history (pg, 492 &account.h_normalized, 493 0, 494 10, 495 &history_cb, 496 &ctx)); 497 FAILIF_C (1 != ctx.total, 498 GNUNET_free (ctx.justification)); 499 FAILIF_C (0 != strcmp (ctx.justification, 500 "looks fine"), 501 GNUNET_free (ctx.justification)); 502 FAILIF_C (! ctx.is_active, 503 GNUNET_free (ctx.justification)); 504 FAILIF_C (! ctx.have_officer, 505 GNUNET_free (ctx.justification)); 506 GNUNET_free (ctx.justification); 507 return 0; 508 } 509 510 511 /** 512 * A decision older than the one on file is refused; a newer one 513 * supersedes it. 514 * 515 * @param pg the database context 516 * @return 0 on success 517 */ 518 static int 519 check_supersede (struct TALER_EXCHANGEDB_PostgresContext *pg) 520 { 521 struct DecisionStatus st; 522 struct HistoryContext ctx; 523 524 /* an older decision is refused (signalled by NO_RESULTS), and the 525 decision that is already on file is reported back */ 526 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 527 decide (pg, 528 &officer_pub, 529 21, 530 1500000000, 531 "too late", 532 false, 533 &st)); 534 FAILIF (GNUNET_TIME_timestamp_cmp (st.last_date, 535 !=, 536 ts (1600000000))); 537 FAILIF (1 != TDB_count (pg, 538 "FROM aml_history")); 539 540 /* a newer one is taken and deactivates the previous outcome */ 541 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 542 decide (pg, 543 &officer_pub, 544 22, 545 1600003600, 546 "investigate this", 547 true, 548 &st)); 549 FAILIF (2 != TDB_count (pg, 550 "FROM aml_history")); 551 FAILIF (2 != TDB_count (pg, 552 "FROM legitimization_outcomes")); 553 FAILIF (1 != TDB_count (pg, 554 "FROM legitimization_outcomes" 555 " WHERE is_active")); 556 557 memset (&ctx, 558 0, 559 sizeof (ctx)); 560 FAILIF (0 >= 561 TALER_EXCHANGEDB_iterate_aml_history (pg, 562 &account.h_normalized, 563 0, 564 10, 565 &history_cb, 566 &ctx)); 567 FAILIF_C (2 != ctx.total, 568 GNUNET_free (ctx.justification)); 569 GNUNET_free (ctx.justification); 570 return 0; 571 } 572 573 574 /** 575 * The AML officer's view filters by investigation and activity. 576 * 577 * @param pg the database context 578 * @return 0 on success 579 */ 580 static int 581 check_decisions (struct TALER_EXCHANGEDB_PostgresContext *pg) 582 { 583 struct HistoryContext ctx; 584 585 memset (&ctx, 586 0, 587 sizeof (ctx)); 588 FAILIF (0 >= 589 TALER_EXCHANGEDB_iterate_aml_decisions (pg, 590 &account.h_normalized, 591 TALER_EXCHANGE_YNA_ALL, 592 TALER_EXCHANGE_YNA_ALL, 593 0, 594 10, 595 &decision_cb, 596 &ctx)); 597 FAILIF_C (2 != ctx.total, 598 GNUNET_free (ctx.justification)); 599 GNUNET_free (ctx.justification); 600 601 /* only the newer decision asked for an investigation */ 602 memset (&ctx, 603 0, 604 sizeof (ctx)); 605 FAILIF (0 >= 606 TALER_EXCHANGEDB_iterate_aml_decisions (pg, 607 &account.h_normalized, 608 TALER_EXCHANGE_YNA_YES, 609 TALER_EXCHANGE_YNA_ALL, 610 0, 611 10, 612 &decision_cb, 613 &ctx)); 614 FAILIF_C (1 != ctx.total, 615 GNUNET_free (ctx.justification)); 616 FAILIF_C (0 != strcmp (ctx.justification, 617 "investigate this"), 618 GNUNET_free (ctx.justification)); 619 GNUNET_free (ctx.justification); 620 621 /* and only one of them is the active outcome */ 622 memset (&ctx, 623 0, 624 sizeof (ctx)); 625 FAILIF (0 >= 626 TALER_EXCHANGEDB_iterate_aml_decisions (pg, 627 &account.h_normalized, 628 TALER_EXCHANGE_YNA_ALL, 629 TALER_EXCHANGE_YNA_YES, 630 0, 631 10, 632 &decision_cb, 633 &ctx)); 634 FAILIF_C (1 != ctx.total, 635 GNUNET_free (ctx.justification)); 636 FAILIF_C (! ctx.is_active, 637 GNUNET_free (ctx.justification)); 638 GNUNET_free (ctx.justification); 639 640 /* an account nobody decided about has nothing */ 641 { 642 struct TALER_NormalizedPaytoHashP other; 643 644 TDB_FILL (other, 645 98); 646 memset (&ctx, 647 0, 648 sizeof (ctx)); 649 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 650 TALER_EXCHANGEDB_iterate_aml_decisions (pg, 651 &other, 652 TALER_EXCHANGE_YNA_ALL, 653 TALER_EXCHANGE_YNA_ALL, 654 0, 655 10, 656 &decision_cb, 657 &ctx)); 658 FAILIF (0 != ctx.total); 659 } 660 return 0; 661 } 662 663 664 /** 665 * The auditor's view walks the decisions by serial and honours an 666 * aborting callback. 667 * 668 * @param pg the database context 669 * @return 0 on success 670 */ 671 static int 672 check_serial (struct TALER_EXCHANGEDB_PostgresContext *pg) 673 { 674 struct HistoryContext ctx; 675 676 memset (&ctx, 677 0, 678 sizeof (ctx)); 679 FAILIF (0 >= 680 TALER_EXCHANGEDB_iterate_aml_history_above_serial_id ( 681 pg, 682 0, 683 &serial_cb, 684 &ctx)); 685 FAILIF_C (2 != ctx.total, 686 GNUNET_free (ctx.justification)); 687 FAILIF_C (! ctx.have_officer, 688 GNUNET_free (ctx.justification)); 689 GNUNET_free (ctx.justification); 690 691 memset (&ctx, 692 0, 693 sizeof (ctx)); 694 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 695 TALER_EXCHANGEDB_iterate_aml_history_above_serial_id ( 696 pg, 697 1000, 698 &serial_cb, 699 &ctx)); 700 FAILIF (0 != ctx.total); 701 702 memset (&ctx, 703 0, 704 sizeof (ctx)); 705 ctx.stop_after = 1; 706 FAILIF (0 >= 707 TALER_EXCHANGEDB_iterate_aml_history_above_serial_id ( 708 pg, 709 0, 710 &serial_cb, 711 &ctx)); 712 FAILIF_C (1 != ctx.total, 713 GNUNET_free (ctx.justification)); 714 GNUNET_free (ctx.justification); 715 return 0; 716 } 717 718 719 /** 720 * The history builder renders the account's AML history as JSON. 721 * 722 * @param pg the database context 723 * @return 0 on success 724 */ 725 static int 726 check_history_builder (struct TALER_EXCHANGEDB_PostgresContext *pg) 727 { 728 struct TALER_AttributeEncryptionKeyP attribute_key; 729 struct TALER_EXCHANGEDB_HistoryBuilderContext hbc = { 730 .account = &account.h_normalized, 731 .pg = pg, 732 .attribute_key = &attribute_key, 733 .is_wallet = false 734 }; 735 json_t *j; 736 737 TDB_FILL (attribute_key, 738 1); 739 j = TALER_EXCHANGEDB_aml_history_builder (&hbc); 740 FAILIF (NULL == j); 741 FAILIF_C (! json_is_array (j), 742 json_decref (j)); 743 FAILIF_C (2 != json_array_size (j), 744 json_decref (j)); 745 json_decref (j); 746 return 0; 747 } 748 749 750 /** 751 * The checks to run, in order. 752 */ 753 static const struct TDB_Test tests[] = { 754 { "aml-history-empty", 755 &check_empty }, 756 { "aml-history-invalid-officer", 757 &check_invalid_officer }, 758 { "aml-history-decide", 759 &check_decide }, 760 { "aml-history-supersede", 761 &check_supersede }, 762 { "aml-history-decisions", 763 &check_decisions }, 764 { "aml-history-serial", 765 &check_serial }, 766 { "aml-history-history-builder", 767 &check_history_builder }, 768 { NULL, NULL } 769 }; 770 771 772 int 773 main (int argc, 774 char *const *argv) 775 { 776 int ret; 777 778 ret = TDB_main (argc, 779 argv, 780 "test-aml-history", 781 "Tests for the exchangedb `aml_history' table", 782 tests); 783 TDB_account_free (&account); 784 return ret; 785 } 786 787 788 /* end of test_aml_history.c */