exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

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 */