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 :
209 1 : FAILIF (GNUNET_OK !=
210 : TALER_EXCHANGEDB_start (pg,
211 : "misc-isolation"));
212 1 : TDB_account (pg,
213 : 10,
214 : &account);
215 1 : FAILIF (0 > TALER_EXCHANGEDB_commit (pg));
216 1 : FAILIF (1 != TDB_count (pg,
217 : "FROM wire_targets"));
218 1 : return 0;
219 : }
220 :
221 :
222 : /**
223 : * Preflight is happy on an idle connection and cleans up after a caller
224 : * that forgot to end its transaction.
225 : *
226 : * @param pg the database context
227 : * @return 0 on success
228 : */
229 : static int
230 1 : check_preflight (struct TALER_EXCHANGEDB_PostgresContext *pg)
231 : {
232 1 : FAILIF (GNUNET_OK !=
233 : TALER_EXCHANGEDB_preflight (pg));
234 :
235 : /* a forgotten transaction is rolled back, and preflight says so */
236 1 : FAILIF (GNUNET_OK !=
237 : TALER_EXCHANGEDB_start (pg,
238 : "misc-leak"));
239 1 : FAILIF (GNUNET_NO !=
240 : TALER_EXCHANGEDB_preflight (pg));
241 1 : FAILIF (NULL != pg->transaction_name);
242 1 : FAILIF (GNUNET_OK !=
243 : TALER_EXCHANGEDB_preflight (pg));
244 :
245 : /* which is also what start() does for us */
246 1 : FAILIF (GNUNET_OK !=
247 : TALER_EXCHANGEDB_start (pg,
248 : "misc-leak"));
249 1 : FAILIF (GNUNET_OK !=
250 : TALER_EXCHANGEDB_start (pg,
251 : "misc-after-leak"));
252 1 : TALER_EXCHANGEDB_rollback (pg);
253 1 : return 0;
254 : }
255 :
256 :
257 : /**
258 : * The shard of a merchant key is deterministic and stays in range.
259 : *
260 : * @param pg the database context
261 : * @return 0 on success
262 : */
263 : static int
264 1 : check_compute_shard (struct TALER_EXCHANGEDB_PostgresContext *pg)
265 : {
266 : struct TALER_MerchantPublicKeyP m1;
267 : struct TALER_MerchantPublicKeyP m2;
268 : uint64_t s1;
269 : uint64_t s2;
270 :
271 : (void) pg;
272 1 : TDB_FILL (m1,
273 : 1);
274 1 : TDB_FILL (m2,
275 : 2);
276 1 : s1 = TALER_EXCHANGEDB_compute_shard (&m1);
277 1 : s2 = TALER_EXCHANGEDB_compute_shard (&m2);
278 1 : FAILIF (s1 > INT32_MAX);
279 1 : FAILIF (s2 > INT32_MAX);
280 1 : FAILIF (s1 == s2);
281 : /* the same key always maps to the same shard, otherwise the shard
282 : locks of the aggregator would not partition the work */
283 1 : FAILIF (s1 != TALER_EXCHANGEDB_compute_shard (&m1));
284 1 : FAILIF (s2 != TALER_EXCHANGEDB_compute_shard (&m2));
285 1 : return 0;
286 : }
287 :
288 :
289 : /**
290 : * The coin transaction list adds up and can be released.
291 : *
292 : * @param pg the database context
293 : * @return 0 on success
294 : */
295 : static int
296 1 : check_coin_list (struct TALER_EXCHANGEDB_PostgresContext *pg)
297 : {
298 1 : struct TALER_EXCHANGEDB_TransactionList *tl = NULL;
299 : struct TALER_CoinPublicInfo coin;
300 : struct TDB_Deposit dep;
301 : struct TALER_Amount balance;
302 : struct TALER_Amount total;
303 1 : struct TALER_Amount zero = TDB_amount ("0");
304 1 : struct TALER_Amount one = TDB_amount ("1");
305 1 : struct TALER_Amount two = TDB_amount ("2");
306 : struct TALER_DenominationHashP h_denom_pub;
307 1 : uint64_t etag = 0;
308 :
309 : /* an empty list is worth the offset it started from */
310 1 : FAILIF (GNUNET_OK !=
311 : TALER_EXCHANGEDB_calculate_transaction_list_totals (NULL,
312 : &one,
313 : &total));
314 1 : FAILIF (0 != TALER_amount_cmp (&total,
315 : &one));
316 : /* and freeing it is a no-op */
317 1 : TALER_EXCHANGEDB_free_coin_transaction_list (NULL);
318 :
319 1 : TDB_denom (pg,
320 : 10,
321 : "5",
322 : "0.1",
323 : &denom);
324 1 : TDB_coin (pg,
325 : &denom,
326 : 20,
327 : &coin,
328 : NULL);
329 1 : TDB_deposit (pg,
330 : &account,
331 : &coin,
332 : 30,
333 : "1",
334 : "0.1",
335 : GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS),
336 : GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS),
337 : &dep);
338 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
339 : TALER_EXCHANGEDB_get_coin_transactions (pg,
340 : true,
341 : &coin.coin_pub,
342 : 0,
343 : 0,
344 : &etag,
345 : &balance,
346 : &h_denom_pub,
347 : &tl),
348 : TDB_coin_free (&coin));
349 1 : TDB_coin_free (&coin);
350 1 : FAILIF (NULL == tl);
351 1 : FAILIF_C (GNUNET_OK !=
352 : TALER_EXCHANGEDB_calculate_transaction_list_totals (tl,
353 : &zero,
354 : &total),
355 : TALER_EXCHANGEDB_free_coin_transaction_list (tl));
356 1 : FAILIF_C (0 != TALER_amount_cmp (&total,
357 : &one),
358 : TALER_EXCHANGEDB_free_coin_transaction_list (tl));
359 : /* the offset is added on top */
360 1 : FAILIF_C (GNUNET_OK !=
361 : TALER_EXCHANGEDB_calculate_transaction_list_totals (tl,
362 : &one,
363 : &total),
364 : TALER_EXCHANGEDB_free_coin_transaction_list (tl));
365 1 : FAILIF_C (0 != TALER_amount_cmp (&total,
366 : &two),
367 : TALER_EXCHANGEDB_free_coin_transaction_list (tl));
368 1 : TALER_EXCHANGEDB_free_coin_transaction_list (tl);
369 1 : return 0;
370 : }
371 :
372 :
373 : /**
374 : * The reserve history can be released.
375 : *
376 : * @param pg the database context
377 : * @return 0 on success
378 : */
379 : static int
380 1 : check_reserve_history (struct TALER_EXCHANGEDB_PostgresContext *pg)
381 : {
382 1 : struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
383 : struct TALER_ReservePublicKeyP reserve_pub;
384 : struct TALER_Amount balance;
385 1 : uint64_t etag = 0;
386 1 : unsigned int cnt = 0;
387 :
388 : /* freeing nothing is a no-op */
389 1 : TALER_EXCHANGEDB_free_reserve_history (NULL);
390 :
391 1 : TDB_reserve_in (pg,
392 : &account,
393 : 11,
394 : "10",
395 : &reserve_pub);
396 1 : (void) TDB_withdraw (pg,
397 : &denom,
398 : &reserve_pub,
399 : 11,
400 : "5");
401 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
402 : TALER_EXCHANGEDB_get_reserve_history (pg,
403 : &reserve_pub,
404 : 0,
405 : 0,
406 : &etag,
407 : &balance,
408 : &rh));
409 1 : FAILIF (NULL == rh);
410 1 : for (const struct TALER_EXCHANGEDB_ReserveHistory *pos = rh;
411 3 : NULL != pos;
412 2 : pos = pos->next)
413 2 : cnt++;
414 1 : FAILIF_C (2 != cnt,
415 : TALER_EXCHANGEDB_free_reserve_history (rh));
416 1 : TALER_EXCHANGEDB_free_reserve_history (rh);
417 1 : return 0;
418 : }
419 :
420 :
421 : /**
422 : * State of the event check.
423 : */
424 : struct EventState
425 : {
426 : /**
427 : * How often did the callback fire with a payload?
428 : */
429 : unsigned int fired;
430 :
431 : /**
432 : * Payload of the last notification.
433 : */
434 : char payload[16];
435 :
436 : /**
437 : * Number of bytes in @e payload.
438 : */
439 : size_t payload_len;
440 : };
441 :
442 :
443 : /**
444 : * Payload of the notification.
445 : */
446 : #define PAYLOAD "hello"
447 :
448 :
449 : /**
450 : * Called when the notification arrives (or the listener times out).
451 : *
452 : * @param cls a `struct EventState *`
453 : * @param extra the payload, NULL on timeout
454 : * @param extra_size number of bytes in @a extra
455 : */
456 : static void
457 1 : event_cb (void *cls,
458 : const void *extra,
459 : size_t extra_size)
460 : {
461 1 : struct EventState *st = cls;
462 :
463 1 : if (NULL == extra)
464 0 : return; /* timeout event, not what we are after */
465 1 : st->fired++;
466 1 : st->payload_len = GNUNET_MIN (extra_size,
467 : sizeof (st->payload));
468 1 : memcpy (st->payload,
469 : extra,
470 : st->payload_len);
471 : }
472 :
473 :
474 : /**
475 : * A notification sent on the connection reaches a listener registered on
476 : * it, and stops reaching it once the listener is cancelled.
477 : *
478 : * @param pg the database context
479 : * @return 0 on success
480 : */
481 : static int
482 1 : check_events (struct TALER_EXCHANGEDB_PostgresContext *pg)
483 : {
484 1 : struct GNUNET_DB_EventHeaderP es = {
485 1 : .size = htons (sizeof (es)),
486 1 : .type = htons (TALER_DBEVENT_EXCHANGE_KYC_COMPLETED)
487 : };
488 : struct GNUNET_DB_EventHandler *eh;
489 1 : struct EventState st = { 0 };
490 :
491 1 : eh = TALER_EXCHANGEDB_event_listen (pg,
492 : GNUNET_TIME_UNIT_MINUTES,
493 : &es,
494 : &event_cb,
495 : &st);
496 1 : FAILIF (NULL == eh);
497 1 : TALER_EXCHANGEDB_event_notify (pg,
498 : &es,
499 : PAYLOAD,
500 : strlen (PAYLOAD));
501 : /* the scheduler task that would pick this up does not get to run while
502 : the checks are executing, so poll for it ourselves -- which is also
503 : what the exchange does after every notifying transaction */
504 1 : GNUNET_PQ_event_do_poll (pg->conn);
505 1 : FAILIF_C (1 != st.fired,
506 : TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
507 1 : FAILIF_C (strlen (PAYLOAD) != st.payload_len,
508 : TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
509 1 : FAILIF_C (0 != memcmp (st.payload,
510 : PAYLOAD,
511 : st.payload_len),
512 : TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
513 :
514 : /* an unrelated event does not wake us */
515 : {
516 1 : struct GNUNET_DB_EventHeaderP other = {
517 1 : .size = htons (sizeof (other)),
518 1 : .type = htons (TALER_DBEVENT_EXCHANGE_NEW_KYC_ATTRIBUTES)
519 : };
520 :
521 1 : TALER_EXCHANGEDB_event_notify (pg,
522 : &other,
523 : PAYLOAD,
524 : strlen (PAYLOAD));
525 1 : GNUNET_PQ_event_do_poll (pg->conn);
526 1 : FAILIF_C (1 != st.fired,
527 : TALER_EXCHANGEDB_event_listen_cancel (pg, eh));
528 : }
529 :
530 : /* and after cancelling, neither does the one we listened for */
531 1 : TALER_EXCHANGEDB_event_listen_cancel (pg,
532 : eh);
533 1 : TALER_EXCHANGEDB_event_notify (pg,
534 : &es,
535 : PAYLOAD,
536 : strlen (PAYLOAD));
537 1 : GNUNET_PQ_event_do_poll (pg->conn);
538 1 : FAILIF (1 != st.fired);
539 1 : return 0;
540 : }
541 :
542 :
543 : /**
544 : * Customization rules that were never deployed cannot be dropped.
545 : *
546 : * @param pg the database context
547 : * @return 0 on success
548 : */
549 : static int
550 1 : check_rules (struct TALER_EXCHANGEDB_PostgresContext *pg)
551 : {
552 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
553 : TALER_EXCHANGEDB_disable_rules (pg,
554 : "no-such-customization"));
555 : /* and a schema without SQL files on disk cannot be deployed; the
556 : search_path must be back to `exchange' afterwards either way */
557 1 : FAILIF (GNUNET_SYSERR ==
558 : TALER_EXCHANGEDB_enable_rules (pg,
559 : "no-such-customization"));
560 : /* the search_path is back to `exchange', so an unqualified query
561 : still finds the exchange's own tables */
562 1 : FAILIF (1 != TDB_count (pg,
563 : "FROM wire_targets"));
564 1 : return 0;
565 : }
566 :
567 :
568 : /**
569 : * The auditor triggers can be injected into an existing schema.
570 : *
571 : * @param pg the database context
572 : * @return 0 on success
573 : */
574 : static int
575 1 : check_auditor_triggers (struct TALER_EXCHANGEDB_PostgresContext *pg)
576 : {
577 1 : FAILIF (GNUNET_OK !=
578 : TALER_EXCHANGEDB_inject_auditor_triggers (pg));
579 1 : return 0;
580 : }
581 :
582 :
583 : /**
584 : * Dropping and re-creating the schema leaves an empty but usable
585 : * database. Nothing may run after this check.
586 : *
587 : * @param pg the database context
588 : * @return 0 on success
589 : */
590 : static int
591 1 : check_schema (struct TALER_EXCHANGEDB_PostgresContext *pg)
592 : {
593 1 : FAILIF (0 == TDB_count (pg,
594 : "FROM wire_targets"));
595 1 : FAILIF (GNUNET_OK !=
596 : TALER_EXCHANGEDB_drop_tables (pg));
597 : /* dropping twice fails: drop.sql says `DROP SCHEMA exchange CASCADE'
598 : without IF EXISTS, so there is nothing left to drop the second time */
599 1 : FAILIF (GNUNET_OK ==
600 : TALER_EXCHANGEDB_drop_tables (pg));
601 1 : FAILIF (GNUNET_OK !=
602 : TALER_EXCHANGEDB_create_tables (pg,
603 : false,
604 : 0));
605 1 : FAILIF (0 != TDB_count (pg,
606 : "FROM wire_targets"));
607 1 : return 0;
608 : }
609 :
610 :
611 : /**
612 : * The checks to run, in order.
613 : */
614 : static const struct TDB_Test tests[] = {
615 : { "misc-transactions",
616 : &check_transactions },
617 : { "misc-isolation",
618 : &check_isolation },
619 : { "misc-preflight",
620 : &check_preflight },
621 : { "misc-compute-shard",
622 : &check_compute_shard },
623 : { "misc-coin-list",
624 : &check_coin_list },
625 : { "misc-reserve-history",
626 : &check_reserve_history },
627 : { "misc-events",
628 : &check_events },
629 : { "misc-rules",
630 : &check_rules },
631 : { "misc-auditor-triggers",
632 : &check_auditor_triggers },
633 : { "misc-schema",
634 : &check_schema },
635 : { NULL, NULL }
636 : };
637 :
638 :
639 : int
640 1 : main (int argc,
641 : char *const *argv)
642 : {
643 : int ret;
644 :
645 1 : ret = TDB_main (argc,
646 : argv,
647 : "test-misc",
648 : "Tests for the exchangedb functions without a table",
649 : tests);
650 1 : TDB_denom_free (&denom);
651 1 : TDB_account_free (&account);
652 1 : return ret;
653 : }
654 :
655 :
656 : /* end of test_misc.c */
|