test_legitimization_outcomes.c (16567B)
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_legitimization_outcomes.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `legitimization_outcomes` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_get_kyc_rules(), 23 * #TALER_EXCHANGEDB_get_kyc_rules_with_account(), 24 * #TALER_EXCHANGEDB_get_rules_by_access_token(), 25 * #TALER_EXCHANGEDB_insert_sanction_list_hit(), 26 * #TALER_EXCHANGEDB_do_persist_aml_program_result(), 27 * #TALER_EXCHANGEDB_current_rule_builder(), 28 * #TALER_EXCHANGEDB_iterate_legitimization_outcomes_above_serial_id() and, 29 * as far as a synchronous test can, #TALER_EXCHANGEDB_begin_rule_update() 30 * and #TALER_EXCHANGEDB_begin_rule_update_cancel(). 31 * 32 * A row of `legitimization_outcomes` is one decision about an account: the 33 * rule set that applies to it from then on. Only one row per account is 34 * active at a time; writing a new one deactivates the old. 35 * 36 * begin_rule_update() answers through the scheduler, so a check that runs 37 * inside one scheduler task cannot wait for it. What is checked here is 38 * that it starts and that cancelling it releases everything. 39 */ 40 #include "test_common.h" 41 #include "exchange-database/account_history.h" 42 #include "exchange-database/begin_rule_update.h" 43 #include "exchange-database/do_persist_aml_program_result.h" 44 #include "exchange-database/do_trigger_kyc_rule_for_account.h" 45 #include "exchange-database/get_kyc_rules.h" 46 #include "exchange-database/get_rules_by_access_token.h" 47 #include "exchange-database/insert_sanction_list_hit.h" 48 #include "exchange-database/iterate_legitimization_outcomes_above_serial_id.h" 49 50 51 /** 52 * Account the checks decide about. 53 */ 54 static struct TDB_Account account; 55 56 57 /** 58 * Build a rule set naming @a name as its only rule. 59 * 60 * @param name name of the rule 61 * @return the rule set, to be freed with json_decref() 62 */ 63 static json_t * 64 make_rules (const char *name) 65 { 66 json_t *j; 67 68 j = GNUNET_JSON_PACK ( 69 GNUNET_JSON_pack_string ("rule_name", 70 name), 71 GNUNET_JSON_pack_array_steal ("rules", 72 json_array ())); 73 GNUNET_assert (NULL != j); 74 return j; 75 } 76 77 78 /** 79 * Closure for #outcome_cb(). 80 */ 81 struct OutcomeContext 82 { 83 /** 84 * How many outcomes did the callback see? 85 */ 86 unsigned int total; 87 88 /** 89 * Stop after this many rows; 0 for no limit. 90 */ 91 unsigned int stop_after; 92 93 /** 94 * Account we are looking for, NULL to match nothing. 95 */ 96 const struct TALER_NormalizedPaytoHashP *h_payto; 97 98 /** 99 * How many times did we see it? 100 */ 101 unsigned int matched; 102 103 /** 104 * Whether the last matching outcome had an AML officer behind it. 105 */ 106 bool has_aml_decision; 107 108 /** 109 * Whether it had a legitimization process behind it. 110 */ 111 bool has_legitimization_process; 112 113 /** 114 * Whether an earlier outcome had expired by then. 115 */ 116 bool has_expired_predecessor; 117 }; 118 119 120 /** 121 * Callback for 122 * #TALER_EXCHANGEDB_iterate_legitimization_outcomes_above_serial_id(). 123 * 124 * @param cls a `struct OutcomeContext *` 125 * @param rowid row of the outcome 126 * @param h_payto account the outcome is about 127 * @param decision_time when it was decided 128 * @param expiration_time when it expires 129 * @param has_aml_decision whether an AML officer signed it 130 * @param has_legitimization_process whether a KYC process preceded it 131 * @param has_expired_predecessor whether it succeeded an expired outcome 132 * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop 133 */ 134 static enum GNUNET_GenericReturnValue 135 outcome_cb (void *cls, 136 uint64_t rowid, 137 const struct TALER_NormalizedPaytoHashP *h_payto, 138 struct GNUNET_TIME_Timestamp decision_time, 139 struct GNUNET_TIME_Timestamp expiration_time, 140 bool has_aml_decision, 141 bool has_legitimization_process, 142 bool has_expired_predecessor) 143 { 144 struct OutcomeContext *ctx = cls; 145 146 (void) rowid; 147 (void) decision_time; 148 (void) expiration_time; 149 ctx->total++; 150 if ( (NULL != ctx->h_payto) && 151 (0 == GNUNET_memcmp (h_payto, 152 ctx->h_payto)) ) 153 { 154 ctx->matched++; 155 ctx->has_aml_decision = has_aml_decision; 156 ctx->has_legitimization_process = has_legitimization_process; 157 ctx->has_expired_predecessor = has_expired_predecessor; 158 } 159 if ( (0 != ctx->stop_after) && 160 (ctx->total >= ctx->stop_after) ) 161 return GNUNET_SYSERR; 162 return GNUNET_OK; 163 } 164 165 166 /** 167 * Callback for #TALER_EXCHANGEDB_begin_rule_update(), which a 168 * synchronous check never gets to see. 169 * 170 * @param cls closure 171 * @param rur the rule set that was worked out 172 */ 173 static void 174 rules_cb (void *cls, 175 struct TALER_EXCHANGEDB_RuleUpdaterResult *rur) 176 { 177 (void) cls; 178 (void) rur; 179 GNUNET_break (0); 180 } 181 182 183 /** 184 * An account nobody decided about has no custom rules. 185 * 186 * @param pg the database context 187 * @return 0 on success 188 */ 189 static int 190 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 191 { 192 struct TALER_NormalizedPaytoHashP h_payto; 193 struct OutcomeContext ctx = { 0 }; 194 json_t *jrules = NULL; 195 uint64_t rowid; 196 197 TDB_account (pg, 198 10, 199 &account); 200 TDB_FILL (h_payto, 201 1); 202 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 203 TALER_EXCHANGEDB_get_kyc_rules (pg, 204 &h_payto, 205 &jrules)); 206 FAILIF (NULL != jrules); 207 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 208 TALER_EXCHANGEDB_get_rules_by_access_token (pg, 209 &h_payto, 210 &jrules, 211 &rowid)); 212 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 213 TALER_EXCHANGEDB_iterate_legitimization_outcomes_above_serial_id ( 214 pg, 215 0, 216 &outcome_cb, 217 &ctx)); 218 FAILIF (0 != ctx.total); 219 /* an account that exists but has no decision has none either */ 220 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 221 TALER_EXCHANGEDB_get_rules_by_access_token (pg, 222 &account.h_normalized, 223 &jrules, 224 &rowid)); 225 return 0; 226 } 227 228 229 /** 230 * An AML program result is persisted as an outcome, and the rules it set 231 * become the account's active rules. 232 * 233 * @param pg the database context 234 * @return 0 on success 235 */ 236 static int 237 check_persist (struct TALER_EXCHANGEDB_PostgresContext *pg) 238 { 239 struct TALER_KYCLOGIC_AmlProgramResult apr; 240 enum TALER_EXCHANGEDB_PersistProgramResultStatus pprs; 241 json_t *new_rules = make_rules ("strict"); 242 json_t *properties = GNUNET_JSON_PACK ( 243 GNUNET_JSON_pack_string ("FILE_NOTE", 244 "checked by hand")); 245 json_t *got = NULL; 246 uint64_t rowid = 0; 247 248 GNUNET_assert (NULL != properties); 249 memset (&apr, 250 0, 251 sizeof (apr)); 252 apr.status = TALER_KYCLOGIC_AMLR_SUCCESS; 253 apr.details.success.account_properties = properties; 254 apr.details.success.new_rules = new_rules; 255 apr.details.success.expiration_time 256 = GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS); 257 apr.details.success.to_investigate = true; 258 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 259 TALER_EXCHANGEDB_do_persist_aml_program_result ( 260 pg, 261 0, 262 &account.h_normalized, 263 &apr, 264 &pprs), 265 json_decref (new_rules); json_decref (properties)); 266 FAILIF_C (TALER_EXCHANGEDB_PPRS_OK != pprs, 267 json_decref (new_rules); json_decref (properties)); 268 FAILIF_C (1 != TDB_count (pg, 269 "FROM legitimization_outcomes"), 270 json_decref (new_rules); json_decref (properties)); 271 272 /* the rules are now the account's active rules */ 273 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 274 TALER_EXCHANGEDB_get_kyc_rules (pg, 275 &account.h_normalized, 276 &got), 277 json_decref (new_rules); json_decref (properties)); 278 FAILIF_C (1 != json_equal (got, 279 new_rules), 280 json_decref (got); 281 json_decref (new_rules); json_decref (properties)); 282 json_decref (got); 283 got = NULL; 284 285 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 286 TALER_EXCHANGEDB_get_rules_by_access_token ( 287 pg, 288 &account.h_normalized, 289 &got, 290 &rowid), 291 json_decref (new_rules); json_decref (properties)); 292 FAILIF_C (0 == rowid, 293 json_decref (got); 294 json_decref (new_rules); json_decref (properties)); 295 json_decref (got); 296 json_decref (new_rules); 297 json_decref (properties); 298 return 0; 299 } 300 301 302 /** 303 * The account-aware rule lookup also reports the keys that may act for 304 * the account. 305 * 306 * @param pg the database context 307 * @return 0 on success 308 */ 309 static int 310 check_rules_with_account (struct TALER_EXCHANGEDB_PostgresContext *pg) 311 { 312 union TALER_AccountPublicKeyP account_pub; 313 struct TALER_ReservePublicKeyP reserve_pub; 314 json_t *jrules = NULL; 315 bool no_account_pub = false; 316 bool no_reserve_pub = false; 317 318 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 319 TALER_EXCHANGEDB_get_kyc_rules_with_account (pg, 320 &account.h_normalized, 321 NULL, 322 &no_account_pub, 323 &account_pub, 324 &no_reserve_pub, 325 &reserve_pub, 326 &jrules)); 327 FAILIF_C (NULL == jrules, 328 json_decref (jrules)); 329 json_decref (jrules); 330 /* nobody ever registered a key for this account */ 331 FAILIF (! no_account_pub); 332 FAILIF (! no_reserve_pub); 333 return 0; 334 } 335 336 337 /** 338 * A sanction list hit replaces the account's rules and flags it. 339 * 340 * @param pg the database context 341 * @return 0 on success 342 */ 343 static int 344 check_sanction_hit (struct TALER_EXCHANGEDB_PostgresContext *pg) 345 { 346 json_t *new_rules = make_rules ("frozen"); 347 json_t *properties = GNUNET_JSON_PACK ( 348 GNUNET_JSON_pack_bool ("HIGH_RISK_CUSTOMER", 349 true)); 350 json_t *got = NULL; 351 const char *events[] = { "sanction-hit" }; 352 353 GNUNET_assert (NULL != properties); 354 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 355 TALER_EXCHANGEDB_insert_sanction_list_hit ( 356 pg, 357 &account.h_normalized, 358 true, 359 new_rules, 360 properties, 361 1, 362 events), 363 json_decref (new_rules); json_decref (properties)); 364 /* the previous outcome was deactivated, not deleted */ 365 FAILIF_C (2 != TDB_count (pg, 366 "FROM legitimization_outcomes"), 367 json_decref (new_rules); json_decref (properties)); 368 FAILIF_C (1 != TDB_count (pg, 369 "FROM legitimization_outcomes" 370 " WHERE is_active"), 371 json_decref (new_rules); json_decref (properties)); 372 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 373 TALER_EXCHANGEDB_get_kyc_rules (pg, 374 &account.h_normalized, 375 &got), 376 json_decref (new_rules); json_decref (properties)); 377 FAILIF_C (1 != json_equal (got, 378 new_rules), 379 json_decref (got); 380 json_decref (new_rules); json_decref (properties)); 381 json_decref (got); 382 json_decref (new_rules); 383 json_decref (properties); 384 /* the events the hit asked for were recorded */ 385 FAILIF (1 != TDB_count (pg, 386 "FROM kyc_events" 387 " WHERE event_type='sanction-hit'")); 388 return 0; 389 } 390 391 392 /** 393 * The rule builder renders the account's active rules as JSON. 394 * 395 * @param pg the database context 396 * @return 0 on success 397 */ 398 static int 399 check_rule_builder (struct TALER_EXCHANGEDB_PostgresContext *pg) 400 { 401 struct TALER_AttributeEncryptionKeyP attribute_key; 402 struct TALER_EXCHANGEDB_HistoryBuilderContext hbc = { 403 .account = &account.h_normalized, 404 .pg = pg, 405 .attribute_key = &attribute_key, 406 .is_wallet = false 407 }; 408 json_t *j; 409 410 TDB_FILL (attribute_key, 411 1); 412 j = TALER_EXCHANGEDB_current_rule_builder (&hbc); 413 FAILIF (NULL == j); 414 json_decref (j); 415 return 0; 416 } 417 418 419 /** 420 * The outcome listing reports the decisions with what could account for 421 * them. 422 * 423 * @param pg the database context 424 * @return 0 on success 425 */ 426 static int 427 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg) 428 { 429 struct OutcomeContext ctx; 430 431 memset (&ctx, 432 0, 433 sizeof (ctx)); 434 ctx.h_payto = &account.h_normalized; 435 FAILIF (0 >= 436 TALER_EXCHANGEDB_iterate_legitimization_outcomes_above_serial_id ( 437 pg, 438 0, 439 &outcome_cb, 440 &ctx)); 441 FAILIF (2 != ctx.total); 442 FAILIF (2 != ctx.matched); 443 /* no AML officer signed either decision */ 444 FAILIF (ctx.has_aml_decision); 445 /* and there was no KYC process behind them */ 446 FAILIF (ctx.has_legitimization_process); 447 448 memset (&ctx, 449 0, 450 sizeof (ctx)); 451 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 452 TALER_EXCHANGEDB_iterate_legitimization_outcomes_above_serial_id ( 453 pg, 454 1000, 455 &outcome_cb, 456 &ctx)); 457 FAILIF (0 != ctx.total); 458 459 memset (&ctx, 460 0, 461 sizeof (ctx)); 462 ctx.stop_after = 1; 463 FAILIF (0 >= 464 TALER_EXCHANGEDB_iterate_legitimization_outcomes_above_serial_id ( 465 pg, 466 0, 467 &outcome_cb, 468 &ctx)); 469 FAILIF (1 != ctx.total); 470 return 0; 471 } 472 473 474 /** 475 * Starting a rule update and cancelling it releases everything. 476 * 477 * The update answers through the scheduler, so a check that itself runs 478 * inside a scheduler task cannot wait for the answer; what is checked is 479 * that the handle is created and that cancelling it is clean. 480 * 481 * @param pg the database context 482 * @return 0 on success 483 */ 484 static int 485 check_rule_update (struct TALER_EXCHANGEDB_PostgresContext *pg) 486 { 487 struct TALER_AttributeEncryptionKeyP attribute_key; 488 struct TALER_EXCHANGEDB_RuleUpdater *ru; 489 490 TDB_FILL (attribute_key, 491 1); 492 ru = TALER_EXCHANGEDB_begin_rule_update (pg, 493 &attribute_key, 494 &account.h_normalized, 495 false, 496 &rules_cb, 497 NULL); 498 FAILIF (NULL == ru); 499 TALER_EXCHANGEDB_begin_rule_update_cancel (ru); 500 return 0; 501 } 502 503 504 /** 505 * The checks to run, in order. 506 */ 507 static const struct TDB_Test tests[] = { 508 { "legitimization-outcomes-empty", 509 &check_empty }, 510 { "legitimization-outcomes-persist", 511 &check_persist }, 512 { "legitimization-outcomes-rules-with-account", 513 &check_rules_with_account }, 514 { "legitimization-outcomes-sanction-hit", 515 &check_sanction_hit }, 516 { "legitimization-outcomes-rule-builder", 517 &check_rule_builder }, 518 { "legitimization-outcomes-iterate", 519 &check_iterate }, 520 { "legitimization-outcomes-rule-update", 521 &check_rule_update }, 522 { NULL, NULL } 523 }; 524 525 526 int 527 main (int argc, 528 char *const *argv) 529 { 530 int ret; 531 532 ret = TDB_main (argc, 533 argv, 534 "test-legitimization-outcomes", 535 "Tests for the exchangedb `legitimization_outcomes' table", 536 tests); 537 TDB_account_free (&account); 538 return ret; 539 } 540 541 542 /* end of test_legitimization_outcomes.c */