commit 0d3bf26b491a5f97ff8c0f291f56b47370484db6
parent b82f45cd64f87ab7be99096bac9196e830f9a9dc
Author: Christian Grothoff <christian@grothoff.org>
Date: Sat, 10 Oct 2026 21:23:29 +0200
testing: run every auditor helper in test_auditor_api and fail on findings (TEST-4)
TALER_TESTING_cmd_exec_auditor() still exec'd the long-gone taler-auditor,
so test_auditor_api had all three calls commented out. The command now
runs each taler-helper-auditor-* once in test mode (-t) with -i directly
against the exchange database, then queries the /monitoring/ reports of
the taler-auditor-httpd that taler-unified-setup.sh -a keeps running for
the whole test, and fails if any of them is non-empty. The wire helpers
get -I, as the old command did.
Diffstat:
5 files changed, 413 insertions(+), 68 deletions(-)
diff --git a/src/include/taler/taler_testing_lib.h b/src/include/taler/taler_testing_lib.h
@@ -847,7 +847,11 @@ TALER_TESTING_cmd_transfer_retry (struct TALER_TESTING_Command cmd);
/**
- * Make the "exec-auditor" CMD.
+ * Make the "exec-auditor" CMD. Runs every taler-helper-auditor-*
+ * program once in test mode directly against the exchange database, and
+ * then fails unless the taler-auditor-httpd at the auditor's BASE_URL
+ * reports no findings. The auditor (and the bank, for the wire helpers)
+ * must be running.
*
* @param label command label.
* @param config_filename configuration filename.
diff --git a/src/testing/test_auditor_api-cs.conf b/src/testing/test_auditor_api-cs.conf
@@ -5,3 +5,14 @@
[exchange]
ENABLE_KYC = NO
+
+# The test only uses exchange-account-2. exchange-account-1 is the same
+# bank account, and exchange-account-3 one the bank does not have; the
+# auditor would compare the bank's history against both of them.
+[exchange-account-1]
+ENABLE_CREDIT = NO
+ENABLE_DEBIT = NO
+
+[exchange-account-3]
+ENABLE_CREDIT = NO
+ENABLE_DEBIT = NO
diff --git a/src/testing/test_auditor_api-rsa.conf b/src/testing/test_auditor_api-rsa.conf
@@ -5,3 +5,14 @@
[exchange]
ENABLE_KYC = NO
+
+# The test only uses exchange-account-2. exchange-account-1 is the same
+# bank account, and exchange-account-3 one the bank does not have; the
+# auditor would compare the bank's history against both of them.
+[exchange-account-1]
+ENABLE_CREDIT = NO
+ENABLE_DEBIT = NO
+
+[exchange-account-3]
+ENABLE_CREDIT = NO
+ENABLE_DEBIT = NO
diff --git a/src/testing/test_auditor_api.c b/src/testing/test_auditor_api.c
@@ -80,7 +80,7 @@ static struct TALER_TESTING_Credentials cred;
cred.user42_payto)
/**
- * Run the taler-auditor.
+ * Run all auditor helpers and check that the auditor has no findings.
*
* @param label label to use for the command.
*/
@@ -635,7 +635,7 @@ run (void *cls,
1,
"EUR:0.99",
MHD_HTTP_NO_CONTENT),
- // CMD_RUN_AUDITOR ("massive-auditor"),
+ CMD_RUN_AUDITOR ("massive-auditor"),
TALER_TESTING_cmd_end ()
};
@@ -659,7 +659,7 @@ run (void *cls,
true),
TALER_TESTING_cmd_exec_auditor_offline ("auditor-offline",
config_file),
- // CMD_RUN_AUDITOR ("virgin-auditor"),
+ CMD_RUN_AUDITOR ("virgin-auditor"),
TALER_TESTING_cmd_batch ("massive-deposit-confirms",
massive_deposit_confirms),
TALER_TESTING_cmd_batch ("withdraw",
@@ -676,7 +676,7 @@ run (void *cls,
refund),
TALER_TESTING_cmd_batch ("recoup",
recoup),
- // CMD_RUN_AUDITOR ("normal-auditor"),
+ CMD_RUN_AUDITOR ("normal-auditor"),
TALER_TESTING_cmd_end ()
};
diff --git a/src/testing/testing_api_cmd_auditor_exec_auditor.c b/src/testing/testing_api_cmd_auditor_exec_auditor.c
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- Copyright (C) 2018 Taler Systems SA
+ Copyright (C) 2018, 2026 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published
@@ -18,37 +18,343 @@
*/
/**
* @file testing/testing_api_cmd_auditor_exec_auditor.c
- * @brief run the taler-auditor command
+ * @brief run all taler-helper-auditor-* programs and check that
+ * taler-auditor-httpd reports no findings
* @author Marcello Stanisci
* @author Christian Grothoff
*/
-#include "platform.h" /* UNNECESSARY? */
+#include "platform.h"
#include "taler/taler_json_lib.h"
#include <gnunet/gnunet_curl_lib.h>
-#include "taler/taler_signatures.h" /* UNNECESSARY? */
#include "taler/taler_testing_lib.h"
/**
+ * Auditor helpers to run, in this order. Each is run in test mode
+ * ("-t"), so it audits everything the exchange database holds so far
+ * and then exits.
+ */
+static const struct
+{
+ /**
+ * Name of the helper binary.
+ */
+ const char *name;
+
+ /**
+ * True if the helper reads the bank's history of the exchange's
+ * accounts. Test configurations may list accounts the bank does
+ * not have, so those helpers are told to skip them ("-I").
+ */
+ bool wire;
+} helpers[] = {
+ { "taler-helper-auditor-aggregation", false },
+ { "taler-helper-auditor-aml", false },
+ { "taler-helper-auditor-coins", false },
+ { "taler-helper-auditor-deposits", false },
+ { "taler-helper-auditor-purses", false },
+ { "taler-helper-auditor-reserves", false },
+ { "taler-helper-auditor-transfer", false },
+ { "taler-helper-auditor-wire-credit", true },
+ { "taler-helper-auditor-wire-debit", true },
+ { NULL, false }
+};
+
+
+/**
+ * /monitoring/ reports of taler-auditor-httpd that must be empty after a
+ * correct exchange was audited. Not listed are the balances, progress
+ * and per-reserve/per-purse summaries, which are not findings, and
+ * "pending-deposits" and "aml-holds", which list payouts the exchange has
+ * not made *yet* (deposits before their wire deadline, transfers held
+ * for KYC) and thus depend on when the test last ran the aggregator.
+ */
+static const char *const reports[] = {
+ "amount-arithmetic-inconsistencies",
+ "bad-sig-losses",
+ "closure-lags",
+ "coin-inconsistencies",
+ "denomination-key-validity-withdraw-inconsistencies",
+ "denominations-without-sigs",
+ "deposit-confirmations",
+ "early-aggregations",
+ "emergencies",
+ "emergencies-by-count",
+ "fee-time-inconsistencies",
+ "kycauth-in-inconsistencies",
+ "misattribution-in-inconsistencies",
+ "purse-not-closed-inconsistencies",
+ "reserve-balance-insufficient-inconsistencies",
+ "reserve-balance-summary-wrong-inconsistencies",
+ "reserve-in-inconsistencies",
+ "reserve-not-closed-inconsistencies",
+ "row-inconsistencies",
+ "row-minor-inconsistencies",
+ "wire-format-inconsistencies",
+ "wire-out-inconsistencies",
+ NULL
+};
+
+
+/**
* State for a "auditor" CMD.
*/
struct AuditorState
{
/**
- * Process for the "auditor" command.
+ * Our interpreter.
*/
- struct GNUNET_Process *auditor_proc;
+ struct TALER_TESTING_Interpreter *is;
+
+ /**
+ * Process of the helper currently running.
+ */
+ struct GNUNET_Process *helper_proc;
+
+ /**
+ * Handle to wait for @e helper_proc to terminate.
+ */
+ struct GNUNET_ChildWaitHandle *cwh;
+
+ /**
+ * Task to start the next helper.
+ */
+ struct GNUNET_SCHEDULER_Task *task;
+
+ /**
+ * Request for a report, or NULL.
+ */
+ struct GNUNET_CURL_Job *job;
+
+ /**
+ * Headers to send with each request for a report.
+ */
+ struct curl_slist *job_headers;
/**
* Configuration file used by the command.
*/
const char *config_filename;
+
+ /**
+ * Base URL of the auditor, from @e config_filename.
+ */
+ char *auditor_url;
+
+ /**
+ * Index into #helpers of the helper currently running.
+ */
+ unsigned int helper_off;
+
+ /**
+ * Index into #reports of the report currently requested.
+ */
+ unsigned int report_off;
+
+ /**
+ * Number of reports that were not empty.
+ */
+ unsigned int findings;
};
/**
- * Run the command; calls the `taler-auditor` program.
+ * Request the next report from #reports, or finish the command
+ * if all of them were checked.
+ *
+ * @param as our state
+ */
+static void
+check_next_report (struct AuditorState *as);
+
+
+/**
+ * Called with a report from the auditor.
+ *
+ * @param cls our `struct AuditorState`
+ * @param response_code HTTP status returned by the auditor
+ * @param response response body, NULL if not in JSON
+ */
+static void
+report_cb (void *cls,
+ long response_code,
+ const void *response)
+{
+ struct AuditorState *as = cls;
+ const json_t *reply = response;
+ const char *report = reports[as->report_off];
+ const json_t *records;
+
+ as->job = NULL;
+ if (MHD_HTTP_OK != response_code)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Auditor returned HTTP status %u for report `%s'\n",
+ (unsigned int) response_code,
+ report);
+ TALER_TESTING_interpreter_fail (as->is);
+ return;
+ }
+ records = json_object_get (reply,
+ "records");
+ if (! json_is_array (records))
+ {
+ GNUNET_break_op (0);
+ TALER_TESTING_interpreter_fail (as->is);
+ return;
+ }
+ if (0 != json_array_size (records))
+ {
+ char *s;
+
+ s = json_dumps (records,
+ JSON_COMPACT);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Auditor reports %u %s: %s\n",
+ (unsigned int) json_array_size (records),
+ report,
+ s);
+ free (s);
+ as->findings++;
+ }
+ as->report_off++;
+ check_next_report (as);
+}
+
+
+static void
+check_next_report (struct AuditorState *as)
+{
+ CURL *eh;
+ char *url;
+
+ if (NULL == reports[as->report_off])
+ {
+ if (0 != as->findings)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Auditor has findings in %u reports\n",
+ as->findings);
+ TALER_TESTING_interpreter_fail (as->is);
+ return;
+ }
+ TALER_TESTING_interpreter_next (as->is);
+ return;
+ }
+ GNUNET_asprintf (&url,
+ "%smonitoring/%s",
+ as->auditor_url,
+ reports[as->report_off]);
+ eh = curl_easy_init ();
+ GNUNET_assert (NULL != eh);
+ GNUNET_assert (CURLE_OK ==
+ curl_easy_setopt (eh,
+ CURLOPT_URL,
+ url));
+ GNUNET_free (url);
+ as->job = GNUNET_CURL_job_add2 (TALER_TESTING_interpreter_get_context (
+ as->is),
+ eh,
+ as->job_headers,
+ &report_cb,
+ as);
+ if (NULL == as->job)
+ {
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (as->is);
+ return;
+ }
+}
+
+
+/**
+ * Start the next helper from #helpers, or start checking
+ * the reports if all helpers have run.
+ *
+ * @param cls our `struct AuditorState`
+ */
+static void
+run_next_helper (void *cls);
+
+
+/**
+ * Called when the helper we started has terminated.
+ *
+ * @param cls our `struct AuditorState`
+ * @param type how the helper terminated
+ * @param code exit code of the helper
+ */
+static void
+helper_done (void *cls,
+ enum GNUNET_OS_ProcessStatusType type,
+ long unsigned int code)
+{
+ struct AuditorState *as = cls;
+
+ as->cwh = NULL;
+ GNUNET_process_destroy (as->helper_proc);
+ as->helper_proc = NULL;
+ if ( (GNUNET_OS_PROCESS_EXITED != type) ||
+ (0 != code) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "%s failed (status %d, code %lu)\n",
+ helpers[as->helper_off].name,
+ (int) type,
+ code);
+ TALER_TESTING_interpreter_fail (as->is);
+ return;
+ }
+ as->helper_off++;
+ /* Not directly: GNUNET_wait_child() must not be called from
+ within a GNUNET_ChildCompletedCallback. */
+ as->task = GNUNET_SCHEDULER_add_now (&run_next_helper,
+ as);
+}
+
+
+static void
+run_next_helper (void *cls)
+{
+ struct AuditorState *as = cls;
+ const char *helper = helpers[as->helper_off].name;
+
+ as->task = NULL;
+ if (NULL == helper)
+ {
+ check_next_report (as);
+ return;
+ }
+ as->helper_proc = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR);
+ if (GNUNET_OK !=
+ GNUNET_process_run_command_va (as->helper_proc,
+ helper,
+ helper,
+ "-c", as->config_filename,
+ "-L", "INFO",
+ "-i", /* we audit the exchange DB directly */
+ "-t", /* exit when done */
+ helpers[as->helper_off].wire
+ ? "-I" /* ignore unknown accounts */
+ : NULL,
+ NULL))
+ {
+ GNUNET_break (0);
+ GNUNET_process_destroy (as->helper_proc);
+ as->helper_proc = NULL;
+ TALER_TESTING_interpreter_fail (as->is);
+ return;
+ }
+ as->cwh = GNUNET_wait_child (as->helper_proc,
+ &helper_done,
+ as);
+}
+
+
+/**
+ * Run the command; runs all auditor helpers against the exchange
+ * database and then asks the auditor for its findings.
*
* @param cls closure.
* @param cmd the commaind being run.
@@ -59,28 +365,53 @@ auditor_run (void *cls,
const struct TALER_TESTING_Command *cmd,
struct TALER_TESTING_Interpreter *is)
{
- struct AuditorState *ks = cls;
+ struct AuditorState *as = cls;
+ struct GNUNET_CONFIGURATION_Handle *cfg;
(void) cmd;
- // FIXME: taler-auditor is no more, run the individual
- // helpers here instead, or re-introduce the taler-auditor
- // command to run all helpers!
- ks->auditor_proc = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR);
+ as->is = is;
+ cfg = GNUNET_CONFIGURATION_create (TALER_AUDITOR_project_data ());
if (GNUNET_OK !=
- GNUNET_process_run_command_va (ks->auditor_proc,
- "taler-auditor",
- "taler-auditor",
- "-c", ks->config_filename,
- "-I",
- NULL))
+ GNUNET_CONFIGURATION_load (cfg,
+ as->config_filename))
{
GNUNET_break (0);
- GNUNET_process_destroy (ks->auditor_proc);
- ks->auditor_proc = NULL;
+ GNUNET_CONFIGURATION_destroy (cfg);
+ TALER_TESTING_interpreter_fail (is);
+ return;
+ }
+ if (GNUNET_OK !=
+ GNUNET_CONFIGURATION_get_value_string (cfg,
+ "auditor",
+ "BASE_URL",
+ &as->auditor_url))
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
+ "auditor",
+ "BASE_URL");
+ GNUNET_CONFIGURATION_destroy (cfg);
TALER_TESTING_interpreter_fail (is);
return;
}
- TALER_TESTING_wait_for_sigchld (is);
+ GNUNET_CONFIGURATION_destroy (cfg);
+ {
+ const char *tok = getenv ("TALER_AUDITOR_ACCESS_TOKEN");
+
+ /* taler-auditor-httpd requires the token iff it is set */
+ if (NULL != tok)
+ {
+ char *hdr;
+
+ GNUNET_asprintf (&hdr,
+ "%s: Bearer secret-token:%s",
+ MHD_HTTP_HEADER_AUTHORIZATION,
+ tok);
+ as->job_headers = curl_slist_append (as->job_headers,
+ hdr);
+ GNUNET_free (hdr);
+ }
+ }
+ run_next_helper (as);
}
@@ -95,50 +426,39 @@ static void
auditor_cleanup (void *cls,
const struct TALER_TESTING_Command *cmd)
{
- struct AuditorState *ks = cls;
+ struct AuditorState *as = cls;
(void) cmd;
- if (NULL != ks->auditor_proc)
+ if (NULL != as->task)
+ {
+ GNUNET_SCHEDULER_cancel (as->task);
+ as->task = NULL;
+ }
+ if (NULL != as->cwh)
+ {
+ GNUNET_wait_child_cancel (as->cwh);
+ as->cwh = NULL;
+ }
+ if (NULL != as->helper_proc)
{
GNUNET_break (GNUNET_OK ==
- GNUNET_process_kill (ks->auditor_proc,
+ GNUNET_process_kill (as->helper_proc,
SIGKILL));
- GNUNET_process_wait (ks->auditor_proc,
+ GNUNET_process_wait (as->helper_proc,
true,
NULL,
NULL);
- GNUNET_process_destroy (ks->auditor_proc);
- ks->auditor_proc = NULL;
+ GNUNET_process_destroy (as->helper_proc);
+ as->helper_proc = NULL;
}
- GNUNET_free (ks);
-}
-
-
-/**
- * Offer "auditor" CMD internal data to other commands.
- *
- * @param cls closure.
- * @param[out] ret result
- * @param trait name of the trait.
- * @param index index number of the object to offer.
- * @return #GNUNET_OK on success.
- */
-static enum GNUNET_GenericReturnValue
-auditor_traits (void *cls,
- const void **ret,
- const char *trait,
- unsigned int index)
-{
- struct AuditorState *ks = cls;
- struct TALER_TESTING_Trait traits[] = {
- TALER_TESTING_make_trait_process (&ks->auditor_proc),
- TALER_TESTING_trait_end ()
- };
-
- return TALER_TESTING_get_trait (traits,
- ret,
- trait,
- index);
+ if (NULL != as->job)
+ {
+ GNUNET_CURL_job_cancel (as->job);
+ as->job = NULL;
+ }
+ curl_slist_free_all (as->job_headers);
+ GNUNET_free (as->auditor_url);
+ GNUNET_free (as);
}
@@ -146,17 +466,16 @@ struct TALER_TESTING_Command
TALER_TESTING_cmd_exec_auditor (const char *label,
const char *config_filename)
{
- struct AuditorState *ks;
+ struct AuditorState *as;
- ks = GNUNET_new (struct AuditorState);
- ks->config_filename = config_filename;
+ as = GNUNET_new (struct AuditorState);
+ as->config_filename = config_filename;
{
struct TALER_TESTING_Command cmd = {
- .cls = ks,
+ .cls = as,
.label = label,
.run = &auditor_run,
- .cleanup = &auditor_cleanup,
- .traits = &auditor_traits
+ .cleanup = &auditor_cleanup
};
return cmd;
@@ -164,4 +483,4 @@ TALER_TESTING_cmd_exec_auditor (const char *label,
}
-/* end of testing_auditor_api_cmd_exec_auditor.c */
+/* end of testing_api_cmd_auditor_exec_auditor.c */