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_purse_deposits.c
18 : * @brief tests for the exchangedb functions whose primary table is
19 : * `purse_deposits`
20 : * @author Christian Grothoff
21 : *
22 : * Covers #TALER_EXCHANGEDB_do_purse_deposit(),
23 : * #TALER_EXCHANGEDB_get_purse_deposit(),
24 : * #TALER_EXCHANGEDB_iterate_purse_deposits_above_serial_id() and
25 : * #TALER_EXCHANGEDB_iterate_purse_deposits_by_purse().
26 : *
27 : * `purse_deposits` references `purse_requests` and `known_coins`, which
28 : * TDB_purse() and TDB_coin() create. do_purse_deposit() declines in three
29 : * ways: the coin has too little left, the purse has already been decided
30 : * (too late), and the same coin was already paid into the purse with a
31 : * different amount (conflict).
32 : */
33 : #include "test_common.h"
34 : #include "exchange-database/do_expire_purse.h"
35 : #include "exchange-database/do_purse_deposit.h"
36 : #include "exchange-database/get_purse_deposit.h"
37 : #include "exchange-database/iterate_purse_deposits_above_serial_id.h"
38 : #include "exchange-database/iterate_purse_deposits_by_purse.h"
39 : #include "exchange-database/rollback.h"
40 : #include "exchange-database/start.h"
41 :
42 :
43 : /**
44 : * Denomination the checks deposit.
45 : */
46 : static struct TDB_Denom denom;
47 :
48 :
49 : /**
50 : * Build a timestamp from a number of seconds since the epoch.
51 : *
52 : * @param secs seconds since the epoch
53 : * @return the timestamp
54 : */
55 : static struct GNUNET_TIME_Timestamp
56 3 : ts (uint64_t secs)
57 : {
58 3 : struct GNUNET_TIME_Absolute abs = {
59 3 : .abs_value_us = secs * 1000LLU * 1000LLU
60 : };
61 :
62 3 : return GNUNET_TIME_absolute_to_timestamp (abs);
63 : }
64 :
65 :
66 : /**
67 : * Closure for #deposit_cb() and #refund_cb().
68 : */
69 : struct DepositContext
70 : {
71 : /**
72 : * How many rows did the callback see?
73 : */
74 : unsigned int total;
75 :
76 : /**
77 : * Stop after this many rows; 0 for no limit.
78 : */
79 : unsigned int stop_after;
80 :
81 : /**
82 : * Coin we are looking for, NULL to match nothing.
83 : */
84 : const struct TALER_CoinSpendPublicKeyP *coin_pub;
85 :
86 : /**
87 : * How many times did we see it?
88 : */
89 : unsigned int matched;
90 :
91 : /**
92 : * Amount reported for it.
93 : */
94 : struct TALER_Amount amount;
95 :
96 : /**
97 : * Was a reserve reported for its purse?
98 : */
99 : bool have_reserve;
100 : };
101 :
102 :
103 : /**
104 : * Callback for
105 : * #TALER_EXCHANGEDB_iterate_purse_deposits_above_serial_id().
106 : *
107 : * @param cls a `struct DepositContext *`
108 : * @param rowid row of the deposit
109 : * @param deposit details of the deposit
110 : * @param reserve_pub reserve the purse was merged into, NULL if none
111 : * @param flags flags of the purse
112 : * @param auditor_balance balance of the purse
113 : * @param purse_total target amount of the purse
114 : * @param denom_pub denomination of the coin
115 : * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
116 : */
117 : static enum GNUNET_GenericReturnValue
118 1 : deposit_cb (void *cls,
119 : uint64_t rowid,
120 : const struct TALER_EXCHANGEDB_PurseDeposit *deposit,
121 : const struct TALER_ReservePublicKeyP *reserve_pub,
122 : enum TALER_WalletAccountMergeFlags flags,
123 : const struct TALER_Amount *auditor_balance,
124 : const struct TALER_Amount *purse_total,
125 : const struct TALER_DenominationPublicKey *denom_pub)
126 : {
127 1 : struct DepositContext *ctx = cls;
128 :
129 : (void) rowid;
130 : (void) flags;
131 : (void) auditor_balance;
132 : (void) purse_total;
133 : (void) denom_pub;
134 1 : ctx->total++;
135 1 : if ( (NULL != ctx->coin_pub) &&
136 1 : (0 == GNUNET_memcmp (&deposit->coin_pub,
137 : ctx->coin_pub)) )
138 : {
139 1 : ctx->matched++;
140 1 : ctx->amount = deposit->amount;
141 1 : ctx->have_reserve = (NULL != reserve_pub);
142 : }
143 1 : if ( (0 != ctx->stop_after) &&
144 0 : (ctx->total >= ctx->stop_after) )
145 0 : return GNUNET_SYSERR;
146 1 : return GNUNET_OK;
147 : }
148 :
149 :
150 : /**
151 : * Callback for #TALER_EXCHANGEDB_iterate_purse_deposits_by_purse().
152 : *
153 : * @param cls a `struct DepositContext *`
154 : * @param rowid row of the deposit
155 : * @param amount_with_fee how much was paid in
156 : * @param coin_pub which coin paid
157 : * @param denom_pub denomination of that coin
158 : * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
159 : */
160 : static enum GNUNET_GenericReturnValue
161 1 : refund_cb (void *cls,
162 : uint64_t rowid,
163 : const struct TALER_Amount *amount_with_fee,
164 : const struct TALER_CoinSpendPublicKeyP *coin_pub,
165 : const struct TALER_DenominationPublicKey *denom_pub)
166 : {
167 1 : struct DepositContext *ctx = cls;
168 :
169 : (void) rowid;
170 : (void) denom_pub;
171 1 : ctx->total++;
172 1 : if ( (NULL != ctx->coin_pub) &&
173 1 : (0 == GNUNET_memcmp (coin_pub,
174 : ctx->coin_pub)) )
175 : {
176 1 : ctx->matched++;
177 1 : ctx->amount = *amount_with_fee;
178 : }
179 1 : if ( (0 != ctx->stop_after) &&
180 0 : (ctx->total >= ctx->stop_after) )
181 0 : return GNUNET_SYSERR;
182 1 : return GNUNET_OK;
183 : }
184 :
185 :
186 : /**
187 : * Outcome of a purse deposit.
188 : */
189 : struct DepositStatus
190 : {
191 : /**
192 : * Did the coin have enough left?
193 : */
194 : bool balance_ok;
195 :
196 : /**
197 : * Was the purse already decided?
198 : */
199 : bool too_late;
200 :
201 : /**
202 : * Was the coin already paid into this purse differently?
203 : */
204 : bool conflict;
205 : };
206 :
207 :
208 : /**
209 : * Pay a coin into a purse.
210 : *
211 : * @param pg the database context
212 : * @param purse purse to credit
213 : * @param coin coin to debit
214 : * @param seed seed for the coin signature
215 : * @param amount how much to take from the coin, e.g. "1"
216 : * @param fee purse deposit fee included in @a amount, e.g. "0"
217 : * @param[out] st set to the outcome
218 : * @return transaction status
219 : */
220 : static enum GNUNET_DB_QueryStatus
221 5 : run_deposit (struct TALER_EXCHANGEDB_PostgresContext *pg,
222 : const struct TDB_Purse *purse,
223 : const struct TALER_CoinPublicInfo *coin,
224 : uint32_t seed,
225 : const char *amount,
226 : const char *fee,
227 : struct DepositStatus *st)
228 : {
229 : struct TALER_CoinSpendSignatureP coin_sig;
230 5 : struct TALER_Amount a = TDB_amount (amount);
231 5 : struct TALER_Amount f = TDB_amount (fee);
232 : struct TALER_Amount net;
233 :
234 5 : TDB_fill (&coin_sig,
235 : sizeof (coin_sig),
236 : seed);
237 5 : GNUNET_assert (0 <=
238 : TALER_amount_subtract (&net,
239 : &a,
240 : &f));
241 5 : memset (st,
242 : 0,
243 : sizeof (*st));
244 5 : return TALER_EXCHANGEDB_do_purse_deposit (pg,
245 : &purse->purse_pub,
246 : &coin->coin_pub,
247 : &a,
248 : &coin_sig,
249 : &net,
250 : &st->balance_ok,
251 : &st->too_late,
252 : &st->conflict);
253 : }
254 :
255 :
256 : /**
257 : * Nothing is reported while the table is empty.
258 : *
259 : * @param pg the database context
260 : * @return 0 on success
261 : */
262 : static int
263 1 : check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
264 : {
265 : struct TALER_PurseContractPublicKeyP purse_pub;
266 : struct TALER_CoinSpendPublicKeyP coin_pub;
267 : struct TALER_Amount amount;
268 : struct TALER_DenominationHashP h_denom_pub;
269 : struct TALER_AgeCommitmentHashP phac;
270 : struct TALER_CoinSpendSignatureP coin_sig;
271 1 : struct DepositContext ctx = { 0 };
272 1 : char *partner_url = NULL;
273 : bool no_age_commitment;
274 :
275 1 : TDB_denom (pg,
276 : 10,
277 : "5",
278 : "0.1",
279 : &denom);
280 1 : TDB_FILL (purse_pub,
281 : 1);
282 1 : TDB_FILL (coin_pub,
283 : 1);
284 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
285 : TALER_EXCHANGEDB_get_purse_deposit (pg,
286 : &purse_pub,
287 : &coin_pub,
288 : &amount,
289 : &h_denom_pub,
290 : &phac,
291 : &no_age_commitment,
292 : &coin_sig,
293 : &partner_url));
294 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
295 : TALER_EXCHANGEDB_iterate_purse_deposits_above_serial_id (
296 : pg,
297 : 0,
298 : &deposit_cb,
299 : &ctx));
300 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
301 : TALER_EXCHANGEDB_iterate_purse_deposits_by_purse (pg,
302 : &purse_pub,
303 : &refund_cb,
304 : &ctx));
305 1 : FAILIF (0 != ctx.total);
306 1 : return 0;
307 : }
308 :
309 :
310 : /**
311 : * Paying a coin into a purse debits the coin and credits the purse.
312 : *
313 : * @param pg the database context
314 : * @return 0 on success
315 : */
316 : static int
317 1 : check_deposit (struct TALER_EXCHANGEDB_PostgresContext *pg)
318 : {
319 : struct TDB_Purse purse;
320 : struct TALER_CoinPublicInfo coin;
321 : struct DepositStatus st;
322 : struct TALER_Amount amount;
323 1 : struct TALER_Amount expect = TDB_amount ("1");
324 : struct TALER_DenominationHashP h_denom_pub;
325 : struct TALER_AgeCommitmentHashP phac;
326 : struct TALER_CoinSpendSignatureP coin_sig;
327 : struct TALER_CoinSpendSignatureP expect_sig;
328 1 : char *partner_url = NULL;
329 1 : bool no_age_commitment = false;
330 :
331 1 : TDB_purse (pg,
332 : 10,
333 : "5",
334 : ts (1700000000),
335 : &purse);
336 1 : TDB_coin (pg,
337 : &denom,
338 : 20,
339 : &coin,
340 : NULL);
341 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
342 : run_deposit (pg,
343 : &purse,
344 : &coin,
345 : 20,
346 : "1",
347 : "0",
348 : &st),
349 : TDB_coin_free (&coin));
350 1 : FAILIF_C (! st.balance_ok,
351 : TDB_coin_free (&coin));
352 1 : FAILIF_C (st.too_late,
353 : TDB_coin_free (&coin));
354 1 : FAILIF_C (st.conflict,
355 : TDB_coin_free (&coin));
356 1 : FAILIF_C (1 != TDB_count (pg,
357 : "FROM purse_deposits"),
358 : TDB_coin_free (&coin));
359 :
360 : /* the coin was debited... */
361 : {
362 1 : char *hex = TDB_hex (&coin.coin_pub,
363 : sizeof (coin.coin_pub));
364 :
365 1 : FAILIF_C (1 != TDB_count (pg,
366 : "FROM known_coins"
367 : " WHERE coin_pub=decode('%s','hex')"
368 : " AND remaining=ROW(4,0)::taler_amount",
369 : hex),
370 : GNUNET_free (hex); TDB_coin_free (&coin));
371 1 : GNUNET_free (hex);
372 : }
373 : /* ...and the purse credited */
374 : {
375 1 : char *hex = TDB_hex (&purse.purse_pub,
376 : sizeof (purse.purse_pub));
377 :
378 1 : FAILIF_C (1 != TDB_count (pg,
379 : "FROM purse_requests"
380 : " WHERE purse_pub=decode('%s','hex')"
381 : " AND balance=ROW(1,0)::taler_amount",
382 : hex),
383 : GNUNET_free (hex); TDB_coin_free (&coin));
384 1 : GNUNET_free (hex);
385 : }
386 :
387 1 : TDB_FILL (expect_sig,
388 : 20);
389 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
390 : TALER_EXCHANGEDB_get_purse_deposit (pg,
391 : &purse.purse_pub,
392 : &coin.coin_pub,
393 : &amount,
394 : &h_denom_pub,
395 : &phac,
396 : &no_age_commitment,
397 : &coin_sig,
398 : &partner_url),
399 : TDB_coin_free (&coin));
400 1 : FAILIF_C (0 != TALER_amount_cmp (&amount,
401 : &expect),
402 : GNUNET_free (partner_url); TDB_coin_free (&coin));
403 1 : FAILIF_C (0 != GNUNET_memcmp (&h_denom_pub,
404 : &denom.h_denom_pub),
405 : GNUNET_free (partner_url); TDB_coin_free (&coin));
406 1 : FAILIF_C (0 != GNUNET_memcmp (&coin_sig,
407 : &expect_sig),
408 : GNUNET_free (partner_url); TDB_coin_free (&coin));
409 1 : FAILIF_C (! no_age_commitment,
410 : GNUNET_free (partner_url); TDB_coin_free (&coin));
411 : /* the purse is ours, so there is no partner exchange */
412 1 : FAILIF_C (NULL != partner_url,
413 : GNUNET_free (partner_url); TDB_coin_free (&coin));
414 :
415 : /* re-depositing the same coin with the same amount is idempotent */
416 1 : FAILIF_C (0 >
417 : run_deposit (pg,
418 : &purse,
419 : &coin,
420 : 20,
421 : "1",
422 : "0",
423 : &st),
424 : TDB_coin_free (&coin));
425 1 : FAILIF_C (1 != TDB_count (pg,
426 : "FROM purse_deposits"),
427 : TDB_coin_free (&coin));
428 :
429 : /* ...but a second attempt with a different coin signature is a conflict:
430 : the idempotency check compares the signature, not the amount */
431 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
432 : run_deposit (pg,
433 : &purse,
434 : &coin,
435 : 29,
436 : "1",
437 : "0",
438 : &st),
439 : TDB_coin_free (&coin));
440 1 : FAILIF_C (! st.conflict,
441 : TDB_coin_free (&coin));
442 1 : FAILIF_C (1 != TDB_count (pg,
443 : "FROM purse_deposits"),
444 : TDB_coin_free (&coin));
445 1 : TDB_coin_free (&coin);
446 1 : return 0;
447 : }
448 :
449 :
450 : /**
451 : * A coin without enough left cannot be paid into a purse.
452 : *
453 : * @param pg the database context
454 : * @return 0 on success
455 : */
456 : static int
457 1 : check_insufficient_balance (struct TALER_EXCHANGEDB_PostgresContext *pg)
458 : {
459 : struct TDB_Purse purse;
460 : struct TALER_CoinPublicInfo coin;
461 : struct DepositStatus st;
462 :
463 1 : TDB_purse (pg,
464 : 10,
465 : "5",
466 : ts (1700000000),
467 : &purse);
468 1 : TDB_coin (pg,
469 : &denom,
470 : 21,
471 : &coin,
472 : NULL);
473 : /* The `purse_deposits` row goes in before the coin balance is checked,
474 : so the caller has to roll back -- which is what the /purses/.../deposit
475 : handler does. */
476 1 : FAILIF_C (GNUNET_OK !=
477 : TALER_EXCHANGEDB_start (pg,
478 : "test-purse-deposit-insufficient"),
479 : TDB_coin_free (&coin));
480 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
481 : run_deposit (pg,
482 : &purse,
483 : &coin,
484 : 21,
485 : "6",
486 : "0",
487 : &st),
488 : TALER_EXCHANGEDB_rollback (pg); TDB_coin_free (&coin));
489 1 : TALER_EXCHANGEDB_rollback (pg);
490 1 : TDB_coin_free (&coin);
491 1 : FAILIF (st.balance_ok);
492 1 : FAILIF (st.conflict);
493 1 : FAILIF (1 != TDB_count (pg,
494 : "FROM purse_deposits"));
495 1 : return 0;
496 : }
497 :
498 :
499 : /**
500 : * Once a merged purse has expired and been decided, the deposit that
501 : * would complete it is refused as too late.
502 : *
503 : * Note what this shows about the shape of the function: the "too late"
504 : * answer only comes out of the attempt to record the decision, which is
505 : * only reached for a purse that has been merged *and* is completed by this
506 : * deposit. A deposit into an expired purse that neither completes it nor
507 : * has been merged is accepted -- see EDBT-9 in bugs.txt.
508 : *
509 : * @param pg the database context
510 : * @return 0 on success
511 : */
512 : static int
513 1 : check_too_late (struct TALER_EXCHANGEDB_PostgresContext *pg)
514 : {
515 : struct TDB_Purse purse;
516 : struct TALER_CoinPublicInfo coin;
517 : struct TALER_ReservePublicKeyP reserve_pub;
518 : struct DepositStatus st;
519 : struct GNUNET_TIME_Absolute expiration;
520 :
521 : /* a purse that expired in 2020, merged into a reserve */
522 1 : TDB_purse (pg,
523 : 11,
524 : "5",
525 : ts (1600000000),
526 : &purse);
527 1 : TDB_reserve (pg,
528 : 11,
529 : "0",
530 : &reserve_pub);
531 : {
532 1 : char *phex = TDB_hex (&purse.purse_pub,
533 : sizeof (purse.purse_pub));
534 1 : char *rhex = TDB_hex (&reserve_pub,
535 : sizeof (reserve_pub));
536 :
537 1 : FAILIF_C (GNUNET_OK !=
538 : TDB_exec (pg,
539 : "INSERT INTO purse_merges"
540 : " (reserve_pub,purse_pub,merge_sig,merge_timestamp)"
541 : " VALUES (decode('%s','hex'),decode('%s','hex')"
542 : " ,decode(repeat('11',64),'hex'),1000000);",
543 : rhex,
544 : phex),
545 : GNUNET_free (phex); GNUNET_free (rhex));
546 1 : GNUNET_free (phex);
547 1 : GNUNET_free (rhex);
548 : }
549 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
550 : TALER_EXCHANGEDB_do_expire_purse (pg,
551 : GNUNET_TIME_UNIT_ZERO_ABS,
552 : GNUNET_TIME_absolute_get (),
553 : &expiration));
554 1 : TDB_coin (pg,
555 : &denom,
556 : 22,
557 : &coin,
558 : NULL);
559 : /* Pay in the full target amount: the "too late" answer only comes out
560 : when the purse would be decided by this deposit, which is when the
561 : function tries to insert the `purse_decision` row that already exists.
562 : As with the balance check, the row is written first, so roll back. */
563 1 : FAILIF_C (GNUNET_OK !=
564 : TALER_EXCHANGEDB_start (pg,
565 : "test-purse-deposit-too-late"),
566 : TDB_coin_free (&coin));
567 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
568 : run_deposit (pg,
569 : &purse,
570 : &coin,
571 : 22,
572 : "5",
573 : "0",
574 : &st),
575 : TALER_EXCHANGEDB_rollback (pg); TDB_coin_free (&coin));
576 1 : TALER_EXCHANGEDB_rollback (pg);
577 1 : TDB_coin_free (&coin);
578 1 : FAILIF (! st.too_late);
579 1 : FAILIF (1 != TDB_count (pg,
580 : "FROM purse_deposits"));
581 1 : return 0;
582 : }
583 :
584 :
585 : /**
586 : * The iterators see the deposit; the by-purse one only for its own purse.
587 : *
588 : * @param pg the database context
589 : * @return 0 on success
590 : */
591 : static int
592 1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
593 : {
594 : struct TALER_CoinSpendPublicKeyP coin_pub;
595 : struct TALER_PurseContractPublicKeyP purse_pub;
596 : struct DepositContext ctx;
597 1 : struct TALER_Amount expect = TDB_amount ("1");
598 :
599 1 : TDB_FILL (coin_pub,
600 : 20);
601 1 : TDB_FILL (purse_pub,
602 : 10);
603 1 : memset (&ctx,
604 : 0,
605 : sizeof (ctx));
606 1 : ctx.coin_pub = &coin_pub;
607 1 : FAILIF (1 !=
608 : TALER_EXCHANGEDB_iterate_purse_deposits_above_serial_id (
609 : pg,
610 : 0,
611 : &deposit_cb,
612 : &ctx));
613 1 : FAILIF (1 != ctx.matched);
614 1 : FAILIF (0 != TALER_amount_cmp (&ctx.amount,
615 : &expect));
616 : /* the purse was never merged, so there is no reserve */
617 1 : FAILIF (ctx.have_reserve);
618 :
619 1 : memset (&ctx,
620 : 0,
621 : sizeof (ctx));
622 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
623 : TALER_EXCHANGEDB_iterate_purse_deposits_above_serial_id (
624 : pg,
625 : 1000,
626 : &deposit_cb,
627 : &ctx));
628 1 : FAILIF (0 != ctx.total);
629 :
630 1 : memset (&ctx,
631 : 0,
632 : sizeof (ctx));
633 1 : ctx.coin_pub = &coin_pub;
634 1 : FAILIF (1 !=
635 : TALER_EXCHANGEDB_iterate_purse_deposits_by_purse (pg,
636 : &purse_pub,
637 : &refund_cb,
638 : &ctx));
639 1 : FAILIF (1 != ctx.matched);
640 1 : FAILIF (0 != TALER_amount_cmp (&ctx.amount,
641 : &expect));
642 :
643 : /* a purse nobody paid into has no coins */
644 : {
645 : struct TALER_PurseContractPublicKeyP other;
646 :
647 1 : TDB_FILL (other,
648 : 11);
649 1 : memset (&ctx,
650 : 0,
651 : sizeof (ctx));
652 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
653 : TALER_EXCHANGEDB_iterate_purse_deposits_by_purse (pg,
654 : &other,
655 : &refund_cb,
656 : &ctx));
657 1 : FAILIF (0 != ctx.total);
658 : }
659 1 : return 0;
660 : }
661 :
662 :
663 : /**
664 : * The checks to run, in order.
665 : */
666 : static const struct TDB_Test tests[] = {
667 : { "purse-deposits-empty",
668 : &check_empty },
669 : { "purse-deposits-deposit",
670 : &check_deposit },
671 : { "purse-deposits-insufficient-balance",
672 : &check_insufficient_balance },
673 : { "purse-deposits-too-late",
674 : &check_too_late },
675 : { "purse-deposits-iterate",
676 : &check_iterate },
677 : { NULL, NULL }
678 : };
679 :
680 :
681 : int
682 1 : main (int argc,
683 : char *const *argv)
684 : {
685 : int ret;
686 :
687 1 : ret = TDB_main (argc,
688 : argv,
689 : "test-purse-deposits",
690 : "Tests for the exchangedb `purse_deposits' table",
691 : tests);
692 1 : TDB_denom_free (&denom);
693 1 : return ret;
694 : }
695 :
696 :
697 : /* end of test_purse_deposits.c */
|