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_in.c
18 : * @brief tests for the exchangedb functions whose primary table is
19 : * `reserves_in`
20 : * @author Christian Grothoff
21 : *
22 : * Covers #TALER_EXCHANGEDB_do_import_credits() (for its reserve half; the
23 : * KYC-auth half is in test_kycauths_in.c),
24 : * #TALER_EXCHANGEDB_get_reserve_origin(),
25 : * #TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id(),
26 : * #TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id_by_account() and
27 : * #TALER_EXCHANGEDB_iterate_exchange_credit_transfers().
28 : *
29 : * do_import_credits() creates the `wire_targets` and `reserves` rows it
30 : * needs on its own, so the only fixture the checks have to set up is a work
31 : * shard for the batch to advance.
32 : */
33 : #include "test_common.h"
34 : #include "exchange-database/begin_shard.h"
35 : #include "exchange-database/do_import_credits.h"
36 : #include "exchange-database/get_reserve_origin.h"
37 : #include "exchange-database/iterate_exchange_credit_transfers.h"
38 : #include "exchange-database/iterate_reserves_in_above_serial_id.h"
39 : #include "exchange-database/iterate_reserves_in_above_serial_id_by_account.h"
40 :
41 :
42 : /**
43 : * Account the first two transfers come from.
44 : */
45 : #define PAYTO_A "payto://x-taler-bank/localhost/ri-a?receiver-name=A"
46 :
47 : /**
48 : * Account the third transfer comes from.
49 : */
50 : #define PAYTO_B "payto://x-taler-bank/localhost/ri-b?receiver-name=B"
51 :
52 :
53 : /**
54 : * Closure for #reserve_in_cb().
55 : */
56 : struct InContext
57 : {
58 : /**
59 : * How many rows did the callback see?
60 : */
61 : unsigned int total;
62 :
63 : /**
64 : * Stop after this many rows; 0 for no limit.
65 : */
66 : unsigned int stop_after;
67 :
68 : /**
69 : * Reserve we are looking for, NULL to match nothing.
70 : */
71 : const struct TALER_ReservePublicKeyP *reserve_pub;
72 :
73 : /**
74 : * How many times did we see @e reserve_pub?
75 : */
76 : unsigned int matched;
77 :
78 : /**
79 : * Credit reported for @e reserve_pub.
80 : */
81 : struct TALER_Amount credit;
82 :
83 : /**
84 : * Row reported for @e reserve_pub.
85 : */
86 : uint64_t rowid;
87 :
88 : /**
89 : * Wire reference reported for @e reserve_pub.
90 : */
91 : uint64_t wire_reference;
92 :
93 : /**
94 : * Sender account reported for @e reserve_pub, owned by this struct.
95 : */
96 : char *sender;
97 : };
98 :
99 :
100 : /**
101 : * Callback for the `reserves_in` iterators.
102 : *
103 : * @param cls a `struct InContext *`
104 : * @param rowid row of the transfer
105 : * @param reserve_pub reserve that was credited
106 : * @param credit amount that was credited
107 : * @param sender_account_details account the money came from
108 : * @param wire_reference bank's identifier for the transfer
109 : * @param execution_date when the transfer was made
110 : * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
111 : */
112 : static enum GNUNET_GenericReturnValue
113 10 : reserve_in_cb (void *cls,
114 : uint64_t rowid,
115 : const struct TALER_ReservePublicKeyP *reserve_pub,
116 : const struct TALER_Amount *credit,
117 : const struct TALER_FullPayto sender_account_details,
118 : uint64_t wire_reference,
119 : struct GNUNET_TIME_Timestamp execution_date)
120 : {
121 10 : struct InContext *ctx = cls;
122 :
123 : (void) execution_date;
124 10 : ctx->total++;
125 10 : if ( (NULL != ctx->reserve_pub) &&
126 4 : (0 == GNUNET_memcmp (reserve_pub,
127 : ctx->reserve_pub)) )
128 : {
129 2 : ctx->matched++;
130 2 : ctx->credit = *credit;
131 2 : ctx->rowid = rowid;
132 2 : ctx->wire_reference = wire_reference;
133 2 : GNUNET_free (ctx->sender);
134 2 : ctx->sender = (NULL == sender_account_details.full_payto)
135 : ? NULL
136 2 : : GNUNET_strdup (sender_account_details.full_payto);
137 : }
138 10 : if ( (0 != ctx->stop_after) &&
139 1 : (ctx->total >= ctx->stop_after) )
140 1 : return GNUNET_SYSERR;
141 9 : return GNUNET_OK;
142 : }
143 :
144 :
145 : /**
146 : * Closure for #transfer_cb().
147 : */
148 : struct TransferContext
149 : {
150 : /**
151 : * How many transfers did the callback see?
152 : */
153 : unsigned int total;
154 :
155 : /**
156 : * Row of the first transfer seen.
157 : */
158 : uint64_t first_row;
159 :
160 : /**
161 : * Sum of the whole-unit parts of the amounts seen.
162 : */
163 : uint64_t value_sum;
164 : };
165 :
166 :
167 : /**
168 : * Callback for #TALER_EXCHANGEDB_iterate_exchange_credit_transfers().
169 : *
170 : * @param cls a `struct TransferContext *`
171 : * @param row_id row of the transfer
172 : * @param payto_uri account the money came from
173 : * @param execution_time when the transfer was made
174 : * @param amount amount that was transferred
175 : */
176 : static void
177 11 : transfer_cb (void *cls,
178 : uint64_t row_id,
179 : const char *payto_uri,
180 : struct GNUNET_TIME_Absolute execution_time,
181 : const struct TALER_Amount *amount)
182 : {
183 11 : struct TransferContext *ctx = cls;
184 :
185 : (void) payto_uri;
186 : (void) execution_time;
187 11 : if (0 == ctx->total++)
188 5 : ctx->first_row = row_id;
189 11 : ctx->value_sum += amount->value;
190 11 : }
191 :
192 :
193 : /**
194 : * Import one wire transfer into a reserve, advancing a work shard of its
195 : * own along with it.
196 : *
197 : * @param pg the database context
198 : * @param job_name job whose shard to open and advance
199 : * @param account_name exchange bank account the transfer arrived at
200 : * @param payto account the money came from
201 : * @param seed seed for the reserve's public key
202 : * @param amount amount that was transferred, e.g. "10"
203 : * @param[out] reserve_pub set to the credited reserve
204 : * @param[out] status set to the per-transfer status
205 : * @return transaction status of the import
206 : */
207 : static enum GNUNET_DB_QueryStatus
208 4 : import_one (struct TALER_EXCHANGEDB_PostgresContext *pg,
209 : const char *job_name,
210 : const char *account_name,
211 : const char *payto,
212 : uint32_t seed,
213 : const char *amount,
214 : struct TALER_ReservePublicKeyP *reserve_pub,
215 : enum GNUNET_DB_QueryStatus *status)
216 : {
217 4 : struct TALER_Amount balance = TDB_amount (amount);
218 : struct TALER_EXCHANGEDB_ReserveInInfo info;
219 : struct TALER_EXCHANGEDB_CreditBatch batch;
220 : uint64_t start;
221 : uint64_t end;
222 : uint64_t progress;
223 :
224 4 : GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
225 : TALER_EXCHANGEDB_begin_shard (pg,
226 : job_name,
227 : GNUNET_TIME_UNIT_HOURS,
228 : 1024,
229 : &start,
230 : &end,
231 : &progress));
232 4 : TDB_fill (reserve_pub,
233 : sizeof (*reserve_pub),
234 : seed);
235 4 : memset (&info,
236 : 0,
237 : sizeof (info));
238 4 : info.reserve_pub = reserve_pub;
239 4 : info.balance = &balance;
240 4 : info.execution_time = GNUNET_TIME_timestamp_get ();
241 4 : info.sender_account_details.full_payto = (char *) payto;
242 4 : info.wire_reference = start + 1;
243 4 : memset (&batch,
244 : 0,
245 : sizeof (batch));
246 4 : batch.exchange_account_name = account_name;
247 4 : batch.reserves = &info;
248 4 : batch.reserves_length = 1;
249 4 : batch.job_name = job_name;
250 4 : batch.shard_start = start;
251 4 : batch.shard_end = end;
252 4 : batch.progress_row = end;
253 4 : batch.lease = GNUNET_TIME_UNIT_HOURS;
254 4 : return TALER_EXCHANGEDB_do_import_credits (pg,
255 : &batch,
256 : status);
257 : }
258 :
259 :
260 : /**
261 : * Nothing is known while the table is empty.
262 : *
263 : * @param pg the database context
264 : * @return 0 on success
265 : */
266 : static int
267 1 : check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
268 : {
269 : struct TALER_ReservePublicKeyP reserve_pub;
270 : struct TALER_FullPaytoHashP h_payto;
271 1 : struct TALER_FullPayto payto = { NULL };
272 1 : struct InContext ctx = { 0 };
273 1 : struct TransferContext tctx = { 0 };
274 1 : struct TALER_Amount zero = TDB_amount ("0");
275 :
276 1 : TDB_FILL (reserve_pub,
277 : 1);
278 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
279 : TALER_EXCHANGEDB_get_reserve_origin (pg,
280 : &reserve_pub,
281 : &h_payto,
282 : &payto));
283 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
284 : TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (pg,
285 : 0,
286 : &reserve_in_cb,
287 : &ctx));
288 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
289 : TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id_by_account (
290 : pg,
291 : "exchange-account-1",
292 : 0,
293 : &reserve_in_cb,
294 : &ctx));
295 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
296 : TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg,
297 : &zero,
298 : 0,
299 : 10,
300 : NULL,
301 : &transfer_cb,
302 : &tctx));
303 1 : FAILIF (0 != ctx.total);
304 1 : FAILIF (0 != tctx.total);
305 1 : return 0;
306 : }
307 :
308 :
309 : /**
310 : * Importing a transfer creates the reserve, the account and the
311 : * `reserves_in` row, and doing it again is a no-op.
312 : *
313 : * @param pg the database context
314 : * @return 0 on success
315 : */
316 : static int
317 1 : check_import (struct TALER_EXCHANGEDB_PostgresContext *pg)
318 : {
319 : struct TALER_ReservePublicKeyP reserve_pub;
320 : struct TALER_FullPaytoHashP h_payto;
321 1 : struct TALER_FullPayto payto = { NULL };
322 1 : struct TALER_FullPayto expect = {
323 : .full_payto = (char *) PAYTO_A
324 : };
325 : struct TALER_FullPaytoHashP expect_h;
326 1 : struct TALER_Amount expect_amount = TDB_amount ("10");
327 : enum GNUNET_DB_QueryStatus status;
328 :
329 1 : FAILIF (0 >
330 : import_one (pg,
331 : "ri-job-a",
332 : "exchange-account-1",
333 : PAYTO_A,
334 : 10,
335 : "10",
336 : &reserve_pub,
337 : &status));
338 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != status);
339 1 : FAILIF (1 != TDB_count (pg,
340 : "FROM reserves_in"));
341 1 : FAILIF (1 != TDB_count (pg,
342 : "FROM reserves"));
343 :
344 1 : TALER_full_payto_hash (expect,
345 : &expect_h);
346 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
347 : TALER_EXCHANGEDB_get_reserve_origin (pg,
348 : &reserve_pub,
349 : &h_payto,
350 : &payto));
351 1 : FAILIF (0 != GNUNET_memcmp (&h_payto,
352 : &expect_h));
353 1 : FAILIF (0 != strcmp (payto.full_payto,
354 : PAYTO_A));
355 1 : GNUNET_free (payto.full_payto);
356 :
357 : /* the same transfer again: the row is already there */
358 : {
359 : struct TALER_ReservePublicKeyP again;
360 :
361 1 : FAILIF (0 >
362 : import_one (pg,
363 : "ri-job-a",
364 : "exchange-account-1",
365 : PAYTO_A,
366 : 10,
367 : "10",
368 : &again,
369 : &status));
370 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != status);
371 1 : FAILIF (1 != TDB_count (pg,
372 : "FROM reserves_in"));
373 : }
374 :
375 : {
376 : struct InContext ctx;
377 :
378 1 : memset (&ctx,
379 : 0,
380 : sizeof (ctx));
381 1 : ctx.reserve_pub = &reserve_pub;
382 1 : FAILIF (0 >=
383 : TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (
384 : pg,
385 : 0,
386 : &reserve_in_cb,
387 : &ctx));
388 1 : FAILIF_C (1 != ctx.matched,
389 : GNUNET_free (ctx.sender));
390 1 : FAILIF_C (0 != TALER_amount_cmp (&ctx.credit,
391 : &expect_amount),
392 : GNUNET_free (ctx.sender));
393 1 : FAILIF_C (0 != strcmp (ctx.sender,
394 : PAYTO_A),
395 : GNUNET_free (ctx.sender));
396 1 : GNUNET_free (ctx.sender);
397 : }
398 1 : return 0;
399 : }
400 :
401 :
402 : /**
403 : * With several transfers on file the iterators filter by serial and by
404 : * exchange bank account, and honour an aborting callback.
405 : *
406 : * @param pg the database context
407 : * @return 0 on success
408 : */
409 : static int
410 1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
411 : {
412 : struct TALER_ReservePublicKeyP r2;
413 : struct TALER_ReservePublicKeyP r3;
414 : struct InContext ctx;
415 : enum GNUNET_DB_QueryStatus status;
416 :
417 : /* a second transfer from the same account to the same exchange account */
418 1 : FAILIF (0 >
419 : import_one (pg,
420 : "ri-job-a",
421 : "exchange-account-1",
422 : PAYTO_A,
423 : 11,
424 : "2",
425 : &r2,
426 : &status));
427 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != status);
428 : /* and one from a different account to a different exchange account */
429 1 : FAILIF (0 >
430 : import_one (pg,
431 : "ri-job-b",
432 : "exchange-account-2",
433 : PAYTO_B,
434 : 12,
435 : "5",
436 : &r3,
437 : &status));
438 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != status);
439 1 : FAILIF (3 != TDB_count (pg,
440 : "FROM reserves_in"));
441 :
442 1 : memset (&ctx,
443 : 0,
444 : sizeof (ctx));
445 1 : ctx.reserve_pub = &r3;
446 1 : FAILIF (3 !=
447 : TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (pg,
448 : 0,
449 : &reserve_in_cb,
450 : &ctx));
451 1 : FAILIF_C (1 != ctx.matched,
452 : GNUNET_free (ctx.sender));
453 1 : FAILIF_C (0 != strcmp (ctx.sender,
454 : PAYTO_B),
455 : GNUNET_free (ctx.sender));
456 1 : GNUNET_free (ctx.sender);
457 :
458 : /* by exchange bank account */
459 1 : memset (&ctx,
460 : 0,
461 : sizeof (ctx));
462 1 : FAILIF (2 !=
463 : TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id_by_account (
464 : pg,
465 : "exchange-account-1",
466 : 0,
467 : &reserve_in_cb,
468 : &ctx));
469 1 : GNUNET_free (ctx.sender);
470 1 : memset (&ctx,
471 : 0,
472 : sizeof (ctx));
473 1 : FAILIF (1 !=
474 : TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id_by_account (
475 : pg,
476 : "exchange-account-2",
477 : 0,
478 : &reserve_in_cb,
479 : &ctx));
480 1 : GNUNET_free (ctx.sender);
481 1 : memset (&ctx,
482 : 0,
483 : sizeof (ctx));
484 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
485 : TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id_by_account (
486 : pg,
487 : "exchange-account-does-not-exist",
488 : 0,
489 : &reserve_in_cb,
490 : &ctx));
491 1 : FAILIF (0 != ctx.total);
492 :
493 : /* A callback that gives up stops the iteration but is not an error: the
494 : status stays the number of rows the query matched, not the number the
495 : callback was shown. */
496 1 : memset (&ctx,
497 : 0,
498 : sizeof (ctx));
499 1 : ctx.stop_after = 1;
500 1 : FAILIF (3 !=
501 : TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (pg,
502 : 0,
503 : &reserve_in_cb,
504 : &ctx));
505 1 : FAILIF (1 != ctx.total);
506 1 : GNUNET_free (ctx.sender);
507 1 : return 0;
508 : }
509 :
510 :
511 : /**
512 : * The serial number bound of the plain iterator excludes earlier rows.
513 : * The bound is inclusive, so passing the row of the second transfer
514 : * leaves the first one out and nothing else.
515 : *
516 : * @param pg the database context
517 : * @return 0 on success
518 : */
519 : static int
520 1 : check_serial_bound (struct TALER_EXCHANGEDB_PostgresContext *pg)
521 : {
522 : struct InContext ctx;
523 :
524 1 : FAILIF (3 != TDB_count (pg,
525 : "FROM reserves_in"));
526 1 : memset (&ctx,
527 : 0,
528 : sizeof (ctx));
529 1 : FAILIF (2 !=
530 : TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (pg,
531 : 2,
532 : &reserve_in_cb,
533 : &ctx));
534 1 : GNUNET_free (ctx.sender);
535 1 : memset (&ctx,
536 : 0,
537 : sizeof (ctx));
538 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
539 : TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (pg,
540 : 1000,
541 : &reserve_in_cb,
542 : &ctx));
543 1 : GNUNET_free (ctx.sender);
544 1 : return 0;
545 : }
546 :
547 :
548 : /**
549 : * The AML view of the same rows filters by amount and by account, and can
550 : * walk the table backwards.
551 : *
552 : * @param pg the database context
553 : * @return 0 on success
554 : */
555 : static int
556 1 : check_credit_transfers (struct TALER_EXCHANGEDB_PostgresContext *pg)
557 : {
558 : struct TALER_NormalizedPayto npayto;
559 : struct TALER_NormalizedPaytoHashP h_payto;
560 1 : struct TALER_FullPayto payto_a = {
561 : .full_payto = (char *) PAYTO_A
562 : };
563 1 : struct TALER_Amount zero = TDB_amount ("0");
564 1 : struct TALER_Amount five = TDB_amount ("5");
565 1 : struct TALER_Amount huge = TDB_amount ("1000");
566 : struct TransferContext ctx;
567 : uint64_t lowest;
568 :
569 : /* everything, ascending */
570 1 : memset (&ctx,
571 : 0,
572 : sizeof (ctx));
573 1 : FAILIF (3 !=
574 : TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg,
575 : &zero,
576 : 0,
577 : 10,
578 : NULL,
579 : &transfer_cb,
580 : &ctx));
581 1 : FAILIF (3 != ctx.total);
582 1 : FAILIF (10 + 2 + 5 != ctx.value_sum);
583 1 : lowest = ctx.first_row;
584 :
585 : /* the amount threshold drops the EUR:2 transfer */
586 1 : memset (&ctx,
587 : 0,
588 : sizeof (ctx));
589 1 : FAILIF (2 !=
590 : TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg,
591 : &five,
592 : 0,
593 : 10,
594 : NULL,
595 : &transfer_cb,
596 : &ctx));
597 1 : FAILIF (10 + 5 != ctx.value_sum);
598 :
599 : /* a threshold nobody meets */
600 1 : memset (&ctx,
601 : 0,
602 : sizeof (ctx));
603 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
604 : TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg,
605 : &huge,
606 : 0,
607 : 10,
608 : NULL,
609 : &transfer_cb,
610 : &ctx));
611 1 : FAILIF (0 != ctx.total);
612 :
613 : /* filtering by account keeps the two transfers from PAYTO_A */
614 1 : npayto = TALER_payto_normalize (payto_a);
615 1 : GNUNET_assert (NULL != npayto.normalized_payto);
616 1 : TALER_normalized_payto_hash (npayto,
617 : &h_payto);
618 1 : GNUNET_free (npayto.normalized_payto);
619 1 : memset (&ctx,
620 : 0,
621 : sizeof (ctx));
622 1 : FAILIF (2 !=
623 : TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg,
624 : &zero,
625 : 0,
626 : 10,
627 : &h_payto,
628 : &transfer_cb,
629 : &ctx));
630 1 : FAILIF (10 + 2 != ctx.value_sum);
631 :
632 : /* the limit caps the result set... */
633 1 : memset (&ctx,
634 : 0,
635 : sizeof (ctx));
636 1 : FAILIF (1 !=
637 : TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg,
638 : &zero,
639 : 0,
640 : 1,
641 : NULL,
642 : &transfer_cb,
643 : &ctx));
644 1 : FAILIF (lowest != ctx.first_row);
645 :
646 : /* ...and a negative limit walks the table the other way, starting from
647 : the offset rather than from the beginning */
648 1 : memset (&ctx,
649 : 0,
650 : sizeof (ctx));
651 1 : FAILIF (3 !=
652 : TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg,
653 : &zero,
654 : 1000,
655 : -10,
656 : NULL,
657 : &transfer_cb,
658 : &ctx));
659 1 : FAILIF (lowest == ctx.first_row);
660 1 : FAILIF (10 + 2 + 5 != ctx.value_sum);
661 1 : return 0;
662 : }
663 :
664 :
665 : /**
666 : * The checks to run, in order.
667 : */
668 : static const struct TDB_Test tests[] = {
669 : { "reserves-in-empty",
670 : &check_empty },
671 : { "reserves-in-import",
672 : &check_import },
673 : { "reserves-in-iterate",
674 : &check_iterate },
675 : { "reserves-in-serial-bound",
676 : &check_serial_bound },
677 : { "reserves-in-credit-transfers",
678 : &check_credit_transfers },
679 : { NULL, NULL }
680 : };
681 :
682 :
683 : int
684 1 : main (int argc,
685 : char *const *argv)
686 : {
687 1 : return TDB_main (argc,
688 : argv,
689 : "test-reserves-in",
690 : "Tests for the exchangedb `reserves_in' table",
691 : tests);
692 : }
693 :
694 :
695 : /* end of test_reserves_in.c */
|