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 : 1,
359 : true,
360 : new_rules,
361 : properties,
362 : 1,
363 : events),
364 : json_decref (new_rules);
365 : json_decref (properties));
366 : /* the previous outcome was deactivated, not deleted */
367 1 : FAILIF_C (2 != TDB_count (pg,
368 : "FROM legitimization_outcomes"),
369 : json_decref (new_rules); json_decref (properties));
370 1 : FAILIF_C (1 != TDB_count (pg,
371 : "FROM legitimization_outcomes"
372 : " WHERE is_active"),
373 : json_decref (new_rules); json_decref (properties));
374 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
375 : TALER_EXCHANGEDB_get_kyc_rules (pg,
376 : &account.h_normalized,
377 : &got),
378 : json_decref (new_rules); json_decref (properties));
379 1 : FAILIF_C (1 != json_equal (got,
380 : new_rules),
381 : json_decref (got);
382 : json_decref (new_rules); json_decref (properties));
383 1 : json_decref (got);
384 1 : json_decref (new_rules);
385 1 : json_decref (properties);
386 : /* the events the hit asked for were recorded */
387 1 : FAILIF (1 != TDB_count (pg,
388 : "FROM kyc_events"
389 : " WHERE event_type='sanction-hit'"));
390 1 : return 0;
391 : }
392 :
393 :
394 : /**
395 : * The rule builder renders the account's active rules as JSON.
396 : *
397 : * @param pg the database context
398 : * @return 0 on success
399 : */
400 : static int
401 1 : check_rule_builder (struct TALER_EXCHANGEDB_PostgresContext *pg)
402 : {
403 : struct TALER_AttributeEncryptionKeyP attribute_key;
404 1 : struct TALER_EXCHANGEDB_HistoryBuilderContext hbc = {
405 : .account = &account.h_normalized,
406 : .pg = pg,
407 : .attribute_key = &attribute_key,
408 : .is_wallet = false
409 : };
410 : json_t *j;
411 :
412 1 : TDB_FILL (attribute_key,
413 : 1);
414 1 : j = TALER_EXCHANGEDB_current_rule_builder (&hbc);
415 1 : FAILIF (NULL == j);
416 1 : json_decref (j);
417 1 : return 0;
418 : }
419 :
420 :
421 : /**
422 : * The outcome listing reports the decisions with what could account for
423 : * them.
424 : *
425 : * @param pg the database context
426 : * @return 0 on success
427 : */
428 : static int
429 1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
430 : {
431 : struct OutcomeContext ctx;
432 :
433 1 : memset (&ctx,
434 : 0,
435 : sizeof (ctx));
436 1 : ctx.h_payto = &account.h_normalized;
437 1 : FAILIF (0 >=
438 : TALER_EXCHANGEDB_iterate_legitimization_outcomes_above_serial_id (
439 : pg,
440 : 0,
441 : &outcome_cb,
442 : &ctx));
443 1 : FAILIF (2 != ctx.total);
444 1 : FAILIF (2 != ctx.matched);
445 : /* no AML officer signed either decision */
446 1 : FAILIF (ctx.has_aml_decision);
447 : /* and there was no KYC process behind them */
448 1 : FAILIF (ctx.has_legitimization_process);
449 :
450 1 : memset (&ctx,
451 : 0,
452 : sizeof (ctx));
453 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
454 : TALER_EXCHANGEDB_iterate_legitimization_outcomes_above_serial_id (
455 : pg,
456 : 1000,
457 : &outcome_cb,
458 : &ctx));
459 1 : FAILIF (0 != ctx.total);
460 :
461 1 : memset (&ctx,
462 : 0,
463 : sizeof (ctx));
464 1 : ctx.stop_after = 1;
465 1 : FAILIF (0 >=
466 : TALER_EXCHANGEDB_iterate_legitimization_outcomes_above_serial_id (
467 : pg,
468 : 0,
469 : &outcome_cb,
470 : &ctx));
471 1 : FAILIF (1 != ctx.total);
472 1 : return 0;
473 : }
474 :
475 :
476 : /**
477 : * Starting a rule update and cancelling it releases everything.
478 : *
479 : * The update answers through the scheduler, so a check that itself runs
480 : * inside a scheduler task cannot wait for the answer; what is checked is
481 : * that the handle is created and that cancelling it is clean.
482 : *
483 : * @param pg the database context
484 : * @return 0 on success
485 : */
486 : static int
487 1 : check_rule_update (struct TALER_EXCHANGEDB_PostgresContext *pg)
488 : {
489 : struct TALER_AttributeEncryptionKeyP attribute_key;
490 : struct TALER_EXCHANGEDB_RuleUpdater *ru;
491 :
492 1 : TDB_FILL (attribute_key,
493 : 1);
494 1 : ru = TALER_EXCHANGEDB_begin_rule_update (pg,
495 : &attribute_key,
496 : &account.h_normalized,
497 : false,
498 : &rules_cb,
499 : NULL);
500 1 : FAILIF (NULL == ru);
501 1 : TALER_EXCHANGEDB_begin_rule_update_cancel (ru);
502 1 : return 0;
503 : }
504 :
505 :
506 : /**
507 : * The checks to run, in order.
508 : */
509 : static const struct TDB_Test tests[] = {
510 : { "legitimization-outcomes-empty",
511 : &check_empty },
512 : { "legitimization-outcomes-persist",
513 : &check_persist },
514 : { "legitimization-outcomes-rules-with-account",
515 : &check_rules_with_account },
516 : { "legitimization-outcomes-sanction-hit",
517 : &check_sanction_hit },
518 : { "legitimization-outcomes-rule-builder",
519 : &check_rule_builder },
520 : { "legitimization-outcomes-iterate",
521 : &check_iterate },
522 : { "legitimization-outcomes-rule-update",
523 : &check_rule_update },
524 : { NULL, NULL }
525 : };
526 :
527 :
528 : int
529 1 : main (int argc,
530 : char *const *argv)
531 : {
532 : int ret;
533 :
534 1 : ret = TDB_main (argc,
535 : argv,
536 : "test-legitimization-outcomes",
537 : "Tests for the exchangedb `legitimization_outcomes' table",
538 : tests);
539 1 : TDB_account_free (&account);
540 1 : return ret;
541 : }
542 :
543 :
544 : /* end of test_legitimization_outcomes.c */
|