LCOV - code coverage report
Current view: top level - exchangedb - test_regressions.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 91.3 % 230 210
Test Date: 2026-09-11 18:55:36 Functions: 100.0 % 17 17

            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_regressions.c
      18              :  * @brief regression tests for individual exchangedb operations
      19              :  *
      20              :  * Each check in the #tests table pins down one previously broken behaviour.
      21              :  * The tests share one scratch database, which test_regressions.sh creates and
      22              :  * removes; every check must therefore use fresh keys rather than assume an
      23              :  * empty database.
      24              :  */
      25              : #include "exchangedb_lib.h"
      26              : #include "taler/taler_json_lib.h"
      27              : #include "helper.h"
      28              : #include "exchange-database/create_tables.h"
      29              : #include "exchange-database/do_aggregate.h"
      30              : #include "exchange-database/compute_shard.h"
      31              : #include "exchange-database/start.h"
      32              : #include "exchange-database/commit.h"
      33              : #include "exchange-database/rollback.h"
      34              : #include "exchange-database/do_reserve_open.h"
      35              : #include "exchange-database/get_purse_deposit.h"
      36              : #include "exchange-database/get_reserve_close_info.h"
      37              : #include "exchange-database/begin_shard.h"
      38              : #include "exchange-database/abort_shard.h"
      39              : #include "exchange-database/update_shard_progress.h"
      40              : #include "exchange-database/do_import_credits.h"
      41              : 
      42              : 
      43              : /**
      44              :  * Currency we use, must match test_regressions.sh.
      45              :  */
      46              : #define CURRENCY "EUR"
      47              : 
      48              : /**
      49              :  * Report a failed expectation and return 1 from the calling check.
      50              :  */
      51              : #define FAILIF(cond)                            \
      52              :         do {                                          \
      53              :           if (! (cond)) break;                        \
      54              :           GNUNET_break (0);                           \
      55              :           fprintf (stderr,                            \
      56              :                    "FAILED: %s at %s:%u\n",           \
      57              :                    # cond, __FILE__, __LINE__);       \
      58              :           return 1;                                   \
      59              :         } while (0)
      60              : 
      61              : 
      62              : /**
      63              :  * Our database context.
      64              :  */
      65              : static struct TALER_EXCHANGEDB_PostgresContext *pg;
      66              : 
      67              : /**
      68              :  * Name of the single check to run, NULL to run all of them.
      69              :  */
      70              : static char *only;
      71              : 
      72              : /**
      73              :  * Return value of the process.
      74              :  */
      75              : static int result;
      76              : 
      77              : 
      78              : /**
      79              :  * Run @a sql on our database connection, outside of any transaction.
      80              :  *
      81              :  * @param sql statement(s) to run
      82              :  * @return #GNUNET_OK on success
      83              :  */
      84              : static enum GNUNET_GenericReturnValue
      85           15 : exec_sql (const char *sql)
      86              : {
      87           15 :   struct GNUNET_PQ_ExecuteStatement es[] = {
      88           15 :     GNUNET_PQ_make_execute (sql),
      89              :     GNUNET_PQ_EXECUTE_STATEMENT_END
      90              :   };
      91              : 
      92           15 :   return GNUNET_PQ_exec_statements (pg->conn,
      93              :                                     es);
      94              : }
      95              : 
      96              : 
      97              : /**
      98              :  * Convert @a val to an amount in our currency.
      99              :  *
     100              :  * @param str amount without the currency prefix, e.g. "10.5"
     101              :  * @param[out] amount set to the parsed amount
     102              :  */
     103              : static void
     104            8 : parse_amount (const char *str,
     105              :               struct TALER_Amount *amount)
     106              : {
     107              :   char *s;
     108              : 
     109            8 :   GNUNET_asprintf (&s,
     110              :                    CURRENCY ":%s",
     111              :                    str);
     112            8 :   GNUNET_assert (GNUNET_OK ==
     113              :                  TALER_string_to_amount (s,
     114              :                                          amount));
     115            8 :   GNUNET_free (s);
     116            8 : }
     117              : 
     118              : 
     119              : /**
     120              :  * Build an INSERT that creates a reserve with a zero balance and no
     121              :  * `reserves_in` row, the way exchange_do_purse_merge() does.
     122              :  *
     123              :  * @param reserve_pub public key of the reserve to create
     124              :  * @return SQL statement, to be freed by the caller
     125              :  */
     126              : static char *
     127            4 : hex_insert_reserve (const struct TALER_ReservePublicKeyP *reserve_pub)
     128              : {
     129              :   char hex[sizeof (*reserve_pub) * 2 + 1];
     130            4 :   const unsigned char *raw = (const unsigned char *) reserve_pub;
     131              :   char *sql;
     132              : 
     133          132 :   for (unsigned int i = 0; i<sizeof (*reserve_pub); i++)
     134          128 :     GNUNET_snprintf (&hex[i * 2],
     135              :                      3,
     136              :                      "%02x",
     137          128 :                      raw[i]);
     138            4 :   GNUNET_asprintf (&sql,
     139              :                    "INSERT INTO reserves"
     140              :                    " (reserve_pub,current_balance,expiration_date,gc_date)"
     141              :                    " VALUES"
     142              :                    " (decode('%s','hex')"
     143              :                    " ,ROW(0,0)::taler_amount"
     144              :                    " ,1770000000000000"
     145              :                    " ,1780000000000000);",
     146              :                    hex);
     147            4 :   return sql;
     148              : }
     149              : 
     150              : 
     151              : /**
     152              :  * Render @a data as a lowercase hex string for use in an SQL literal.
     153              :  *
     154              :  * @param data binary data
     155              :  * @param size number of bytes in @a data
     156              :  * @return hex string, to be freed by the caller
     157              :  */
     158              : static char *
     159            6 : to_hex (const void *data,
     160              :         size_t size)
     161              : {
     162            6 :   const unsigned char *raw = data;
     163              :   char *hex;
     164              : 
     165            6 :   hex = GNUNET_malloc (size * 2 + 1);
     166          198 :   for (size_t i = 0; i<size; i++)
     167          192 :     GNUNET_snprintf (&hex[i * 2],
     168              :                      3,
     169              :                      "%02x",
     170          192 :                      raw[i]);
     171            6 :   return hex;
     172              : }
     173              : 
     174              : 
     175              : /**
     176              :  * E-2: a merchant may refund more than (deposit - deposit fee).  The
     177              :  * aggregator used to charge the full deposit fee on top of such a refund,
     178              :  * making the amount to be wired out negative, and then abort() on the
     179              :  * GNUNET_assert() guarding the subtraction -- stopping every payout for the
     180              :  * shard, and doing so again on every restart.
     181              :  *
     182              :  * Deposit EUR:1.00 of a denomination with a EUR:0.10 deposit fee, refund
     183              :  * EUR:0.95 of it, and require the aggregation to come out at EUR:0.
     184              :  */
     185              : static int
     186            1 : check_aggregate_refund_below_deposit_fee (void)
     187              : {
     188              :   struct TALER_MerchantPublicKeyP merchant_pub;
     189              :   struct TALER_FullPaytoHashP h_payto;
     190              :   struct TALER_NormalizedPaytoHashP h_norm;
     191              :   struct TALER_CoinSpendPublicKeyP coin_pub;
     192              :   struct TALER_WireTransferIdentifierRawP wtid;
     193              :   struct TALER_Amount total;
     194              :   struct TALER_Amount zero;
     195              :   char *sql;
     196              :   char *m_hex;
     197              :   char *p_hex;
     198              :   char *n_hex;
     199              :   char *c_hex;
     200              :   enum GNUNET_GenericReturnValue ok;
     201              : 
     202            1 :   parse_amount ("0",
     203              :                 &zero);
     204            1 :   memset (&merchant_pub,
     205              :           0x41,
     206              :           sizeof (merchant_pub));
     207            1 :   memset (&h_payto,
     208              :           0x42,
     209              :           sizeof (h_payto));
     210            1 :   memset (&h_norm,
     211              :           0x43,
     212              :           sizeof (h_norm));
     213            1 :   memset (&coin_pub,
     214              :           0x44,
     215              :           sizeof (coin_pub));
     216            1 :   memset (&wtid,
     217              :           0x45,
     218              :           sizeof (wtid));
     219            1 :   m_hex = to_hex (&merchant_pub,
     220              :                   sizeof (merchant_pub));
     221            1 :   p_hex = to_hex (&h_payto,
     222              :                   sizeof (h_payto));
     223            1 :   n_hex = to_hex (&h_norm,
     224              :                   sizeof (h_norm));
     225            1 :   c_hex = to_hex (&coin_pub,
     226              :                   sizeof (coin_pub));
     227            1 :   GNUNET_asprintf (
     228              :     &sql,
     229              :     "INSERT INTO kyc_targets (h_normalized_payto)"
     230              :     " VALUES (decode('%s','hex')) ON CONFLICT DO NOTHING;"
     231              :     "INSERT INTO wire_targets"
     232              :     " (wire_target_h_payto,payto_uri,h_normalized_payto)"
     233              :     " VALUES (decode('%s','hex'),'payto://x-taler-bank/h/e2',"
     234              :     "         decode('%s','hex')) ON CONFLICT DO NOTHING;"
     235              :     "INSERT INTO denominations"
     236              :     " (denom_pub_hash,denom_type,age_mask,denom_pub,master_sig"
     237              :     " ,valid_from,expire_withdraw,expire_deposit,expire_legal"
     238              :     " ,coin,fee_withdraw,fee_deposit,fee_refresh,fee_refund)"
     239              :     " VALUES (decode(repeat('e2',64),'hex'),1,0,decode('00','hex')"
     240              :     "        ,decode(repeat('00',64),'hex'),0,0,0,0"
     241              :     "        ,ROW(1,0)::taler_amount,ROW(0,0)::taler_amount"
     242              :     "        ,ROW(0,10000000)::taler_amount,ROW(0,0)::taler_amount"
     243              :     "        ,ROW(0,0)::taler_amount);"
     244              :     "INSERT INTO known_coins"
     245              :     " (denominations_serial,coin_pub,denom_sig,remaining)"
     246              :     " VALUES ((SELECT denominations_serial FROM denominations"
     247              :     "           WHERE denom_pub_hash=decode(repeat('e2',64),'hex'))"
     248              :     "        ,decode('%s','hex'),decode('00','hex'),ROW(0,0)::taler_amount);"
     249              :     "INSERT INTO batch_deposits"
     250              :     " (shard,merchant_pub,wallet_timestamp,exchange_timestamp"
     251              :     " ,refund_deadline,wire_deadline,h_contract_terms,wire_salt"
     252              :     " ,wire_target_h_payto,policy_blocked,total_amount,merchant_sig"
     253              :     " ,done,total_without_fee)"
     254              :     " VALUES (%llu,decode('%s','hex'),0,0,1,1"
     255              :     "        ,decode(repeat('e2',64),'hex'),decode(repeat('e2',16),'hex')"
     256              :     "        ,decode('%s','hex'),FALSE,ROW(1,0)::taler_amount"
     257              :     "        ,decode(repeat('00',64),'hex'),FALSE"
     258              :     "        ,ROW(0,90000000)::taler_amount);"
     259              :     "INSERT INTO coin_deposits"
     260              :     " (batch_deposit_serial_id,coin_pub,coin_sig,amount_with_fee)"
     261              :     " VALUES ((SELECT batch_deposit_serial_id FROM batch_deposits"
     262              :     "           WHERE merchant_pub=decode('%s','hex'))"
     263              :     "        ,decode('%s','hex'),decode(repeat('e2',64),'hex')"
     264              :     "        ,ROW(1,0)::taler_amount);"
     265              :     "INSERT INTO refunds"
     266              :     " (coin_pub,batch_deposit_serial_id,merchant_sig,rtransaction_id"
     267              :     " ,amount_with_fee)"
     268              :     " VALUES (decode('%s','hex')"
     269              :     "        ,(SELECT batch_deposit_serial_id FROM batch_deposits"
     270              :     "           WHERE merchant_pub=decode('%s','hex'))"
     271              :     "        ,decode(repeat('00',64),'hex'),1"
     272              :     "        ,ROW(0,95000000)::taler_amount);",
     273              :     n_hex,
     274              :     p_hex,
     275              :     n_hex,
     276              :     c_hex,
     277            1 :     (unsigned long long) TALER_EXCHANGEDB_compute_shard (&merchant_pub),
     278              :     m_hex,
     279              :     p_hex,
     280              :     m_hex,
     281              :     c_hex,
     282              :     c_hex,
     283              :     m_hex);
     284            1 :   ok = exec_sql (sql);
     285            1 :   GNUNET_free (sql);
     286            1 :   GNUNET_free (m_hex);
     287            1 :   GNUNET_free (p_hex);
     288            1 :   GNUNET_free (n_hex);
     289            1 :   GNUNET_free (c_hex);
     290            1 :   FAILIF (GNUNET_OK != ok);
     291              : 
     292              :   /* Before the fix this abort()ed inside TALER_EXCHANGEDB_do_aggregate(). */
     293            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     294              :           TALER_EXCHANGEDB_do_aggregate (pg,
     295              :                                          &h_payto,
     296              :                                          &merchant_pub,
     297              :                                          &wtid,
     298              :                                          &total));
     299            1 :   FAILIF (0 !=
     300              :           TALER_amount_cmp (&zero,
     301              :                             &total));
     302            1 :   return 0;
     303              : }
     304              : 
     305              : 
     306              : /**
     307              :  * E-13: PostgreSQL accepts COMMIT on a transaction it has already aborted,
     308              :  * rolls it back and answers with the command tag ROLLBACK and no error at
     309              :  * all.  TALER_EXCHANGEDB_commit() used to pass that straight through as
     310              :  * GNUNET_DB_STATUS_SUCCESS_NO_RESULTS, i.e. every caller was told the
     311              :  * transaction had committed while its writes were gone.
     312              :  */
     313              : static int
     314            1 : check_commit_detects_rolled_back_transaction (void)
     315              : {
     316              :   /* A transaction that is aborted mid-way must NOT commit successfully. */
     317            1 :   FAILIF (GNUNET_OK !=
     318              :           TALER_EXCHANGEDB_start (pg,
     319              :                                   "test-e13-aborted"));
     320            1 :   FAILIF (GNUNET_OK !=
     321              :           exec_sql ("INSERT INTO kyc_targets (h_normalized_payto)"
     322              :                     " VALUES (decode(repeat('13',32),'hex'));"));
     323              :   /* Provoke an error; from here on the transaction is doomed. */
     324            1 :   FAILIF (GNUNET_OK ==
     325              :           exec_sql ("SELECT 1/0;"));
     326              :   /* Before the fix this returned SUCCESS_NO_RESULTS (0). */
     327            1 :   FAILIF (0 <=
     328              :           TALER_EXCHANGEDB_commit (pg));
     329              :   /* ...and the row is indeed gone, so 'success' would have been a lie. */
     330            1 :   FAILIF (GNUNET_OK !=
     331              :           exec_sql ("DO $$ BEGIN"
     332              :                     " IF EXISTS (SELECT FROM kyc_targets"
     333              :                     "             WHERE h_normalized_payto"
     334              :                     "                   =decode(repeat('13',32),'hex'))"
     335              :                     " THEN RAISE EXCEPTION 'row survived a rollback';"
     336              :                     " END IF; END $$;"));
     337              : 
     338              :   /* A clean transaction must still commit and still return exactly 0. */
     339            1 :   FAILIF (GNUNET_OK !=
     340              :           TALER_EXCHANGEDB_start (pg,
     341              :                                   "test-e13-clean"));
     342            1 :   FAILIF (GNUNET_OK !=
     343              :           exec_sql ("INSERT INTO kyc_targets (h_normalized_payto)"
     344              :                     " VALUES (decode(repeat('14',32),'hex'));"));
     345            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     346              :           TALER_EXCHANGEDB_commit (pg));
     347            1 :   FAILIF (GNUNET_OK !=
     348              :           exec_sql ("DO $$ BEGIN"
     349              :                     " IF NOT EXISTS (SELECT FROM kyc_targets"
     350              :                     "                 WHERE h_normalized_payto"
     351              :                     "                       =decode(repeat('14',32),'hex'))"
     352              :                     " THEN RAISE EXCEPTION 'committed row is missing';"
     353              :                     " END IF; END $$;"));
     354            1 :   return 0;
     355              : }
     356              : 
     357              : 
     358              : /**
     359              :  * Helper for the reserve-open checks: create a reserve with the given
     360              :  * balance and run exchange_do_reserve_open() on it.
     361              :  *
     362              :  * @param reserve_pub reserve to create and open
     363              :  * @param desired_expiration expiration the client asks for
     364              :  * @param now current time to assume
     365              :  * @param min_purse_limit number of purses the client asks for
     366              :  * @param open_fee annual account fee of the exchange
     367              :  * @param[out] open_cost set to the cost the exchange computed
     368              :  * @return transaction status
     369              :  */
     370              : static enum GNUNET_DB_QueryStatus
     371            3 : try_reserve_open (const struct TALER_ReservePublicKeyP *reserve_pub,
     372              :                   struct GNUNET_TIME_Timestamp desired_expiration,
     373              :                   struct GNUNET_TIME_Timestamp now,
     374              :                   uint32_t min_purse_limit,
     375              :                   const struct TALER_Amount *open_fee,
     376              :                   struct TALER_Amount *open_cost)
     377              : {
     378              :   struct TALER_ReserveSignatureP reserve_sig;
     379              :   struct TALER_Amount zero;
     380              :   struct TALER_Amount balance;
     381              :   struct GNUNET_TIME_Timestamp final_expiration;
     382              :   bool no_funds;
     383              :   char *sql;
     384              : 
     385            3 :   memset (&reserve_sig,
     386              :           0x51,
     387              :           sizeof (reserve_sig));
     388            3 :   parse_amount ("0",
     389              :                 &zero);
     390            3 :   sql = hex_insert_reserve (reserve_pub);
     391            3 :   GNUNET_assert (GNUNET_OK ==
     392              :                  exec_sql (sql));
     393            3 :   GNUNET_free (sql);
     394            3 :   return TALER_EXCHANGEDB_do_reserve_open (pg,
     395              :                                            reserve_pub,
     396              :                                            &zero,
     397              :                                            &zero,
     398              :                                            min_purse_limit,
     399              :                                            &reserve_sig,
     400              :                                            desired_expiration,
     401              :                                            now,
     402              :                                            open_fee,
     403              :                                            &no_funds,
     404              :                                            &balance,
     405              :                                            open_cost,
     406              :                                            &final_expiration);
     407              : }
     408              : 
     409              : 
     410              : /**
     411              :  * E-3: `{"reserve_expiration":"never"}` is a perfectly well-formed request:
     412              :  * GNUNET_JSON_spec_timestamp() turns it into GNUNET_TIME_UNIT_FOREVER_ABS and
     413              :  * qconv_abs_time() clamps that to INT64_MAX.  The stored procedure then
     414              :  * overflowed INT8 computing the number of years, which SQLSTATE 22003 turns
     415              :  * into a hard error and the handler into an HTTP 500.
     416              :  */
     417              : static int
     418            1 : check_reserve_open_never_expires (void)
     419              : {
     420              :   struct TALER_ReservePublicKeyP reserve_pub;
     421              :   struct TALER_Amount open_fee;
     422              :   struct TALER_Amount open_cost;
     423              : 
     424            1 :   memset (&reserve_pub,
     425              :           0x33,
     426              :           sizeof (reserve_pub));
     427              :   /* No fractional part here; that is E-4's business. */
     428            1 :   parse_amount ("1",
     429              :                 &open_fee);
     430              :   /* Before the fix: 'bigint out of range' -> HARD_ERROR. */
     431            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     432              :           try_reserve_open (&reserve_pub,
     433              :                             GNUNET_TIME_UNIT_FOREVER_TS,
     434              :                             GNUNET_TIME_timestamp_get (),
     435              :                             1,
     436              :                             &open_fee,
     437              :                             &open_cost));
     438            1 :   return 0;
     439              : }
     440              : 
     441              : 
     442              : /**
     443              :  * E-4: `my_years * in_open_fee.frac` was an INT4 multiplication that
     444              :  * overflowed *before* the division, so the author's own overflow guard could
     445              :  * never be reached: an exchange whose ACCOUNT_FEE has a fractional part
     446              :  * answered a far-future reserve-open request with an HTTP 500.
     447              :  *
     448              :  * The same block computed the new purse limit in INT4 from the client's
     449              :  * `purse_limit`, which overflows for any value near INT32_MAX.
     450              :  */
     451              : static int
     452            1 : check_reserve_open_int4_overflows (void)
     453              : {
     454              :   struct TALER_ReservePublicKeyP reserve_pub;
     455              :   struct TALER_Amount open_fee;
     456              :   struct TALER_Amount open_cost;
     457              :   struct TALER_Amount expected;
     458              :   struct GNUNET_TIME_Timestamp now;
     459              : 
     460            1 :   parse_amount ("0.5",
     461              :                 &open_fee);
     462            1 :   parse_amount ("25",
     463              :                 &expected);
     464              : 
     465              :   /* 50 years at EUR:0.50/year: 50 * 50000000 does not fit into an INT4. */
     466            1 :   memset (&reserve_pub,
     467              :           0x34,
     468              :           sizeof (reserve_pub));
     469            1 :   now = GNUNET_TIME_timestamp_get ();
     470            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     471              :           try_reserve_open (&reserve_pub,
     472              :                             GNUNET_TIME_absolute_to_timestamp (
     473              :                               GNUNET_TIME_absolute_add (
     474              :                                 now.abs_time,
     475              :                                 GNUNET_TIME_relative_multiply (
     476              :                                   GNUNET_TIME_UNIT_YEARS,
     477              :                                   50))),
     478              :                             now,
     479              :                             1,
     480              :                             &open_fee,
     481              :                             &open_cost));
     482            1 :   FAILIF (0 !=
     483              :           TALER_amount_cmp (&expected,
     484              :                             &open_cost));
     485              : 
     486              :   /* An absurd purse_limit must be priced out of range, not overflow. */
     487            1 :   memset (&reserve_pub,
     488              :           0x35,
     489              :           sizeof (reserve_pub));
     490            1 :   now = GNUNET_TIME_timestamp_get ();
     491            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     492              :           try_reserve_open (&reserve_pub,
     493              :                             now,
     494              :                             now,
     495              :                             2147483647,
     496              :                             &open_fee,
     497              :                             &open_cost));
     498            1 :   return 0;
     499              : }
     500              : 
     501              : 
     502              : /**
     503              :  * E-5: `known_coins.age_commitment_hash` is NULL for every coin without an
     504              :  * age commitment -- the common case -- but the result spec read it without
     505              :  * GNUNET_PQ_result_spec_allow_null(), so building the 409 conflict proof for
     506              :  * POST /purses/$PURSE_PUB/deposit failed with an HTTP 500 instead.
     507              :  */
     508              : static int
     509            1 : check_purse_deposit_without_age_commitment (void)
     510              : {
     511              :   struct TALER_PurseContractPublicKeyP purse_pub;
     512              :   struct TALER_CoinSpendPublicKeyP coin_pub;
     513              :   struct TALER_Amount amount;
     514              :   struct TALER_DenominationHashP h_denom_pub;
     515              :   struct TALER_AgeCommitmentHashP hac;
     516              :   struct TALER_CoinSpendSignatureP coin_sig;
     517              :   bool no_age_commitment;
     518            1 :   char *partner_url = NULL;
     519              :   char *sql;
     520              :   char *p_hex;
     521              :   char *c_hex;
     522              :   enum GNUNET_GenericReturnValue ok;
     523              : 
     524            1 :   memset (&purse_pub,
     525              :           0x55,
     526              :           sizeof (purse_pub));
     527            1 :   memset (&coin_pub,
     528              :           0x56,
     529              :           sizeof (coin_pub));
     530            1 :   p_hex = to_hex (&purse_pub,
     531              :                   sizeof (purse_pub));
     532            1 :   c_hex = to_hex (&coin_pub,
     533              :                   sizeof (coin_pub));
     534            1 :   GNUNET_asprintf (
     535              :     &sql,
     536              :     "INSERT INTO denominations"
     537              :     " (denom_pub_hash,denom_type,age_mask,denom_pub,master_sig"
     538              :     " ,valid_from,expire_withdraw,expire_deposit,expire_legal"
     539              :     " ,coin,fee_withdraw,fee_deposit,fee_refresh,fee_refund)"
     540              :     " VALUES (decode(repeat('e5',64),'hex'),1,0,decode('00','hex')"
     541              :     "        ,decode(repeat('00',64),'hex'),0,0,0,0"
     542              :     "        ,ROW(1,0)::taler_amount,ROW(0,0)::taler_amount"
     543              :     "        ,ROW(0,0)::taler_amount,ROW(0,0)::taler_amount"
     544              :     "        ,ROW(0,0)::taler_amount);"
     545              :     /* age_commitment_hash deliberately left NULL */
     546              :     "INSERT INTO known_coins"
     547              :     " (denominations_serial,coin_pub,denom_sig,remaining)"
     548              :     " VALUES ((SELECT denominations_serial FROM denominations"
     549              :     "           WHERE denom_pub_hash=decode(repeat('e5',64),'hex'))"
     550              :     "        ,decode('%s','hex'),decode('00','hex')"
     551              :     "        ,ROW(0,0)::taler_amount);"
     552              :     "INSERT INTO purse_deposits"
     553              :     " (purse_pub,coin_pub,amount_with_fee,coin_sig)"
     554              :     " VALUES (decode('%s','hex'),decode('%s','hex')"
     555              :     "        ,ROW(1,0)::taler_amount,decode(repeat('e5',64),'hex'));",
     556              :     c_hex,
     557              :     p_hex,
     558              :     c_hex);
     559            1 :   ok = exec_sql (sql);
     560            1 :   GNUNET_free (sql);
     561            1 :   GNUNET_free (p_hex);
     562            1 :   GNUNET_free (c_hex);
     563            1 :   FAILIF (GNUNET_OK != ok);
     564              : 
     565              :   /* Before the fix: HARD_ERROR from the failed NULL extraction. */
     566            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     567              :           TALER_EXCHANGEDB_get_purse_deposit (pg,
     568              :                                               &purse_pub,
     569              :                                               &coin_pub,
     570              :                                               &amount,
     571              :                                               &h_denom_pub,
     572              :                                               &hac,
     573              :                                               &no_age_commitment,
     574              :                                               &coin_sig,
     575              :                                               &partner_url));
     576            1 :   GNUNET_free (partner_url);
     577            1 :   FAILIF (! no_age_commitment);
     578            1 :   return 0;
     579              : }
     580              : 
     581              : 
     582              : /**
     583              :  * E-6: a reserve funded by a purse merge has no `reserves_in` row, so the
     584              :  * LEFT JOIN in get_reserve_close_info() returns a NULL payto_uri.  Without
     585              :  * allow_null the extraction failed and POST /reserves/$RP/close answered 500
     586              :  * instead of the 409 the handler already implements.
     587              :  */
     588              : static int
     589            1 : check_reserve_close_info_without_origin (void)
     590              : {
     591              :   struct TALER_ReservePublicKeyP reserve_pub;
     592              :   struct TALER_Amount balance;
     593              :   struct TALER_FullPayto payto_uri;
     594              :   char *sql;
     595              :   enum GNUNET_GenericReturnValue ok;
     596              : 
     597            1 :   memset (&reserve_pub,
     598              :           0x36,
     599              :           sizeof (reserve_pub));
     600            1 :   sql = hex_insert_reserve (&reserve_pub);
     601            1 :   ok = exec_sql (sql);
     602            1 :   GNUNET_free (sql);
     603            1 :   FAILIF (GNUNET_OK != ok);
     604              : 
     605              :   /* Before the fix: HARD_ERROR from the failed NULL extraction. */
     606            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     607              :           TALER_EXCHANGEDB_get_reserve_close_info (pg,
     608              :                                                    &reserve_pub,
     609              :                                                    &balance,
     610              :                                                    &payto_uri));
     611            1 :   FAILIF (NULL != payto_uri.full_payto);
     612            1 :   return 0;
     613              : }
     614              : 
     615              : 
     616              : /**
     617              :  * Read back the bookkeeping of one work shard.
     618              :  *
     619              :  * @param job_name job the shard belongs to
     620              :  * @param start_row inclusive start row of the shard
     621              :  * @param end_row exclusive end row of the shard
     622              :  * @param[out] progress_row how far the shard has come
     623              :  * @param[out] completed whether the shard is done
     624              :  * @return transaction status code
     625              :  */
     626              : static enum GNUNET_DB_QueryStatus
     627            4 : get_shard_state (const char *job_name,
     628              :                  uint64_t start_row,
     629              :                  uint64_t end_row,
     630              :                  uint64_t *progress_row,
     631              :                  bool *completed)
     632              : {
     633            4 :   struct GNUNET_PQ_QueryParam params[] = {
     634            4 :     GNUNET_PQ_query_param_string (job_name),
     635            4 :     GNUNET_PQ_query_param_uint64 (&start_row),
     636            4 :     GNUNET_PQ_query_param_uint64 (&end_row),
     637              :     GNUNET_PQ_query_param_end
     638              :   };
     639            4 :   struct GNUNET_PQ_ResultSpec rs[] = {
     640            4 :     GNUNET_PQ_result_spec_uint64 ("progress_row",
     641              :                                   progress_row),
     642            4 :     GNUNET_PQ_result_spec_bool ("completed",
     643              :                                 completed),
     644              :     GNUNET_PQ_result_spec_end
     645              :   };
     646              : 
     647            4 :   PREPARE (pg,
     648              :            "test_get_shard_state",
     649              :            "SELECT"
     650              :            " progress_row"
     651              :            ",completed"
     652              :            " FROM work_shards"
     653              :            " WHERE job_name=$1"
     654              :            "   AND start_row=$2"
     655              :            "   AND end_row=$3;");
     656            4 :   return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
     657              :                                                    "test_get_shard_state",
     658              :                                                    params,
     659              :                                                    rs);
     660              : }
     661              : 
     662              : 
     663              : /**
     664              :  * A worker that gets part of the way through a shard and then stops used to
     665              :  * leave nothing behind: the shard was either untouched or completed, so the
     666              :  * next worker to pick it up redid all of it, and nothing it had imported was
     667              :  * visible until the whole shard was done.  Check that the progress marker is
     668              :  * kept, that it survives releasing the shard, that it never moves backwards,
     669              :  * and that reaching the end of the shard is what completes it.
     670              :  */
     671              : static int
     672            1 : check_shard_progress_survives_abort (void)
     673              : {
     674            1 :   const char *job = "test-shard-progress";
     675              :   uint64_t start;
     676              :   uint64_t end;
     677              :   uint64_t progress;
     678              :   uint64_t start2;
     679              :   uint64_t end2;
     680              :   uint64_t progress2;
     681              :   bool completed;
     682              : 
     683            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     684              :           TALER_EXCHANGEDB_begin_shard (pg,
     685              :                                         job,
     686              :                                         GNUNET_TIME_UNIT_HOURS,
     687              :                                         1024,
     688              :                                         &start,
     689              :                                         &end,
     690              :                                         &progress));
     691              :   /* A fresh shard has nothing done yet. */
     692            1 :   FAILIF (progress != start);
     693              : 
     694              :   /* Get half way, then let go of the shard. */
     695            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     696              :           TALER_EXCHANGEDB_update_shard_progress (pg,
     697              :                                                   job,
     698              :                                                   start,
     699              :                                                   end,
     700              :                                                   start + 512,
     701              :                                                   GNUNET_TIME_UNIT_HOURS));
     702            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     703              :           TALER_EXCHANGEDB_abort_shard (pg,
     704              :                                         job,
     705              :                                         start,
     706              :                                         end));
     707              : 
     708              :   /* The next worker gets the same shard back, but resumes in the middle. */
     709            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     710              :           TALER_EXCHANGEDB_begin_shard (pg,
     711              :                                         job,
     712              :                                         GNUNET_TIME_UNIT_HOURS,
     713              :                                         1024,
     714              :                                         &start2,
     715              :                                         &end2,
     716              :                                         &progress2));
     717            1 :   FAILIF (start2 != start);
     718            1 :   FAILIF (end2 != end);
     719            1 :   FAILIF (progress2 != start + 512);
     720              : 
     721              :   /* A straggler reporting older progress must not rewind the shard. */
     722            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     723              :           TALER_EXCHANGEDB_update_shard_progress (pg,
     724              :                                                   job,
     725              :                                                   start,
     726              :                                                   end,
     727              :                                                   start + 1,
     728              :                                                   GNUNET_TIME_UNIT_HOURS));
     729            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     730              :           get_shard_state (job,
     731              :                            start,
     732              :                            end,
     733              :                            &progress,
     734              :                            &completed));
     735            1 :   FAILIF (progress != start + 512);
     736            1 :   FAILIF (completed);
     737              : 
     738              :   /* Reaching the end completes the shard; there is no second statement for
     739              :      the caller to forget, or to crash before. */
     740            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     741              :           TALER_EXCHANGEDB_update_shard_progress (pg,
     742              :                                                   job,
     743              :                                                   start,
     744              :                                                   end,
     745              :                                                   end,
     746              :                                                   GNUNET_TIME_UNIT_HOURS));
     747            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     748              :           get_shard_state (job,
     749              :                            start,
     750              :                            end,
     751              :                            &progress,
     752              :                            &completed));
     753            1 :   FAILIF (progress != end);
     754            1 :   FAILIF (! completed);
     755            1 :   return 0;
     756              : }
     757              : 
     758              : 
     759              : /**
     760              :  * Importing a batch of incoming wire transfers and recording how far the
     761              :  * shard has come is one statement, so a crash cannot land one without the
     762              :  * other.  Check that a batch moves both, that importing it again is harmless
     763              :  * and reported as a duplicate, and that the shard completes as part of the
     764              :  * import rather than in a transaction of its own.
     765              :  */
     766              : static int
     767            1 : check_import_credits_advances_shard (void)
     768              : {
     769            1 :   const char *job = "test-import-credits";
     770              :   struct TALER_ReservePublicKeyP reserve_pub;
     771              :   struct TALER_Amount balance;
     772              :   struct TALER_EXCHANGEDB_ReserveInInfo reserve;
     773              :   struct TALER_EXCHANGEDB_CreditBatch batch;
     774              :   enum GNUNET_DB_QueryStatus results[1];
     775              :   uint64_t start;
     776              :   uint64_t end;
     777              :   uint64_t progress;
     778              :   bool completed;
     779              : 
     780            1 :   memset (&reserve_pub,
     781              :           0x51,
     782              :           sizeof (reserve_pub));
     783            1 :   parse_amount ("4.00",
     784              :                 &balance);
     785            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     786              :           TALER_EXCHANGEDB_begin_shard (pg,
     787              :                                         job,
     788              :                                         GNUNET_TIME_UNIT_HOURS,
     789              :                                         1024,
     790              :                                         &start,
     791              :                                         &end,
     792              :                                         &progress));
     793            1 :   reserve.reserve_pub = &reserve_pub;
     794            1 :   reserve.balance = &balance;
     795            1 :   reserve.execution_time = GNUNET_TIME_timestamp_get ();
     796              :   reserve.sender_account_details.full_payto
     797            1 :     = (char *) "payto://x-taler-bank/localhost/shard-test?receiver-name=Shard";
     798            1 :   reserve.wire_reference = start + 1;
     799            1 :   memset (&batch,
     800              :           0,
     801              :           sizeof (batch));
     802            1 :   batch.exchange_account_name = "exchange-account-test";
     803            1 :   batch.reserves = &reserve;
     804            1 :   batch.reserves_length = 1;
     805            1 :   batch.job_name = job;
     806            1 :   batch.shard_start = start;
     807            1 :   batch.shard_end = end;
     808            1 :   batch.progress_row = start + 1;
     809            1 :   batch.lease = GNUNET_TIME_UNIT_HOURS;
     810              : 
     811            1 :   FAILIF (0 >
     812              :           TALER_EXCHANGEDB_do_import_credits (pg,
     813              :                                               &batch,
     814              :                                               results));
     815            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != results[0]);
     816            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     817              :           get_shard_state (job,
     818              :                            start,
     819              :                            end,
     820              :                            &progress,
     821              :                            &completed));
     822              :   /* The transfer is committed and so is the fact that we consumed its row --
     823              :      long before the rest of the shard has been looked at. */
     824            1 :   FAILIF (progress != start + 1);
     825            1 :   FAILIF (completed);
     826              : 
     827              :   /* Re-importing the same batch is what happens whenever a shard is picked up
     828              :      twice.  It has to be harmless. */
     829            1 :   batch.progress_row = end;
     830            1 :   FAILIF (0 >
     831              :           TALER_EXCHANGEDB_do_import_credits (pg,
     832              :                                               &batch,
     833              :                                               results));
     834            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != results[0]);
     835            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     836              :           get_shard_state (job,
     837              :                            start,
     838              :                            end,
     839              :                            &progress,
     840              :                            &completed));
     841            1 :   FAILIF (progress != end);
     842            1 :   FAILIF (! completed);
     843            1 :   return 0;
     844              : }
     845              : 
     846              : 
     847              : /**
     848              :  * Statistics must return cumulative totals for ranges without their own
     849              :  * events, and age those events using the exchange process's clock.
     850              :  */
     851              : static int
     852            1 : check_statistics_time_travel (void)
     853              : {
     854            1 :   struct TALER_EXCHANGEDB_PostgresContext *original_pg = pg;
     855            1 :   long long original_offset = GNUNET_TIME_get_offset ();
     856            1 :   const long long future = 60LL * 24 * 60 * 60 * 1000000;
     857            1 :   int ret = 1;
     858              : 
     859              :   /* The ordinary connection starts without time travel. */
     860            1 :   FAILIF (GNUNET_OK != exec_sql (
     861              :             "SET TIME ZONE 'Europe/Berlin';"
     862              :             "DO $$ BEGIN "
     863              :             " ASSERT exchange_now() = (CURRENT_TIMESTAMP(0) AT TIME ZONE 'UTC');"
     864              :             "END $$;"
     865              :             "INSERT INTO exchange_statistic_interval_meta"
     866              :             " (origin,slug,description,stype,ranges,precisions) VALUES"
     867              :             " ('test','clock-amount','test','amount',"
     868              :             " ARRAY[604800,2419200,31449600], ARRAY[86400,86400,86400]),"
     869              :             " ('test','clock-number','test','number',"
     870              :             " ARRAY[604800,2419200,31449600], ARRAY[86400,86400,86400]);"
     871              :             "CALL exchange_do_bump_amount_stat('clock-amount',"
     872              :             " decode(repeat('ab',32),'hex'),exchange_now(),(1,25000000)::taler_amount);"
     873              :             "CALL exchange_do_bump_number_stat('clock-number',"
     874              :             " decode(repeat('ab',32),'hex'),exchange_now(),1);"
     875              :             "DO $$ BEGIN "
     876              :             " ASSERT (SELECT count(*) = 3 AND bool_and(rvalue = (1,25000000)::taler_amount)"
     877              :             " FROM exchange_statistic_interval_amount_get('clock-amount',decode(repeat('ab',32),'hex')));"
     878              :             " ASSERT (SELECT count(*) = 3 AND bool_and(rvalue = 1)"
     879              :             " FROM exchange_statistic_interval_number_get('clock-number',decode(repeat('ab',32),'hex')));"
     880              :             "END $$;"));
     881              : 
     882            1 :   GNUNET_TIME_set_offset (future);
     883            1 :   pg = TALER_EXCHANGEDB_connect_admin (original_pg->cfg);
     884            1 :   if (NULL == pg)
     885            0 :     goto cleanup;
     886            1 :   if (GNUNET_OK != exec_sql (
     887              :         "DO $$ BEGIN "
     888              :         " ASSERT exchange_now() = (CURRENT_TIMESTAMP(0) AT TIME ZONE 'UTC') + INTERVAL '60 days';"
     889              :         "END $$;"
     890              :         "CALL exchange_do_bump_amount_stat('clock-amount',"
     891              :         " decode(repeat('ab',32),'hex'),exchange_now(),(0,75000000)::taler_amount);"
     892              :         "CALL exchange_do_bump_number_stat('clock-number',"
     893              :         " decode(repeat('ab',32),'hex'),exchange_now(),1);"
     894              :         "DO $$ BEGIN "
     895              :         " ASSERT (SELECT count(*) = 3 AND bool_and(rvalue = CASE WHEN range < 31449600"
     896              :         " THEN (0,75000000)::taler_amount ELSE (2,0)::taler_amount END)"
     897              :         " FROM exchange_statistic_interval_amount_get('clock-amount',decode(repeat('ab',32),'hex')));"
     898              :         " ASSERT (SELECT count(*) = 3 AND bool_and(rvalue = CASE WHEN range < 31449600 THEN 1 ELSE 2 END)"
     899              :         " FROM exchange_statistic_interval_number_get('clock-number',decode(repeat('ab',32),'hex')));"
     900              :         "END $$;"))
     901            0 :     goto cleanup;
     902              : 
     903              :   /* Deliberately break this connection. The reconnect callback must restore
     904              :      the offset as well as the ordinary database session settings. */
     905            1 :   fprintf (stderr,
     906              :            "Deliberately terminating statistics test connection to test reconnect\n");
     907            1 :   if (GNUNET_SYSERR != exec_sql ("SELECT pg_terminate_backend(pg_backend_pid())"))
     908            0 :     goto cleanup;
     909            1 :   GNUNET_PQ_reconnect_if_down (pg->conn);
     910            1 :   if (GNUNET_OK != exec_sql (
     911              :         "DO $$ BEGIN "
     912              :         " ASSERT exchange_now() = (CURRENT_TIMESTAMP(0) AT TIME ZONE 'UTC') + INTERVAL '60 days';"
     913              :         "END $$;"
     914              :         /* A plain SQL client with no setting falls back to ordinary UTC time. */
     915              :         "SET TIME ZONE 'America/New_York';"
     916              :         "RESET taler.timetravel_us;"
     917              :         "DO $$ BEGIN "
     918              :         " ASSERT exchange_now() = (CURRENT_TIMESTAMP(0) AT TIME ZONE 'UTC');"
     919              :         "END $$;"))
     920            0 :     goto cleanup;
     921            1 :   ret = 0;
     922            1 : cleanup:
     923            1 :   if (NULL != pg)
     924            1 :     TALER_EXCHANGEDB_disconnect (pg);
     925            1 :   pg = original_pg;
     926            1 :   GNUNET_TIME_set_offset (original_offset);
     927            1 :   return ret;
     928              : }
     929              : 
     930              : 
     931              : /**
     932              :  * All checks we know about.
     933              :  */
     934              : static const struct
     935              : {
     936              :   const char *name;
     937              :   int (*fn)(void);
     938              : } tests[] = {
     939              :   { "statistics-time-travel",
     940              :     &check_statistics_time_travel },
     941              :   { "aggregate-refund-below-deposit-fee",
     942              :     &check_aggregate_refund_below_deposit_fee },
     943              :   { "commit-detects-rolled-back-transaction",
     944              :     &check_commit_detects_rolled_back_transaction },
     945              :   { "reserve-open-never-expires",
     946              :     &check_reserve_open_never_expires },
     947              :   { "reserve-open-int4-overflows",
     948              :     &check_reserve_open_int4_overflows },
     949              :   { "purse-deposit-without-age-commitment",
     950              :     &check_purse_deposit_without_age_commitment },
     951              :   { "reserve-close-info-without-origin",
     952              :     &check_reserve_close_info_without_origin },
     953              :   { "shard-progress-survives-abort",
     954              :     &check_shard_progress_survives_abort },
     955              :   { "import-credits-advances-shard",
     956              :     &check_import_credits_advances_shard },
     957              :   { NULL, NULL }
     958              : };
     959              : 
     960              : 
     961              : /**
     962              :  * Main function that runs the checks.
     963              :  *
     964              :  * @param cls closure
     965              :  * @param args remaining command-line arguments
     966              :  * @param cfgfile name of the configuration file used
     967              :  * @param cfg configuration
     968              :  */
     969              : static void
     970            1 : run (void *cls,
     971              :      char *const *args,
     972              :      const char *cfgfile,
     973              :      const struct GNUNET_CONFIGURATION_Handle *cfg)
     974              : {
     975            1 :   unsigned int ran = 0;
     976              : 
     977              :   (void) cls;
     978              :   (void) args;
     979              :   (void) cfgfile;
     980            1 :   pg = TALER_EXCHANGEDB_connect_admin (cfg);
     981            1 :   if (NULL == pg)
     982              :   {
     983            0 :     fprintf (stderr,
     984              :              "Failed to connect to the database\n");
     985            0 :     result = 77;
     986            0 :     return;
     987              :   }
     988            1 :   if (GNUNET_OK !=
     989            1 :       TALER_EXCHANGEDB_create_tables (pg,
     990              :                                       false,
     991              :                                       0))
     992              :   {
     993            0 :     fprintf (stderr,
     994              :              "Failed to create the database schema\n");
     995            0 :     result = 77;
     996            0 :     goto cleanup;
     997              :   }
     998           10 :   for (unsigned int i = 0; NULL != tests[i].name; i++)
     999              :   {
    1000            9 :     if ( (NULL != only) &&
    1001            0 :          (0 != strcmp (only,
    1002            0 :                        tests[i].name)) )
    1003            0 :       continue;
    1004            9 :     fprintf (stderr,
    1005              :              "Running check `%s'\n",
    1006            9 :              tests[i].name);
    1007            9 :     ran++;
    1008            9 :     if (0 != tests[i].fn ())
    1009              :     {
    1010            0 :       fprintf (stderr,
    1011              :                "Check `%s' FAILED\n",
    1012            0 :                tests[i].name);
    1013            0 :       result = 1;
    1014              :     }
    1015              :   }
    1016            1 :   if (0 == ran)
    1017              :   {
    1018            0 :     fprintf (stderr,
    1019              :              "No check matched `%s'\n",
    1020              :              only);
    1021            0 :     result = 1;
    1022              :   }
    1023            1 : cleanup:
    1024            1 :   TALER_EXCHANGEDB_disconnect (pg);
    1025            1 :   pg = NULL;
    1026              : }
    1027              : 
    1028              : 
    1029              : int
    1030            1 : main (int argc,
    1031              :       char *const *argv)
    1032              : {
    1033            1 :   struct GNUNET_GETOPT_CommandLineOption options[] = {
    1034            1 :     GNUNET_GETOPT_option_string ('t',
    1035              :                                  "test",
    1036              :                                  "NAME",
    1037              :                                  "only run the check called NAME",
    1038              :                                  &only),
    1039              :     GNUNET_GETOPT_OPTION_END
    1040              :   };
    1041              :   enum GNUNET_GenericReturnValue ret;
    1042              : 
    1043            1 :   result = 0;
    1044            1 :   ret = GNUNET_PROGRAM_run (TALER_EXCHANGE_project_data (),
    1045              :                             argc,
    1046              :                             argv,
    1047              :                             "test-regressions",
    1048              :                             "Regression tests for the exchange database layer",
    1049              :                             options,
    1050              :                             &run,
    1051              :                             NULL);
    1052            1 :   if (GNUNET_SYSERR == ret)
    1053            0 :     return 3;
    1054            1 :   if (GNUNET_NO == ret)
    1055            0 :     return 0;
    1056            1 :   return result;
    1057              : }
    1058              : 
    1059              : 
    1060              : /* end of test_regressions.c */
        

Generated by: LCOV version 2.0-1