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_misc.c
18 : * @brief tests for the exchangedb functions that have no table of their own
19 : * @author Christian Grothoff
20 : *
21 : * Covers #TALER_EXCHANGEDB_start(), #TALER_EXCHANGEDB_start_read_only(),
22 : * #TALER_EXCHANGEDB_start_read_committed(),
23 : * #TALER_EXCHANGEDB_start_deferred_wire_out(), #TALER_EXCHANGEDB_commit(),
24 : * #TALER_EXCHANGEDB_rollback(), #TALER_EXCHANGEDB_preflight(),
25 : * #TALER_EXCHANGEDB_compute_shard(),
26 : * #TALER_EXCHANGEDB_calculate_transaction_list_totals(),
27 : * #TALER_EXCHANGEDB_free_coin_transaction_list(),
28 : * #TALER_EXCHANGEDB_free_reserve_history(),
29 : * #TALER_EXCHANGEDB_event_listen(),
30 : * #TALER_EXCHANGEDB_event_listen_cancel(),
31 : * #TALER_EXCHANGEDB_event_notify(), #TALER_EXCHANGEDB_enable_rules(),
32 : * #TALER_EXCHANGEDB_disable_rules(),
33 : * #TALER_EXCHANGEDB_inject_auditor_triggers(),
34 : * #TALER_EXCHANGEDB_drop_tables() and #TALER_EXCHANGEDB_create_tables().
35 : *
36 : * These are the connection-level functions: transactions, notifications,
37 : * schema management and the two list destructors. The schema checks come
38 : * last on purpose -- they take the tables away and put them back, so
39 : * nothing may run after them.
40 : */
41 : #include "test_common.h"
42 : #include "taler/taler_dbevents.h"
43 : #include "exchange-database/commit.h"
44 : #include "exchange-database/compute_shard.h"
45 : #include "exchange-database/create_tables.h"
46 : #include "exchange-database/disable_rules.h"
47 : #include "exchange-database/drop_tables.h"
48 : #include "exchange-database/enable_rules.h"
49 : #include "exchange-database/event_listen.h"
50 : #include "exchange-database/event_listen_cancel.h"
51 : #include "exchange-database/event_notify.h"
52 : #include "exchange-database/free_coin_transaction_list.h"
53 : #include "exchange-database/free_reserve_history.h"
54 : #include "exchange-database/get_coin_transactions.h"
55 : #include "exchange-database/get_reserve_history.h"
56 : #include "exchange-database/inject_auditor_triggers.h"
57 : #include "exchange-database/preflight.h"
58 : #include "exchange-database/rollback.h"
59 : #include "exchange-database/start.h"
60 : #include "exchange-database/start_deferred_wire_out.h"
61 : #include "exchange-database/start_read_committed.h"
62 : #include "exchange-database/start_read_only.h"
63 :
64 :
65 : /**
66 : * Account the checks fund their reserves from.
67 : */
68 : static struct TDB_Account account;
69 :
70 :
71 : /**
72 : * Denomination the checks use.
73 : */
74 : static struct TDB_Denom denom;
75 :
76 :
77 : /**
78 : * The four ways to start a transaction.
79 : *
80 : * @param pg the database context
81 : * @param name name of the transaction
82 : * @return #GNUNET_OK on success
83 : */
84 : static enum GNUNET_GenericReturnValue
85 2 : start_serializable (struct TALER_EXCHANGEDB_PostgresContext *pg,
86 : const char *name)
87 : {
88 2 : return TALER_EXCHANGEDB_start (pg,
89 : name);
90 : }
91 :
92 :
93 : /**
94 : * Start a read-only transaction.
95 : *
96 : * @param pg the database context
97 : * @param name name of the transaction
98 : * @return #GNUNET_OK on success
99 : */
100 : static enum GNUNET_GenericReturnValue
101 2 : start_read_only (struct TALER_EXCHANGEDB_PostgresContext *pg,
102 : const char *name)
103 : {
104 2 : return TALER_EXCHANGEDB_start_read_only (pg,
105 : name);
106 : }
107 :
108 :
109 : /**
110 : * Start a read-committed transaction.
111 : *
112 : * @param pg the database context
113 : * @param name name of the transaction
114 : * @return #GNUNET_OK on success
115 : */
116 : static enum GNUNET_GenericReturnValue
117 2 : start_read_committed (struct TALER_EXCHANGEDB_PostgresContext *pg,
118 : const char *name)
119 : {
120 2 : return TALER_EXCHANGEDB_start_read_committed (pg,
121 : name);
122 : }
123 :
124 :
125 : /**
126 : * Start a transaction with deferred wire-out constraints.
127 : *
128 : * @param pg the database context
129 : * @param name name of the transaction
130 : * @return #GNUNET_OK on success
131 : */
132 : static enum GNUNET_GenericReturnValue
133 2 : start_deferred (struct TALER_EXCHANGEDB_PostgresContext *pg,
134 : const char *name)
135 : {
136 : (void) name;
137 2 : return TALER_EXCHANGEDB_start_deferred_wire_out (pg);
138 : }
139 :
140 :
141 : /**
142 : * All four transaction starters behave the same way as far as the
143 : * connection's bookkeeping is concerned.
144 : *
145 : * @param pg the database context
146 : * @return 0 on success
147 : */
148 : static int
149 1 : check_transactions (struct TALER_EXCHANGEDB_PostgresContext *pg)
150 : {
151 : static enum GNUNET_GenericReturnValue (*const starters[])(
152 : struct TALER_EXCHANGEDB_PostgresContext *,
153 : const char *) = {
154 : &start_serializable,
155 : &start_read_only,
156 : &start_read_committed,
157 : &start_deferred
158 : };
159 :
160 1 : FAILIF (NULL != pg->transaction_name);
161 5 : for (unsigned int i = 0; i < 4; i++)
162 : {
163 : /* commit ends the transaction ... */
164 4 : FAILIF (GNUNET_OK !=
165 : starters[i](pg,
166 : "misc-commit"));
167 4 : FAILIF (NULL == pg->transaction_name);
168 4 : FAILIF (0 > TALER_EXCHANGEDB_commit (pg));
169 4 : FAILIF (NULL != pg->transaction_name);
170 :
171 : /* ... and so does a rollback */
172 4 : FAILIF (GNUNET_OK !=
173 : starters[i](pg,
174 : "misc-rollback"));
175 4 : FAILIF (NULL == pg->transaction_name);
176 4 : TALER_EXCHANGEDB_rollback (pg);
177 4 : FAILIF (NULL != pg->transaction_name);
178 : }
179 :
180 : /* rolling back when nothing is open is a no-op */
181 1 : TALER_EXCHANGEDB_rollback (pg);
182 1 : FAILIF (NULL != pg->transaction_name);
183 1 : return 0;
184 : }
185 :
186 :
187 : /**
188 : * A transaction really isolates: what it wrote is gone after a rollback
189 : * and there after a commit.
190 : *
191 : * @param pg the database context
192 : * @return 0 on success
193 : */
194 : static int
195 1 : check_isolation (struct TALER_EXCHANGEDB_PostgresContext *pg)
196 : {
197 1 : FAILIF (GNUNET_OK !=
198 : TALER_EXCHANGEDB_start (pg,
199 : "misc-isolation"));
200 1 : TDB_account (pg,
201 : 10,
202 : &account);
203 1 : FAILIF (1 != TDB_count (pg,
204 : "FROM wire_targets"));
205 1 : TALER_EXCHANGEDB_rollback (pg);
206 1 : FAILIF (0 != TDB_count (pg,
207 : "FROM wire_targets"));
208 1 : TDB_account_free (&account);
209 :
210 1 : FAILIF (GNUNET_OK !=
211 : TALER_EXCHANGEDB_start (pg,
212 : "misc-isolation"));
213 1 : TDB_account (pg,
214 : 10,
215 : &account);
216 1 : FAILIF (0 > TALER_EXCHANGEDB_commit (pg));
217 1 : FAILIF (1 != TDB_count (pg,
218 : "FROM wire_targets"));
219 1 : return 0;
220 : }
221 :
222 :
223 : /**
224 : * Preflight is happy on an idle connection and cleans up after a caller
225 : * that forgot to end its transaction.
226 : *
227 : * @param pg the database context
228 : * @return 0 on success
229 : */
230 : static int
231 1 : check_preflight (struct TALER_EXCHANGEDB_PostgresContext *pg)
232 : {
233 1 : FAILIF (GNUNET_OK !=
234 : TALER_EXCHANGEDB_preflight (pg));
235 :
236 : /* a forgotten transaction is rolled back, and preflight says so */
237 1 : FAILIF (GNUNET_OK !=
238 : TALER_EXCHANGEDB_start (pg,
239 : "misc-leak"));
240 1 : FAILIF (GNUNET_NO !=
241 : TALER_EXCHANGEDB_preflight (pg));
242 1 : FAILIF (NULL != pg->transaction_name);
243 1 : FAILIF (GNUNET_OK !=
244 : TALER_EXCHANGEDB_preflight (pg));
245 :
246 : /* which is also what start() does for us */
247 1 : FAILIF (GNUNET_OK !=
248 : TALER_EXCHANGEDB_start (pg,
249 : "misc-leak"));
250 1 : FAILIF (GNUNET_OK !=
251 : TALER_EXCHANGEDB_start (pg,
252 : "misc-after-leak"));
253 1 : TALER_EXCHANGEDB_rollback (pg);
254 1 : return 0;
255 : }
256 :
257 :
258 : /**
259 : * The shard of a merchant key is deterministic and stays in range.
260 : *
261 : * @param pg the database context
262 : * @return 0 on success
263 : */
264 : static int
265 1 : check_compute_shard (struct TALER_EXCHANGEDB_PostgresContext *pg)
266 : {
267 : struct TALER_MerchantPublicKeyP m1;
268 : struct TALER_MerchantPublicKeyP m2;
269 : uint64_t s1;
270 : uint64_t s2;
271 :
272 : (void) pg;
273 1 : TDB_FILL (m1,
274 : 1);
275 1 : TDB_FILL (m2,
276 : 2);
277 1 : s1 = TALER_EXCHANGEDB_compute_shard (&m1);
278 1 : s2 = TALER_EXCHANGEDB_compute_shard (&m2);
279 1 : FAILIF (s1 > INT32_MAX);
280 1 : FAILIF (s2 > INT32_MAX);
281 1 : FAILIF (s1 == s2);
282 : /* the same key always maps to the same shard, otherwise the shard
283 : locks of the aggregator would not partition the work */
284 1 : FAILIF (s1 != TALER_EXCHANGEDB_compute_shard (&m1));
285 1 : FAILIF (s2 != TALER_EXCHANGEDB_compute_shard (&m2));
286 1 : return 0;
287 : }
288 :
289 :
290 : /**
291 : * The coin transaction list adds up and can be released.
292 : *
293 : * @param pg the database context
294 : * @return 0 on success
295 : */
296 : static int
297 1 : check_coin_list (struct TALER_EXCHANGEDB_PostgresContext *pg)
298 : {
299 1 : struct TALER_EXCHANGEDB_TransactionList *tl = NULL;
300 : struct TALER_CoinPublicInfo coin;
301 : struct TDB_Deposit dep;
302 : struct TALER_Amount balance;
303 : struct TALER_Amount total;
304 1 : struct TALER_Amount zero = TDB_amount ("0");
305 1 : struct TALER_Amount one = TDB_amount ("1");
306 1 : struct TALER_Amount two = TDB_amount ("2");
307 : struct TALER_DenominationHashP h_denom_pub;
308 1 : uint64_t etag = 0;
309 :
310 : /* an empty list is worth the offset it started from */
311 1 : FAILIF (GNUNET_OK !=
312 : TALER_EXCHANGEDB_calculate_transaction_list_totals (NULL,
313 : &one,
314 : &total));
315 1 : FAILIF (0 != TALER_amount_cmp (&total,
316 : &one));
317 : /* and freeing it is a no-op */
318 1 : TALER_EXCHANGEDB_free_coin_transaction_list (NULL);
319 :
320 1 : TDB_denom (pg,
321 : 10,
322 : "5",
323 : "0.1",
324 : &denom);
325 1 : TDB_coin (pg,
326 : &denom,
327 : 20,
328 : &coin,
329 : NULL);
330 1 : TDB_deposit (pg,
331 : &account,
332 : &coin,
333 : 30,
334 : "1",
335 : "0.1",
336 : GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS),
337 : GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS),
338 : &dep);
339 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
340 : TALER_EXCHANGEDB_get_coin_transactions (pg,
341 : true,
342 : &coin.coin_pub,
343 : 0,
344 : 0,
345 : &etag,
346 : &balance,
347 : &h_denom_pub,
348 : &tl),
349 : TDB_coin_free (&coin));
350 1 : TDB_coin_free (&coin);
351 1 : FAILIF (NULL == tl);
352 1 : FAILIF_C (GNUNET_OK !=
353 : TALER_EXCHANGEDB_calculate_transaction_list_totals (tl,
354 : &zero,
355 : &total),
356 : TALER_EXCHANGEDB_free_coin_transaction_list (tl));
357 1 : FAILIF_C (0 != TALER_amount_cmp (&total,
358 : &one),
359 : TALER_EXCHANGEDB_free_coin_transaction_list (tl));
360 : /* the offset is added on top */
361 1 : FAILIF_C (GNUNET_OK !=
362 : TALER_EXCHANGEDB_calculate_transaction_list_totals (tl,
363 : &one,
364 : &total),
365 : TALER_EXCHANGEDB_free_coin_transaction_list (tl));
366 1 : FAILIF_C (0 != TALER_amount_cmp (&total,
367 : &two),
368 : TALER_EXCHANGEDB_free_coin_transaction_list (tl));
369 1 : TALER_EXCHANGEDB_free_coin_transaction_list (tl);
370 1 : return 0;
371 : }
372 :
373 :
374 : /**
375 : * The reserve history can be released.
376 : *
377 : * @param pg the database context
378 : * @return 0 on success
379 : */
380 : static int
381 1 : check_reserve_history (struct TALER_EXCHANGEDB_PostgresContext *pg)
382 : {
383 1 : struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
384 : struct TALER_ReservePublicKeyP reserve_pub;
385 : struct TALER_Amount balance;
386 1 : uint64_t etag = 0;
387 1 : unsigned int cnt = 0;
388 :
389 : /* freeing nothing is a no-op */
390 1 : TALER_EXCHANGEDB_free_reserve_history (NULL);
391 :
392 1 : TDB_reserve_in (pg,
393 : &account,
394 : 11,
395 : "10",
396 : &reserve_pub);
397 1 : (void) TDB_withdraw (pg,
398 : &denom,
399 : &reserve_pub,
400 : 11,
401 : "5");
402 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
403 : TALER_EXCHANGEDB_get_reserve_history (pg,
404 : &reserve_pub,
405 : 0,
406 : 0,
407 : &etag,
408 : &balance,
409 : &rh));
410 1 : FAILIF (NULL == rh);
411 1 : for (const struct TALER_EXCHANGEDB_ReserveHistory *pos = rh;
412 3 : NULL != pos;
413 2 : pos = pos->next)
414 2 : cnt++;
415 1 : FAILIF_C (2 != cnt,
416 : TALER_EXCHANGEDB_free_reserve_history (rh));
417 1 : TALER_EXCHANGEDB_free_reserve_history (rh);
418 1 : return 0;
419 : }
420 :
421 :
422 : /**
423 : * State of the event check.
424 : */
425 : struct EventState
426 : {
427 : /**
428 : * How often did the callback fire with a payload?
429 : */
430 : unsigned int fired;
431 :
432 : /**
433 : * Payload of the last notification.
434 : */
435 : char payload[16];
436 :
437 : /**
438 : * Number of bytes in @e payload.
439 : */
440 : size_t payload_len;
441 : };
442 :
443 :
444 : /**
445 : * Payload of the notification.
446 : */
447 : #define PAYLOAD "hello"
448 :
449 :
450 : /**
451 : * Called when the notification arrives (or the listener times out).
452 : *
453 : * @param cls a `struct EventState *`
454 : * @param extra the payload, NULL on timeout
455 : * @param extra_size number of bytes in @a extra
456 : */
457 : static void
458 1 : event_cb (void *cls,
459 : const void *extra,
460 : size_t extra_size)
461 : {
462 1 : struct EventState *st = cls;
463 :
464 1 : if (NULL == extra)
465 0 : return; /* timeout event, not what we are after */
466 1 : st->fired++;
467 1 : st->payload_len = GNUNET_MIN (extra_size,
468 : sizeof (st->payload));
469 1 : memcpy (st->payload,
470 : extra,
471 : st->payload_len);
472 : }
473 :
474 :
475 : /**
476 : * A notification sent on the connection reaches a listener registered on
477 : * it, and stops reaching it once the listener is cancelled.
478 : *
479 : * @param pg the database context
480 : * @return 0 on success
481 : */
482 : static int
483 1 : check_events (struct TALER_EXCHANGEDB_PostgresContext *pg)
484 : {
485 1 : struct GNUNET_DB_EventHeaderP es = {
486 1 : .size = htons (sizeof (es)),
487 1 : .type = htons (TALER_DBEVENT_EXCHANGE_KYC_COMPLETED)
488 : };
489 : struct GNUNET_DB_EventHandler *eh;
490 1 : struct EventState st = { 0 };
491 :
492 1 : eh = TALER_EXCHANGEDB_event_listen (pg,
493 : GNUNET_TIME_UNIT_MINUTES,
494 : &es,
495 : &event_cb,
496 : &st);
497 1 : FAILIF (NULL == eh);
498 1 : TALER_EXCHANGEDB_event_notify (pg,
499 : &es,
500 : PAYLOAD,
501 : strlen (PAYLOAD));
502 : /* the scheduler task that would pick this up does not get to run while
503 : the checks are executing, so poll for it ourselves -- which is also
504 : what the exchange does after every notifying transaction */
505 1 : GNUNET_PQ_event_do_poll (pg->conn);
506 1 : FAILIF_C (1 != st.fired,
507 : TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
508 1 : FAILIF_C (strlen (PAYLOAD) != st.payload_len,
509 : TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
510 1 : FAILIF_C (0 != memcmp (st.payload,
511 : PAYLOAD,
512 : st.payload_len),
513 : TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
514 :
515 : /* an unrelated event does not wake us */
516 : {
517 1 : struct GNUNET_DB_EventHeaderP other = {
518 1 : .size = htons (sizeof (other)),
519 1 : .type = htons (TALER_DBEVENT_EXCHANGE_NEW_KYC_ATTRIBUTES)
520 : };
521 :
522 1 : TALER_EXCHANGEDB_event_notify (pg,
523 : &other,
524 : PAYLOAD,
525 : strlen (PAYLOAD));
526 1 : GNUNET_PQ_event_do_poll (pg->conn);
527 1 : FAILIF_C (1 != st.fired,
528 : TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
529 : }
530 :
531 : /* and after cancelling, neither does the one we listened for */
532 1 : TALER_EXCHANGEDB_event_listen_cancel (pg,
533 : eh);
534 1 : TALER_EXCHANGEDB_event_notify (pg,
535 : &es,
536 : PAYLOAD,
537 : strlen (PAYLOAD));
538 1 : GNUNET_PQ_event_do_poll (pg->conn);
539 1 : FAILIF (1 != st.fired);
540 1 : return 0;
541 : }
542 :
543 :
544 : /**
545 : * Customization rules that were never deployed cannot be dropped.
546 : *
547 : * @param pg the database context
548 : * @return 0 on success
549 : */
550 : static int
551 1 : check_rules (struct TALER_EXCHANGEDB_PostgresContext *pg)
552 : {
553 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
554 : TALER_EXCHANGEDB_disable_rules (pg,
555 : "no-such-customization"));
556 : /* and a schema without SQL files on disk cannot be deployed; the
557 : search_path must be back to `exchange' afterwards either way */
558 1 : FAILIF (GNUNET_SYSERR ==
559 : TALER_EXCHANGEDB_enable_rules (pg,
560 : "no-such-customization"));
561 : /* the search_path is back to `exchange', so an unqualified query
562 : still finds the exchange's own tables */
563 1 : FAILIF (1 != TDB_count (pg,
564 : "FROM wire_targets"));
565 1 : return 0;
566 : }
567 :
568 :
569 : /**
570 : * The auditor triggers can be injected into an existing schema.
571 : *
572 : * @param pg the database context
573 : * @return 0 on success
574 : */
575 : static int
576 1 : check_auditor_triggers (struct TALER_EXCHANGEDB_PostgresContext *pg)
577 : {
578 1 : FAILIF (GNUNET_OK !=
579 : TALER_EXCHANGEDB_inject_auditor_triggers (pg));
580 1 : return 0;
581 : }
582 :
583 :
584 : /**
585 : * Dropping and re-creating the schema leaves an empty but usable
586 : * database. Nothing may run after this check.
587 : *
588 : * @param pg the database context
589 : * @return 0 on success
590 : */
591 : static int
592 1 : check_schema (struct TALER_EXCHANGEDB_PostgresContext *pg)
593 : {
594 1 : FAILIF (0 == TDB_count (pg,
595 : "FROM wire_targets"));
596 1 : FAILIF (GNUNET_OK !=
597 : TALER_EXCHANGEDB_drop_tables (pg));
598 : /* dropping twice fails: drop.sql says `DROP SCHEMA exchange CASCADE'
599 : without IF EXISTS, so there is nothing left to drop the second time */
600 1 : FAILIF (GNUNET_OK ==
601 : TALER_EXCHANGEDB_drop_tables (pg));
602 1 : FAILIF (GNUNET_OK !=
603 : TALER_EXCHANGEDB_create_tables (pg,
604 : false,
605 : 0));
606 1 : FAILIF (0 != TDB_count (pg,
607 : "FROM wire_targets"));
608 1 : return 0;
609 : }
610 :
611 :
612 : /**
613 : * The checks to run, in order.
614 : */
615 : static const struct TDB_Test tests[] = {
616 : { "misc-transactions",
617 : &check_transactions },
618 : { "misc-isolation",
619 : &check_isolation },
620 : { "misc-preflight",
621 : &check_preflight },
622 : { "misc-compute-shard",
623 : &check_compute_shard },
624 : { "misc-coin-list",
625 : &check_coin_list },
626 : { "misc-reserve-history",
627 : &check_reserve_history },
628 : { "misc-events",
629 : &check_events },
630 : { "misc-rules",
631 : &check_rules },
632 : { "misc-auditor-triggers",
633 : &check_auditor_triggers },
634 : { "misc-schema",
635 : &check_schema },
636 : { NULL, NULL }
637 : };
638 :
639 :
640 : int
641 1 : main (int argc,
642 : char *const *argv)
643 : {
644 : int ret;
645 :
646 1 : ret = TDB_main (argc,
647 : argv,
648 : "test-misc",
649 : "Tests for the exchangedb functions without a table",
650 : tests);
651 1 : TDB_denom_free (&denom);
652 1 : TDB_account_free (&account);
653 1 : return ret;
654 : }
655 :
656 :
657 : /* end of test_misc.c */
|