LCOV - code coverage report
Current view: top level - exchangedb - test_legitimization_outcomes.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 97.4 % 116 113
Test Date: 2026-09-11 18:55:36 Functions: 90.9 % 11 10

            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_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            2 : make_rules (const char *name)
      65              : {
      66              :   json_t *j;
      67              : 
      68            2 :   j = GNUNET_JSON_PACK (
      69              :     GNUNET_JSON_pack_string ("rule_name",
      70              :                              name),
      71              :     GNUNET_JSON_pack_array_steal ("rules",
      72              :                                   json_array ()));
      73            2 :   GNUNET_assert (NULL != j);
      74            2 :   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            3 : 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            3 :   struct OutcomeContext *ctx = cls;
     145              : 
     146              :   (void) rowid;
     147              :   (void) decision_time;
     148              :   (void) expiration_time;
     149            3 :   ctx->total++;
     150            3 :   if ( (NULL != ctx->h_payto) &&
     151            2 :        (0 == GNUNET_memcmp (h_payto,
     152              :                             ctx->h_payto)) )
     153              :   {
     154            2 :     ctx->matched++;
     155            2 :     ctx->has_aml_decision = has_aml_decision;
     156            2 :     ctx->has_legitimization_process = has_legitimization_process;
     157            2 :     ctx->has_expired_predecessor = has_expired_predecessor;
     158              :   }
     159            3 :   if ( (0 != ctx->stop_after) &&
     160            1 :        (ctx->total >= ctx->stop_after) )
     161            1 :     return GNUNET_SYSERR;
     162            2 :   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            0 : rules_cb (void *cls,
     175              :           struct TALER_EXCHANGEDB_RuleUpdaterResult *rur)
     176              : {
     177              :   (void) cls;
     178              :   (void) rur;
     179            0 :   GNUNET_break (0);
     180            0 : }
     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            1 : check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
     191              : {
     192              :   struct TALER_NormalizedPaytoHashP h_payto;
     193            1 :   struct OutcomeContext ctx = { 0 };
     194            1 :   json_t *jrules = NULL;
     195              :   uint64_t rowid;
     196              : 
     197            1 :   TDB_account (pg,
     198              :                10,
     199              :                &account);
     200            1 :   TDB_FILL (h_payto,
     201              :             1);
     202            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     203              :           TALER_EXCHANGEDB_get_kyc_rules (pg,
     204              :                                           &h_payto,
     205              :                                           &jrules));
     206            1 :   FAILIF (NULL != jrules);
     207            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     208              :           TALER_EXCHANGEDB_get_rules_by_access_token (pg,
     209              :                                                       &h_payto,
     210              :                                                       &jrules,
     211              :                                                       &rowid));
     212            1 :   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            1 :   FAILIF (0 != ctx.total);
     219              :   /* an account that exists but has no decision has none either */
     220            1 :   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            1 :   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            1 : check_persist (struct TALER_EXCHANGEDB_PostgresContext *pg)
     238              : {
     239              :   struct TALER_KYCLOGIC_AmlProgramResult apr;
     240              :   enum TALER_EXCHANGEDB_PersistProgramResultStatus pprs;
     241            1 :   json_t *new_rules = make_rules ("strict");
     242            1 :   json_t *properties = GNUNET_JSON_PACK (
     243              :     GNUNET_JSON_pack_string ("FILE_NOTE",
     244              :                              "checked by hand"));
     245            1 :   json_t *got = NULL;
     246            1 :   uint64_t rowid = 0;
     247              : 
     248            1 :   GNUNET_assert (NULL != properties);
     249            1 :   memset (&apr,
     250              :           0,
     251              :           sizeof (apr));
     252            1 :   apr.status = TALER_KYCLOGIC_AMLR_SUCCESS;
     253            1 :   apr.details.success.account_properties = properties;
     254            1 :   apr.details.success.new_rules = new_rules;
     255              :   apr.details.success.expiration_time
     256            1 :     = GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS);
     257            1 :   apr.details.success.to_investigate = true;
     258            1 :   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            1 :   FAILIF_C (TALER_EXCHANGEDB_PPRS_OK != pprs,
     267              :             json_decref (new_rules); json_decref (properties));
     268            1 :   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            1 :   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            1 :   FAILIF_C (1 != json_equal (got,
     279              :                              new_rules),
     280              :             json_decref (got);
     281              :             json_decref (new_rules); json_decref (properties));
     282            1 :   json_decref (got);
     283            1 :   got = NULL;
     284              : 
     285            1 :   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            1 :   FAILIF_C (0 == rowid,
     293              :             json_decref (got);
     294              :             json_decref (new_rules); json_decref (properties));
     295            1 :   json_decref (got);
     296            1 :   json_decref (new_rules);
     297            1 :   json_decref (properties);
     298            1 :   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            1 : check_rules_with_account (struct TALER_EXCHANGEDB_PostgresContext *pg)
     311              : {
     312              :   union TALER_AccountPublicKeyP account_pub;
     313              :   struct TALER_ReservePublicKeyP reserve_pub;
     314            1 :   json_t *jrules = NULL;
     315            1 :   bool no_account_pub = false;
     316            1 :   bool no_reserve_pub = false;
     317              : 
     318            1 :   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            1 :   FAILIF_C (NULL == jrules,
     328              :             json_decref (jrules));
     329            1 :   json_decref (jrules);
     330              :   /* nobody ever registered a key for this account */
     331            1 :   FAILIF (! no_account_pub);
     332            1 :   FAILIF (! no_reserve_pub);
     333            1 :   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            1 : check_sanction_hit (struct TALER_EXCHANGEDB_PostgresContext *pg)
     345              : {
     346            1 :   json_t *new_rules = make_rules ("frozen");
     347            1 :   json_t *properties = GNUNET_JSON_PACK (
     348              :     GNUNET_JSON_pack_bool ("HIGH_RISK_CUSTOMER",
     349              :                            true));
     350            1 :   json_t *got = NULL;
     351            1 :   const char *events[] = { "sanction-hit" };
     352              : 
     353            1 :   GNUNET_assert (NULL != properties);
     354            1 :   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            1 :   FAILIF_C (2 != TDB_count (pg,
     366              :                             "FROM legitimization_outcomes"),
     367              :             json_decref (new_rules); json_decref (properties));
     368            1 :   FAILIF_C (1 != TDB_count (pg,
     369              :                             "FROM legitimization_outcomes"
     370              :                             " WHERE is_active"),
     371              :             json_decref (new_rules); json_decref (properties));
     372            1 :   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            1 :   FAILIF_C (1 != json_equal (got,
     378              :                              new_rules),
     379              :             json_decref (got);
     380              :             json_decref (new_rules); json_decref (properties));
     381            1 :   json_decref (got);
     382            1 :   json_decref (new_rules);
     383            1 :   json_decref (properties);
     384              :   /* the events the hit asked for were recorded */
     385            1 :   FAILIF (1 != TDB_count (pg,
     386              :                           "FROM kyc_events"
     387              :                           " WHERE event_type='sanction-hit'"));
     388            1 :   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            1 : check_rule_builder (struct TALER_EXCHANGEDB_PostgresContext *pg)
     400              : {
     401              :   struct TALER_AttributeEncryptionKeyP attribute_key;
     402            1 :   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            1 :   TDB_FILL (attribute_key,
     411              :             1);
     412            1 :   j = TALER_EXCHANGEDB_current_rule_builder (&hbc);
     413            1 :   FAILIF (NULL == j);
     414            1 :   json_decref (j);
     415            1 :   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            1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
     428              : {
     429              :   struct OutcomeContext ctx;
     430              : 
     431            1 :   memset (&ctx,
     432              :           0,
     433              :           sizeof (ctx));
     434            1 :   ctx.h_payto = &account.h_normalized;
     435            1 :   FAILIF (0 >=
     436              :           TALER_EXCHANGEDB_iterate_legitimization_outcomes_above_serial_id (
     437              :             pg,
     438              :             0,
     439              :             &outcome_cb,
     440              :             &ctx));
     441            1 :   FAILIF (2 != ctx.total);
     442            1 :   FAILIF (2 != ctx.matched);
     443              :   /* no AML officer signed either decision */
     444            1 :   FAILIF (ctx.has_aml_decision);
     445              :   /* and there was no KYC process behind them */
     446            1 :   FAILIF (ctx.has_legitimization_process);
     447              : 
     448            1 :   memset (&ctx,
     449              :           0,
     450              :           sizeof (ctx));
     451            1 :   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            1 :   FAILIF (0 != ctx.total);
     458              : 
     459            1 :   memset (&ctx,
     460              :           0,
     461              :           sizeof (ctx));
     462            1 :   ctx.stop_after = 1;
     463            1 :   FAILIF (0 >=
     464              :           TALER_EXCHANGEDB_iterate_legitimization_outcomes_above_serial_id (
     465              :             pg,
     466              :             0,
     467              :             &outcome_cb,
     468              :             &ctx));
     469            1 :   FAILIF (1 != ctx.total);
     470            1 :   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            1 : check_rule_update (struct TALER_EXCHANGEDB_PostgresContext *pg)
     486              : {
     487              :   struct TALER_AttributeEncryptionKeyP attribute_key;
     488              :   struct TALER_EXCHANGEDB_RuleUpdater *ru;
     489              : 
     490            1 :   TDB_FILL (attribute_key,
     491              :             1);
     492            1 :   ru = TALER_EXCHANGEDB_begin_rule_update (pg,
     493              :                                            &attribute_key,
     494              :                                            &account.h_normalized,
     495              :                                            false,
     496              :                                            &rules_cb,
     497              :                                            NULL);
     498            1 :   FAILIF (NULL == ru);
     499            1 :   TALER_EXCHANGEDB_begin_rule_update_cancel (ru);
     500            1 :   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            1 : main (int argc,
     528              :       char *const *argv)
     529              : {
     530              :   int ret;
     531              : 
     532            1 :   ret = TDB_main (argc,
     533              :                   argv,
     534              :                   "test-legitimization-outcomes",
     535              :                   "Tests for the exchangedb `legitimization_outcomes' table",
     536              :                   tests);
     537            1 :   TDB_account_free (&account);
     538            1 :   return ret;
     539              : }
     540              : 
     541              : 
     542              : /* end of test_legitimization_outcomes.c */
        

Generated by: LCOV version 2.0-1