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_reserves_close.c
18 : * @brief tests for the exchangedb functions whose primary table is
19 : * `reserves_close`
20 : * @author Christian Grothoff
21 : *
22 : * Covers #TALER_EXCHANGEDB_insert_reserve_closed(),
23 : * #TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id() and
24 : * #TALER_EXCHANGEDB_iterate_reserve_close_info().
25 : *
26 : * The table references `reserves` and `wire_targets`, both of which the
27 : * checks create with TDB_reserve() and TDB_account(). The KYC-facing
28 : * iterator groups by the *normalized* account, so the checks close two
29 : * reserves to the same account and one to another.
30 : */
31 : #include "test_common.h"
32 : #include "exchange-database/insert_reserve_closed.h"
33 : #include "exchange-database/iterate_reserve_closed_above_serial_id.h"
34 : #include "exchange-database/iterate_reserve_close_info.h"
35 :
36 :
37 : /**
38 : * Build a timestamp from a number of seconds since the epoch.
39 : *
40 : * @param secs seconds since the epoch
41 : * @return the timestamp
42 : */
43 : static struct GNUNET_TIME_Timestamp
44 6 : ts (uint64_t secs)
45 : {
46 6 : struct GNUNET_TIME_Absolute abs = {
47 6 : .abs_value_us = secs * 1000LLU * 1000LLU
48 : };
49 :
50 6 : return GNUNET_TIME_absolute_to_timestamp (abs);
51 : }
52 :
53 :
54 : /**
55 : * Closure for #closed_cb().
56 : */
57 : struct ClosedContext
58 : {
59 : /**
60 : * How many rows did the callback see?
61 : */
62 : unsigned int total;
63 :
64 : /**
65 : * Stop after this many rows; 0 for no limit.
66 : */
67 : unsigned int stop_after;
68 :
69 : /**
70 : * Reserve we are looking for, NULL to match nothing.
71 : */
72 : const struct TALER_ReservePublicKeyP *reserve_pub;
73 :
74 : /**
75 : * How many times did we see @e reserve_pub?
76 : */
77 : unsigned int matched;
78 :
79 : /**
80 : * Amount reported for @e reserve_pub.
81 : */
82 : struct TALER_Amount amount;
83 :
84 : /**
85 : * Closing fee reported for @e reserve_pub.
86 : */
87 : struct TALER_Amount closing_fee;
88 :
89 : /**
90 : * Wire transfer identifier reported for @e reserve_pub.
91 : */
92 : struct TALER_WireTransferIdentifierRawP wtid;
93 :
94 : /**
95 : * Close request row reported for @e reserve_pub.
96 : */
97 : uint64_t close_request_row;
98 :
99 : /**
100 : * Receiver account reported for @e reserve_pub, owned by this struct.
101 : */
102 : char *receiver;
103 : };
104 :
105 :
106 : /**
107 : * Callback for #TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id().
108 : *
109 : * @param cls a `struct ClosedContext *`
110 : * @param rowid row of the close operation
111 : * @param execution_date when the transfer was made
112 : * @param amount_with_fee how much was debited
113 : * @param closing_fee how much was charged for closing
114 : * @param reserve_pub the reserve that was closed
115 : * @param receiver_account where the money went
116 : * @param wtid identifier of the wire transfer
117 : * @param close_request_row close request that triggered this, 0 if none
118 : * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
119 : */
120 : static enum GNUNET_GenericReturnValue
121 6 : closed_cb (void *cls,
122 : uint64_t rowid,
123 : struct GNUNET_TIME_Timestamp execution_date,
124 : const struct TALER_Amount *amount_with_fee,
125 : const struct TALER_Amount *closing_fee,
126 : const struct TALER_ReservePublicKeyP *reserve_pub,
127 : const struct TALER_FullPayto receiver_account,
128 : const struct TALER_WireTransferIdentifierRawP *wtid,
129 : uint64_t close_request_row)
130 : {
131 6 : struct ClosedContext *ctx = cls;
132 :
133 : (void) rowid;
134 : (void) execution_date;
135 6 : ctx->total++;
136 6 : if ( (NULL != ctx->reserve_pub) &&
137 1 : (0 == GNUNET_memcmp (reserve_pub,
138 : ctx->reserve_pub)) )
139 : {
140 1 : ctx->matched++;
141 1 : ctx->amount = *amount_with_fee;
142 1 : ctx->closing_fee = *closing_fee;
143 1 : ctx->wtid = *wtid;
144 1 : ctx->close_request_row = close_request_row;
145 1 : GNUNET_free (ctx->receiver);
146 1 : ctx->receiver = GNUNET_strdup (receiver_account.full_payto);
147 : }
148 6 : if ( (0 != ctx->stop_after) &&
149 2 : (ctx->total >= ctx->stop_after) )
150 1 : return GNUNET_SYSERR;
151 5 : return GNUNET_OK;
152 : }
153 :
154 :
155 : /**
156 : * Closure for #amount_cb().
157 : */
158 : struct AmountContext
159 : {
160 : /**
161 : * How many amounts did the callback see?
162 : */
163 : unsigned int total;
164 :
165 : /**
166 : * Sum of the whole-unit parts of the amounts seen.
167 : */
168 : uint64_t value_sum;
169 :
170 : /**
171 : * Date of the first amount seen.
172 : */
173 : struct GNUNET_TIME_Absolute first_date;
174 :
175 : /**
176 : * Return this from the callback.
177 : */
178 : enum GNUNET_GenericReturnValue ret;
179 : };
180 :
181 :
182 : /**
183 : * Callback for #TALER_EXCHANGEDB_iterate_reserve_close_info().
184 : *
185 : * @param cls a `struct AmountContext *`
186 : * @param amount the closed amount
187 : * @param date when the reserve was closed
188 : * @return what @e ret of the closure says
189 : */
190 : static enum GNUNET_GenericReturnValue
191 5 : amount_cb (void *cls,
192 : const struct TALER_Amount *amount,
193 : struct GNUNET_TIME_Absolute date)
194 : {
195 5 : struct AmountContext *ctx = cls;
196 :
197 5 : if (0 == ctx->total++)
198 4 : ctx->first_date = date;
199 5 : ctx->value_sum += amount->value;
200 5 : return ctx->ret;
201 : }
202 :
203 :
204 : /**
205 : * Close a reserve.
206 : *
207 : * @param pg the database context
208 : * @param reserve_pub reserve to close
209 : * @param account where the money goes
210 : * @param seed seed for the wire transfer identifier
211 : * @param when when the transfer was made, in seconds since the epoch
212 : * @param amount amount debited from the reserve, e.g. "10"
213 : * @param fee closing fee, e.g. "0.5"
214 : * @param close_request_row close request that triggered this, 0 if none
215 : * @param[out] wtid set to the wire transfer identifier used
216 : * @return transaction status
217 : */
218 : static enum GNUNET_DB_QueryStatus
219 3 : close_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg,
220 : const struct TALER_ReservePublicKeyP *reserve_pub,
221 : const struct TDB_Account *account,
222 : uint32_t seed,
223 : uint64_t when,
224 : const char *amount,
225 : const char *fee,
226 : uint64_t close_request_row,
227 : struct TALER_WireTransferIdentifierRawP *wtid)
228 : {
229 3 : struct TALER_Amount a = TDB_amount (amount);
230 3 : struct TALER_Amount f = TDB_amount (fee);
231 :
232 3 : TDB_fill (wtid,
233 : sizeof (*wtid),
234 : seed);
235 3 : return TALER_EXCHANGEDB_insert_reserve_closed (pg,
236 : reserve_pub,
237 : ts (when),
238 : account->payto,
239 : wtid,
240 : &a,
241 : &f,
242 : close_request_row);
243 : }
244 :
245 :
246 : /**
247 : * Nothing is reported while the table is empty.
248 : *
249 : * @param pg the database context
250 : * @return 0 on success
251 : */
252 : static int
253 1 : check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
254 : {
255 1 : struct ClosedContext ctx = { 0 };
256 1 : struct AmountContext actx = { 0 };
257 : struct TALER_NormalizedPaytoHashP h_payto;
258 :
259 1 : TDB_FILL (h_payto,
260 : 1);
261 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
262 : TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id (
263 : pg,
264 : 0,
265 : &closed_cb,
266 : &ctx));
267 1 : actx.ret = GNUNET_OK;
268 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
269 : TALER_EXCHANGEDB_iterate_reserve_close_info (
270 : pg,
271 : &h_payto,
272 : GNUNET_TIME_UNIT_ZERO_ABS,
273 : &amount_cb,
274 : &actx));
275 1 : FAILIF (0 != ctx.total);
276 1 : FAILIF (0 != actx.total);
277 1 : return 0;
278 : }
279 :
280 :
281 : /**
282 : * A closed reserve is reported with everything it was closed with.
283 : *
284 : * @param pg the database context
285 : * @return 0 on success
286 : */
287 : static int
288 1 : check_insert_and_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
289 : {
290 : struct TDB_Account account;
291 : struct TALER_ReservePublicKeyP reserve_pub;
292 : struct TALER_WireTransferIdentifierRawP wtid;
293 1 : struct TALER_Amount expect = TDB_amount ("10");
294 1 : struct TALER_Amount expect_fee = TDB_amount ("0.5");
295 : struct ClosedContext ctx;
296 :
297 1 : TDB_account (pg,
298 : 10,
299 : &account);
300 : /* insert_reserve_closed() debits the reserve, so it has to hold exactly
301 : what is being closed */
302 1 : TDB_reserve (pg,
303 : 10,
304 : "10",
305 : &reserve_pub);
306 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
307 : close_reserve (pg,
308 : &reserve_pub,
309 : &account,
310 : 10,
311 : 1600000000,
312 : "10",
313 : "0.5",
314 : 0,
315 : &wtid),
316 : TDB_account_free (&account));
317 1 : memset (&ctx,
318 : 0,
319 : sizeof (ctx));
320 1 : ctx.reserve_pub = &reserve_pub;
321 1 : FAILIF_C (0 >=
322 : TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id (
323 : pg,
324 : 0,
325 : &closed_cb,
326 : &ctx),
327 : TDB_account_free (&account));
328 1 : FAILIF_C (1 != ctx.matched,
329 : GNUNET_free (ctx.receiver); TDB_account_free (&account));
330 1 : FAILIF_C (0 != TALER_amount_cmp (&ctx.amount,
331 : &expect),
332 : GNUNET_free (ctx.receiver); TDB_account_free (&account));
333 1 : FAILIF_C (0 != TALER_amount_cmp (&ctx.closing_fee,
334 : &expect_fee),
335 : GNUNET_free (ctx.receiver); TDB_account_free (&account));
336 1 : FAILIF_C (0 != GNUNET_memcmp (&ctx.wtid,
337 : &wtid),
338 : GNUNET_free (ctx.receiver); TDB_account_free (&account));
339 1 : FAILIF_C (0 != ctx.close_request_row,
340 : GNUNET_free (ctx.receiver); TDB_account_free (&account));
341 1 : FAILIF_C (0 != strcmp (ctx.receiver,
342 : account.payto.full_payto),
343 : GNUNET_free (ctx.receiver); TDB_account_free (&account));
344 1 : GNUNET_free (ctx.receiver);
345 1 : TDB_account_free (&account);
346 1 : return 0;
347 : }
348 :
349 :
350 : /**
351 : * The serial bound, the abort return of the callback and a second account
352 : * all behave as documented.
353 : *
354 : * @param pg the database context
355 : * @return 0 on success
356 : */
357 : static int
358 1 : check_iterate_filters (struct TALER_EXCHANGEDB_PostgresContext *pg)
359 : {
360 : struct TDB_Account account;
361 : struct TDB_Account other;
362 : struct TALER_ReservePublicKeyP r2;
363 : struct TALER_ReservePublicKeyP r3;
364 : struct TALER_WireTransferIdentifierRawP wtid;
365 : struct ClosedContext ctx;
366 :
367 1 : TDB_account (pg,
368 : 10,
369 : &account);
370 1 : TDB_account (pg,
371 : 11,
372 : &other);
373 1 : TDB_reserve (pg,
374 : 11,
375 : "4",
376 : &r2);
377 1 : TDB_reserve (pg,
378 : 12,
379 : "9",
380 : &r3);
381 : /* a second close to the same account, later */
382 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
383 : close_reserve (pg,
384 : &r2,
385 : &account,
386 : 11,
387 : 1600003600,
388 : "4",
389 : "0.5",
390 : 0,
391 : &wtid),
392 : TDB_account_free (&account); TDB_account_free (&other));
393 : /* and one to a different account */
394 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
395 : close_reserve (pg,
396 : &r3,
397 : &other,
398 : 12,
399 : 1600007200,
400 : "9",
401 : "0.5",
402 : 0,
403 : &wtid),
404 : TDB_account_free (&account); TDB_account_free (&other));
405 1 : FAILIF_C (3 != TDB_count (pg,
406 : "FROM reserves_close"),
407 : TDB_account_free (&account); TDB_account_free (&other));
408 :
409 1 : memset (&ctx,
410 : 0,
411 : sizeof (ctx));
412 1 : FAILIF_C (3 !=
413 : TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id (
414 : pg,
415 : 0,
416 : &closed_cb,
417 : &ctx),
418 : TDB_account_free (&account); TDB_account_free (&other));
419 1 : memset (&ctx,
420 : 0,
421 : sizeof (ctx));
422 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
423 : TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id (
424 : pg,
425 : 1000,
426 : &closed_cb,
427 : &ctx),
428 : TDB_account_free (&account); TDB_account_free (&other));
429 1 : FAILIF_C (0 != ctx.total,
430 : TDB_account_free (&account); TDB_account_free (&other));
431 :
432 : /* an aborting callback stops the iteration */
433 1 : memset (&ctx,
434 : 0,
435 : sizeof (ctx));
436 1 : ctx.stop_after = 2;
437 1 : FAILIF_C (3 !=
438 : TALER_EXCHANGEDB_iterate_reserve_closed_above_serial_id (
439 : pg,
440 : 0,
441 : &closed_cb,
442 : &ctx),
443 : TDB_account_free (&account); TDB_account_free (&other));
444 1 : FAILIF_C (2 != ctx.total,
445 : TDB_account_free (&account); TDB_account_free (&other));
446 1 : TDB_account_free (&account);
447 1 : TDB_account_free (&other);
448 1 : return 0;
449 : }
450 :
451 :
452 : /**
453 : * The KYC view sums the closures per account, newest first, and honours
454 : * the time limit and the callback's abort.
455 : *
456 : * @param pg the database context
457 : * @return 0 on success
458 : */
459 : static int
460 1 : check_close_info (struct TALER_EXCHANGEDB_PostgresContext *pg)
461 : {
462 : struct TDB_Account account;
463 : struct TDB_Account other;
464 : struct AmountContext ctx;
465 :
466 1 : TDB_account (pg,
467 : 10,
468 : &account);
469 1 : TDB_account (pg,
470 : 11,
471 : &other);
472 :
473 : /* both closures to the first account */
474 1 : memset (&ctx,
475 : 0,
476 : sizeof (ctx));
477 1 : ctx.ret = GNUNET_OK;
478 1 : FAILIF_C (2 !=
479 : TALER_EXCHANGEDB_iterate_reserve_close_info (
480 : pg,
481 : &account.h_normalized,
482 : GNUNET_TIME_UNIT_ZERO_ABS,
483 : &amount_cb,
484 : &ctx),
485 : TDB_account_free (&account); TDB_account_free (&other));
486 1 : FAILIF_C (10 + 4 != ctx.value_sum,
487 : TDB_account_free (&account); TDB_account_free (&other));
488 : /* newest first */
489 1 : FAILIF_C (ctx.first_date.abs_value_us !=
490 : ts (1600003600).abs_time.abs_value_us,
491 : TDB_account_free (&account); TDB_account_free (&other));
492 :
493 : /* the other account has just the one */
494 1 : memset (&ctx,
495 : 0,
496 : sizeof (ctx));
497 1 : ctx.ret = GNUNET_OK;
498 1 : FAILIF_C (1 !=
499 : TALER_EXCHANGEDB_iterate_reserve_close_info (
500 : pg,
501 : &other.h_normalized,
502 : GNUNET_TIME_UNIT_ZERO_ABS,
503 : &amount_cb,
504 : &ctx),
505 : TDB_account_free (&account); TDB_account_free (&other));
506 1 : FAILIF_C (9 != ctx.value_sum,
507 : TDB_account_free (&account); TDB_account_free (&other));
508 :
509 : /* the time limit drops the older closure */
510 1 : memset (&ctx,
511 : 0,
512 : sizeof (ctx));
513 1 : ctx.ret = GNUNET_OK;
514 1 : FAILIF_C (1 !=
515 : TALER_EXCHANGEDB_iterate_reserve_close_info (
516 : pg,
517 : &account.h_normalized,
518 : ts (1600001800).abs_time,
519 : &amount_cb,
520 : &ctx),
521 : TDB_account_free (&account); TDB_account_free (&other));
522 1 : FAILIF_C (4 != ctx.value_sum,
523 : TDB_account_free (&account); TDB_account_free (&other));
524 :
525 : /* a limit past every closure leaves nothing */
526 1 : memset (&ctx,
527 : 0,
528 : sizeof (ctx));
529 1 : ctx.ret = GNUNET_OK;
530 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
531 : TALER_EXCHANGEDB_iterate_reserve_close_info (
532 : pg,
533 : &account.h_normalized,
534 : ts (1700000000).abs_time,
535 : &amount_cb,
536 : &ctx),
537 : TDB_account_free (&account); TDB_account_free (&other));
538 1 : FAILIF_C (0 != ctx.total,
539 : TDB_account_free (&account); TDB_account_free (&other));
540 :
541 : /* a callback that aborts with GNUNET_NO is not an error */
542 1 : memset (&ctx,
543 : 0,
544 : sizeof (ctx));
545 1 : ctx.ret = GNUNET_NO;
546 1 : FAILIF_C (0 >
547 : TALER_EXCHANGEDB_iterate_reserve_close_info (
548 : pg,
549 : &account.h_normalized,
550 : GNUNET_TIME_UNIT_ZERO_ABS,
551 : &amount_cb,
552 : &ctx),
553 : TDB_account_free (&account); TDB_account_free (&other));
554 1 : FAILIF_C (1 != ctx.total,
555 : TDB_account_free (&account); TDB_account_free (&other));
556 1 : TDB_account_free (&account);
557 1 : TDB_account_free (&other);
558 1 : return 0;
559 : }
560 :
561 :
562 : /**
563 : * The checks to run, in order.
564 : */
565 : static const struct TDB_Test tests[] = {
566 : { "reserves-close-empty",
567 : &check_empty },
568 : { "reserves-close-insert-and-iterate",
569 : &check_insert_and_iterate },
570 : { "reserves-close-iterate-filters",
571 : &check_iterate_filters },
572 : { "reserves-close-close-info",
573 : &check_close_info },
574 : { NULL, NULL }
575 : };
576 :
577 :
578 : int
579 1 : main (int argc,
580 : char *const *argv)
581 : {
582 1 : return TDB_main (argc,
583 : argv,
584 : "test-reserves-close",
585 : "Tests for the exchangedb `reserves_close' table",
586 : tests);
587 : }
588 :
589 :
590 : /* end of test_reserves_close.c */
|