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