commit 52d6b14afc917ca78a5a9d19161a2115343b98b5
parent 87f5baf8e9dfa690fe902bd9c525610da8251e77
Author: Christian Grothoff <christian@grothoff.org>
Date: Wed, 5 Aug 2026 23:45:39 +0200
fix inventory_deleted webhook trigger
Diffstat:
2 files changed, 153 insertions(+), 20 deletions(-)
diff --git a/src/backenddb/pg_do_handle_inventory_changes.sql b/src/backenddb/pg_do_handle_inventory_changes.sql
@@ -22,7 +22,11 @@ DECLARE
webhook RECORD; -- To iterate over all matching webhooks
my_instance_id INT8;
my_event_type TEXT;
- my_do_new BOOLEAN;
+ -- Record the un-prefixed placeholders are taken from: NEW, except on
+ -- DELETE where NEW does not exist and the row that went away is the
+ -- one the webhook is about.
+ my_rec RECORD;
+ my_do_cur BOOLEAN;
my_do_old BOOLEAN;
BEGIN
SELECT SUBSTRING(current_schema()::TEXT
@@ -32,15 +36,18 @@ BEGIN
CASE TG_OP
WHEN 'INSERT' THEN
my_event_type := 'inventory_added';
- my_do_new := TRUE;
+ my_rec := NEW;
+ my_do_cur := TRUE;
my_do_old := FALSE;
WHEN 'DELETE' THEN
my_event_type := 'inventory_deleted';
- my_do_new := FALSE;
+ my_rec := OLD;
+ my_do_cur := TRUE;
my_do_old := FALSE;
WHEN 'UPDATE' THEN
my_event_type := 'inventory_updated';
- my_do_new := TRUE;
+ my_rec := NEW;
+ my_do_cur := TRUE;
my_do_old := TRUE;
IF (to_jsonb(OLD) - 'total_locked' - 'total_locked_frac')
IS NOT DISTINCT FROM
@@ -63,56 +70,56 @@ BEGIN
LOOP
-- Resolve placeholders for the current webhook
resolved_body := webhook.body_template;
- IF my_do_new
+ IF my_do_cur
THEN
resolved_body := merchant.replace_placeholder(resolved_body,
'webhook_type',
my_event_type);
resolved_body := merchant.replace_placeholder(resolved_body,
'product_serial',
- NEW.product_serial::TEXT);
+ my_rec.product_serial::TEXT);
resolved_body := merchant.replace_placeholder(resolved_body,
'product_id',
- NEW.product_id);
+ my_rec.product_id);
resolved_body := merchant.replace_placeholder(resolved_body,
'description',
- NEW.description);
+ my_rec.description);
resolved_body := merchant.replace_placeholder(resolved_body,
'description_i18n',
- NEW.description_i18n::TEXT);
+ my_rec.description_i18n::TEXT);
resolved_body := merchant.replace_placeholder(resolved_body,
'unit',
- NEW.unit);
+ my_rec.unit);
resolved_body := merchant.replace_placeholder(resolved_body,
'image',
- NEW.image);
+ my_rec.image);
resolved_body := merchant.replace_placeholder(resolved_body,
'taxes',
- NEW.taxes::TEXT);
+ my_rec.taxes::TEXT);
resolved_body := merchant.replace_placeholder(resolved_body,
'price',
- NEW.price_array[1]::TEXT);
+ my_rec.price_array[1]::TEXT);
resolved_body := merchant.replace_placeholder(resolved_body,
'unit_price',
- NEW.price_array::TEXT);
+ my_rec.price_array::TEXT);
resolved_body := merchant.replace_placeholder(resolved_body,
'total_stock',
- NEW.total_stock::TEXT);
+ my_rec.total_stock::TEXT);
resolved_body := merchant.replace_placeholder(resolved_body,
'total_sold',
- NEW.total_sold::TEXT);
+ my_rec.total_sold::TEXT);
resolved_body := merchant.replace_placeholder(resolved_body,
'total_lost',
- NEW.total_lost::TEXT);
+ my_rec.total_lost::TEXT);
resolved_body := merchant.replace_placeholder(resolved_body,
'address',
- NEW.address::TEXT);
+ my_rec.address::TEXT);
resolved_body := merchant.replace_placeholder(resolved_body,
'next_restock',
- NEW.next_restock::TEXT);
+ my_rec.next_restock::TEXT);
resolved_body := merchant.replace_placeholder(resolved_body,
'minimum_age',
- NEW.minimum_age::TEXT);
+ my_rec.minimum_age::TEXT);
END IF;
IF my_do_old
THEN
diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c
@@ -256,6 +256,41 @@ query_sql_num (const char *sql,
}
+/**
+ * Evaluates @a sql, which must be a parameter-less query returning
+ * exactly one row with a single TEXT column named "txt".
+ *
+ * @param sql the query to run
+ * @param[out] txt set to the value returned, to be freed by the caller
+ * @return 0 on success, 1 otherwise.
+ */
+static int
+query_sql_text (const char *sql,
+ char **txt)
+{
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_string ("txt",
+ txt),
+ GNUNET_PQ_result_spec_end
+ };
+
+ TEST_COND_RET_ON_FAIL (GNUNET_OK ==
+ GNUNET_PQ_prepare_anon (pg->conn,
+ sql),
+ "Preparing direct query failed\n");
+ TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
+ GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "",
+ params,
+ rs),
+ "Direct query failed\n");
+ return 0;
+}
+
+
/* ********** Instances ********** */
@@ -8538,6 +8573,96 @@ test_delete_webhook (const struct InstanceData *instance,
/**
+ * Tests that deleting a product queues an 'inventory_deleted' webhook
+ * whose placeholders were resolved from the row that went away.
+ *
+ * Regression test: handle_inventory_changes() set both substitution
+ * flags to FALSE for TG_OP='DELETE', so the pending webhook shipped
+ * body_template verbatim -- not even {{webhook_type}} was replaced.
+ *
+ * Note: this runs on its own instance and *after* the pending webhook
+ * tests, which rely on the global merchant_pending_webhooks identity
+ * sequence not having been advanced by anybody else.
+ *
+ * @return 0 when successful, 1 otherwise.
+ */
+static int
+test_inventory_deleted_webhook (void)
+{
+ struct InstanceData instance;
+ struct WebhookData webhook;
+ struct ProductData product;
+ char *body = NULL;
+ int ret = 1;
+
+ make_instance ("test_inst_inventory_webhooks",
+ &instance);
+ TEST_WITH_FAIL_CLAUSE (test_insert_instance (&instance,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT),
+ free_instance_data (&instance);
+ return 1;
+ );
+ make_webhook ("test_webhooks_wb_inventory_deleted",
+ &webhook);
+ webhook.webhook.event_type = "inventory_deleted";
+ webhook.webhook.url = "https://example.com/inventory-deleted";
+ webhook.webhook.body_template =
+ "{\"type\":\"{{webhook_type}}\""
+ ",\"product_id\":\"{{product_id}}\""
+ ",\"description\":\"{{description}}\"}";
+ TEST_WITH_FAIL_CLAUSE (test_insert_webhook (&instance,
+ &webhook,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT),
+ free_instance_data (&instance);
+ return 1;
+ );
+ make_product ("test_webhooks_deleted_product",
+ &product);
+ TEST_WITH_FAIL_CLAUSE (test_insert_product (&instance,
+ &product,
+ 0,
+ NULL,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
+ false,
+ false,
+ -1),
+ goto cleanup;
+ );
+ TEST_WITH_FAIL_CLAUSE (test_delete_product (&instance,
+ &product,
+ false,
+ false,
+ false),
+ goto cleanup;
+ );
+ TEST_WITH_FAIL_CLAUSE (
+ query_sql_text ("SELECT body AS txt"
+ " FROM merchant.merchant_pending_webhooks"
+ " WHERE url='https://example.com/inventory-deleted'",
+ &body),
+ goto cleanup;
+ );
+ if (0 != strcmp ("{\"type\":\"inventory_deleted\""
+ ",\"product_id\":\"test_webhooks_deleted_product\""
+ ",\"description\":\"This is a test product\"}",
+ body))
+ {
+ GNUNET_break (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "inventory_deleted webhook body not resolved: `%s'\n",
+ body);
+ goto cleanup;
+ }
+ ret = 0;
+cleanup:
+ GNUNET_free (body);
+ free_product_data (&product);
+ free_instance_data (&instance);
+ return ret;
+}
+
+
+/**
* Closure for webhook tests.
*/
struct TestWebhooks_Closure
@@ -9635,6 +9760,7 @@ run_tests (void)
TEST_RET_ON_FAIL (test_templates ());
TEST_RET_ON_FAIL (test_webhooks ());
TEST_RET_ON_FAIL (test_pending_webhooks ());
+ TEST_RET_ON_FAIL (test_inventory_deleted_webhook ());
TEST_RET_ON_FAIL (test_statistics ());
return 0;
}