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_kyc_events.c
18 : * @brief tests for the exchangedb functions whose primary table is
19 : * `kyc_events`
20 : * @author Christian Grothoff
21 : *
22 : * Covers #TALER_EXCHANGEDB_iterate_aml_statistics().
23 : *
24 : * `kyc_events` is a plain counter log: AML decisions may name events,
25 : * each of which becomes one row, and the statistics query groups those
26 : * rows by name over a time range. Two of the names are special --
27 : * ACCOUNT_OPEN and ACCOUNT_IDLE also move the account's open/close time
28 : * in `kyc_targets` -- so those are checked as well.
29 : */
30 : #include "test_common.h"
31 : #include "exchange-database/insert_aml_decision.h"
32 : #include "exchange-database/insert_aml_officer.h"
33 : #include "exchange-database/iterate_aml_statistics.h"
34 :
35 :
36 : /**
37 : * One second, in microseconds.
38 : */
39 : #define SECOND 1000000LLU
40 :
41 :
42 : /**
43 : * Times at which we take our three decisions, in seconds since the
44 : * epoch.
45 : */
46 : #define T1 1600000000
47 : #define T2 1600003600
48 : #define T3 1600007200
49 :
50 :
51 : /**
52 : * Account the decisions are about.
53 : */
54 : static struct TDB_Account account;
55 :
56 :
57 : /**
58 : * Officer taking the decisions.
59 : */
60 : static struct TALER_AmlOfficerPublicKeyP officer_pub;
61 :
62 :
63 : /**
64 : * Build a timestamp from a number of seconds since the epoch.
65 : *
66 : * @param secs seconds since the epoch
67 : * @return the timestamp
68 : */
69 : static struct GNUNET_TIME_Timestamp
70 19 : ts (uint64_t secs)
71 : {
72 19 : struct GNUNET_TIME_Absolute abs = {
73 19 : .abs_value_us = secs * SECOND
74 : };
75 :
76 19 : return GNUNET_TIME_absolute_to_timestamp (abs);
77 : }
78 :
79 :
80 : /**
81 : * Closure for #statistics_cb().
82 : */
83 : struct StatsContext
84 : {
85 : /**
86 : * How many counters did the callback see?
87 : */
88 : unsigned int total;
89 :
90 : /**
91 : * Name we are interested in.
92 : */
93 : const char *name;
94 :
95 : /**
96 : * Count reported for @e name, UINT64_MAX if it was not reported.
97 : */
98 : uint64_t cnt;
99 : };
100 :
101 :
102 : /**
103 : * Callback for #TALER_EXCHANGEDB_iterate_aml_statistics().
104 : *
105 : * @param cls a `struct StatsContext *`
106 : * @param name name of the counter
107 : * @param cnt value of the counter
108 : */
109 : static void
110 5 : statistics_cb (void *cls,
111 : const char *name,
112 : uint64_t cnt)
113 : {
114 5 : struct StatsContext *ctx = cls;
115 :
116 5 : ctx->total++;
117 5 : if ( (NULL != ctx->name) &&
118 5 : (0 == strcmp (name,
119 : ctx->name)) )
120 3 : ctx->cnt = cnt;
121 5 : }
122 :
123 :
124 : /**
125 : * Take an AML decision that triggers @a events.
126 : *
127 : * @param pg the database context
128 : * @param seed seed for the officer's signature
129 : * @param when when the decision is taken, in seconds since the epoch
130 : * @param num_events length of @a events
131 : * @param events the events to trigger
132 : * @return transaction status
133 : */
134 : static enum GNUNET_DB_QueryStatus
135 4 : decide (struct TALER_EXCHANGEDB_PostgresContext *pg,
136 : uint32_t seed,
137 : uint64_t when,
138 : size_t num_events,
139 : const char *events[static num_events])
140 4 : {
141 : struct TALER_AmlOfficerSignatureP decider_sig;
142 4 : struct TALER_FullPayto null_payto = { NULL };
143 : struct GNUNET_TIME_Timestamp last_date;
144 : json_t *new_rules;
145 : bool invalid_officer;
146 : bool unknown_account;
147 : bool is_wallet;
148 : uint64_t lms;
149 : enum GNUNET_DB_QueryStatus qs;
150 :
151 4 : TDB_fill (&decider_sig,
152 : sizeof (decider_sig),
153 : seed);
154 4 : new_rules = GNUNET_JSON_PACK (
155 : GNUNET_JSON_pack_array_steal ("rules",
156 : json_array ()));
157 4 : GNUNET_assert (NULL != new_rules);
158 4 : qs = TALER_EXCHANGEDB_insert_aml_decision (
159 : pg,
160 : null_payto,
161 : &account.h_normalized,
162 : ts (when),
163 : GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS),
164 : NULL,
165 : new_rules,
166 : false,
167 : NULL,
168 : NULL,
169 : "statistics",
170 : &officer_pub,
171 : &decider_sig,
172 : num_events,
173 : events,
174 : NULL,
175 : 0,
176 : NULL,
177 : NULL,
178 4 : GNUNET_TIME_UNIT_ZERO_TS,
179 : &invalid_officer,
180 : &unknown_account,
181 : &last_date,
182 : &lms,
183 : &is_wallet);
184 4 : json_decref (new_rules);
185 4 : GNUNET_assert (! invalid_officer);
186 4 : return qs;
187 : }
188 :
189 :
190 : /**
191 : * No counters while the table is empty.
192 : *
193 : * @param pg the database context
194 : * @return 0 on success
195 : */
196 : static int
197 1 : check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
198 : {
199 1 : const char *names[] = { "ACCOUNT_OPEN", "sar-filed" };
200 : struct TALER_MasterSignatureP master_sig;
201 : struct GNUNET_TIME_Timestamp previous_change;
202 1 : struct StatsContext ctx = { 0 };
203 :
204 1 : TDB_account (pg,
205 : 10,
206 : &account);
207 1 : TDB_FILL (officer_pub,
208 : 20);
209 1 : TDB_FILL (master_sig,
210 : 21);
211 1 : GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
212 : TALER_EXCHANGEDB_insert_aml_officer (pg,
213 : &officer_pub,
214 : &master_sig,
215 : "Alex Officer",
216 : true,
217 : false,
218 : ts (T1 - 1),
219 : &previous_change));
220 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
221 : TALER_EXCHANGEDB_iterate_aml_statistics (pg,
222 : 2,
223 : names,
224 : ts (0),
225 : ts (T3 + 1),
226 : &statistics_cb,
227 : &ctx));
228 1 : FAILIF (0 != ctx.total);
229 1 : return 0;
230 : }
231 :
232 :
233 : /**
234 : * Decisions record their events, and the two account-lifecycle events
235 : * also move the account's open and close time.
236 : *
237 : * @param pg the database context
238 : * @return 0 on success
239 : */
240 : static int
241 1 : check_record (struct TALER_EXCHANGEDB_PostgresContext *pg)
242 : {
243 1 : const char *open_and_sar[] = { "ACCOUNT_OPEN", "sar-filed" };
244 1 : const char *sar[] = { "sar-filed" };
245 1 : const char *idle[] = { "ACCOUNT_IDLE" };
246 1 : const char *none[1] = { NULL };
247 :
248 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
249 : decide (pg,
250 : 30,
251 : T1,
252 : 2,
253 : open_and_sar));
254 1 : FAILIF (2 != TDB_count (pg,
255 : "FROM kyc_events"));
256 1 : FAILIF (1 != TDB_count (pg,
257 : "FROM kyc_targets"
258 : " WHERE open_time=%llu"
259 : " AND close_time IS NULL",
260 : (unsigned long long) (T1 * SECOND)));
261 :
262 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
263 : decide (pg,
264 : 31,
265 : T2,
266 : 1,
267 : sar));
268 1 : FAILIF (3 != TDB_count (pg,
269 : "FROM kyc_events"));
270 :
271 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
272 : decide (pg,
273 : 32,
274 : T3,
275 : 1,
276 : idle));
277 1 : FAILIF (4 != TDB_count (pg,
278 : "FROM kyc_events"));
279 1 : FAILIF (1 != TDB_count (pg,
280 : "FROM kyc_targets"
281 : " WHERE close_time=%llu",
282 : (unsigned long long) (T3 * SECOND)));
283 :
284 : /* a decision without events records none */
285 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
286 : decide (pg,
287 : 33,
288 : T3 + 1,
289 : 0,
290 : none));
291 1 : FAILIF (4 != TDB_count (pg,
292 : "FROM kyc_events"));
293 1 : return 0;
294 : }
295 :
296 :
297 : /**
298 : * The statistics group the events by name over the requested range.
299 : *
300 : * @param pg the database context
301 : * @return 0 on success
302 : */
303 : static int
304 1 : check_statistics (struct TALER_EXCHANGEDB_PostgresContext *pg)
305 : {
306 1 : const char *all[] = { "ACCOUNT_OPEN", "ACCOUNT_IDLE", "sar-filed" };
307 1 : const char *sar[] = { "sar-filed" };
308 1 : const char *unknown[] = { "no-such-event" };
309 : struct StatsContext ctx;
310 :
311 : /* over the whole range every requested name that occurred is
312 : reported, once */
313 1 : memset (&ctx,
314 : 0,
315 : sizeof (ctx));
316 1 : ctx.name = "sar-filed";
317 1 : ctx.cnt = UINT64_MAX;
318 1 : FAILIF (0 >=
319 : TALER_EXCHANGEDB_iterate_aml_statistics (pg,
320 : 3,
321 : all,
322 : ts (0),
323 : ts (T3 + 2),
324 : &statistics_cb,
325 : &ctx));
326 1 : FAILIF (3 != ctx.total);
327 1 : FAILIF (2 != ctx.cnt);
328 :
329 : /* a name that was not asked for is not reported, even though it
330 : happened */
331 1 : memset (&ctx,
332 : 0,
333 : sizeof (ctx));
334 1 : ctx.name = "sar-filed";
335 1 : ctx.cnt = UINT64_MAX;
336 1 : FAILIF (0 >=
337 : TALER_EXCHANGEDB_iterate_aml_statistics (pg,
338 : 1,
339 : sar,
340 : ts (0),
341 : ts (T3 + 2),
342 : &statistics_cb,
343 : &ctx));
344 1 : FAILIF (1 != ctx.total);
345 1 : FAILIF (2 != ctx.cnt);
346 :
347 : /* the start of the range is inclusive, the end is exclusive */
348 1 : memset (&ctx,
349 : 0,
350 : sizeof (ctx));
351 1 : ctx.name = "sar-filed";
352 1 : ctx.cnt = UINT64_MAX;
353 1 : FAILIF (0 >=
354 : TALER_EXCHANGEDB_iterate_aml_statistics (pg,
355 : 1,
356 : sar,
357 : ts (T2),
358 : ts (T3),
359 : &statistics_cb,
360 : &ctx));
361 1 : FAILIF (1 != ctx.total);
362 1 : FAILIF (1 != ctx.cnt);
363 :
364 1 : memset (&ctx,
365 : 0,
366 : sizeof (ctx));
367 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
368 : TALER_EXCHANGEDB_iterate_aml_statistics (pg,
369 : 1,
370 : sar,
371 : ts (T1),
372 : ts (T1),
373 : &statistics_cb,
374 : &ctx));
375 1 : FAILIF (0 != ctx.total);
376 :
377 : /* a range after everything that happened is empty */
378 1 : memset (&ctx,
379 : 0,
380 : sizeof (ctx));
381 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
382 : TALER_EXCHANGEDB_iterate_aml_statistics (pg,
383 : 3,
384 : all,
385 : ts (T3 + 2),
386 : ts (T3 + 100),
387 : &statistics_cb,
388 : &ctx));
389 1 : FAILIF (0 != ctx.total);
390 :
391 : /* and so is a name nobody ever recorded */
392 1 : memset (&ctx,
393 : 0,
394 : sizeof (ctx));
395 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
396 : TALER_EXCHANGEDB_iterate_aml_statistics (pg,
397 : 1,
398 : unknown,
399 : ts (0),
400 : ts (T3 + 2),
401 : &statistics_cb,
402 : &ctx));
403 1 : FAILIF (0 != ctx.total);
404 1 : return 0;
405 : }
406 :
407 :
408 : /**
409 : * The checks to run, in order.
410 : */
411 : static const struct TDB_Test tests[] = {
412 : { "kyc-events-empty",
413 : &check_empty },
414 : { "kyc-events-record",
415 : &check_record },
416 : { "kyc-events-statistics",
417 : &check_statistics },
418 : { NULL, NULL }
419 : };
420 :
421 :
422 : int
423 1 : main (int argc,
424 : char *const *argv)
425 : {
426 : int ret;
427 :
428 1 : ret = TDB_main (argc,
429 : argv,
430 : "test-kyc-events",
431 : "Tests for the exchangedb `kyc_events' table",
432 : tests);
433 1 : TDB_account_free (&account);
434 1 : return ret;
435 : }
436 :
437 :
438 : /* end of test_kyc_events.c */
|