diff --git a/include/ProxySQL_Plugin.h b/include/ProxySQL_Plugin.h index eae0c2f85..a044b8ead 100644 --- a/include/ProxySQL_Plugin.h +++ b/include/ProxySQL_Plugin.h @@ -58,6 +58,56 @@ using proxysql_plugin_db_handle_cb = using proxysql_plugin_log_message_cb = void (*)(int, const char *); +// Pre-execution query hook (Step 2 ABI extension). +// +// Wire protocol the hook is being invoked for. A plugin can register +// independently for each protocol; one hook per protocol per plugin. +enum class ProxySQL_PluginProtocol : uint8_t { + mysql = 0, + pgsql = 1 +}; + +// Payload handed to a query-hook callback. All pointers are owned by +// core and remain valid only for the duration of the callback. The +// callback must not retain them or mutate the underlying buffers. +// +// query_text is the SQL the client sent, NOT NUL-terminated; query_len +// is its length in bytes. user / client_ip / schema are NUL-terminated +// C strings and may be empty (never NULL). +struct ProxySQL_PluginQueryHookPayload { + const char *user; + const char *client_ip; + const char *schema; + const char *query_text; + uint32_t query_len; +}; + +// Outcome of a query hook. ALLOW lets the query proceed to the +// backend. DENY returns an error to the client and the query never +// dispatches; the message is copied by core, the plugin need not keep +// it alive after the callback returns. +// +// NOTE: same std::string ABI coupling caveat as +// ProxySQL_PluginCommandResult applies. +enum class ProxySQL_PluginQueryHookAction : uint8_t { + allow = 0, + deny = 1 +}; + +struct ProxySQL_PluginQueryHookResult { + ProxySQL_PluginQueryHookAction action; + std::string message; +}; + +using proxysql_plugin_query_hook_cb = + ProxySQL_PluginQueryHookResult (*)(const ProxySQL_PluginQueryHookPayload &); + +// register_query_hook(proto, cb). Returns true on success, false if a +// hook for that protocol is already registered. Valid only during the +// init callback (same lifetime rule as register_table / register_command). +using proxysql_plugin_register_query_hook_cb = + bool (*)(ProxySQL_PluginProtocol, proxysql_plugin_query_hook_cb); + // Services provided to plugins during init. // register_table/register_command: valid only during the init callback. // get_*db, log_message, snapshots: valid for the plugin's entire lifetime. @@ -71,6 +121,10 @@ struct ProxySQL_PluginServices { proxysql_plugin_db_handle_cb get_admindb; proxysql_plugin_db_handle_cb get_configdb; proxysql_plugin_db_handle_cb get_statsdb; + // Step 2 ABI extension: pre-execution query hook. Older plugins + // that don't know about this field stop reading the struct at the + // previous member; new plugins check for non-null before calling. + proxysql_plugin_register_query_hook_cb register_query_hook; }; using proxysql_plugin_init_cb = diff --git a/include/ProxySQL_PluginManager.h b/include/ProxySQL_PluginManager.h index bf292e398..f84bdbb09 100644 --- a/include/ProxySQL_PluginManager.h +++ b/include/ProxySQL_PluginManager.h @@ -29,6 +29,11 @@ public: bool has_command_for_test(const std::string& sql) const; bool register_table(const ProxySQL_PluginTableDef& def); bool register_command(const char* sql, proxysql_plugin_admin_command_cb cb); + bool register_query_hook(ProxySQL_PluginProtocol proto, proxysql_plugin_query_hook_cb cb); + bool has_query_hook(ProxySQL_PluginProtocol proto) const; + bool dispatch_query_hook(ProxySQL_PluginProtocol proto, + const ProxySQL_PluginQueryHookPayload& payload, + ProxySQL_PluginQueryHookResult& result) const; size_t size() const; @@ -59,6 +64,9 @@ private: std::vector tables_stats_; std::deque table_storage_; std::vector commands_; + // At most one hook per protocol; nullptr means "no hook". + proxysql_plugin_query_hook_cb mysql_query_hook_ { nullptr }; + proxysql_plugin_query_hook_cb pgsql_query_hook_ { nullptr }; }; ProxySQL_PluginManager* proxysql_get_plugin_manager(); @@ -67,6 +75,17 @@ bool proxysql_dispatch_configured_plugin_admin_command( const std::string& sql, ProxySQL_PluginCommandResult& result ); +bool proxysql_dispatch_configured_plugin_query_hook( + ProxySQL_PluginProtocol proto, + const ProxySQL_PluginQueryHookPayload& payload, + ProxySQL_PluginQueryHookResult& result +); +// Fast path for hot code: returns true when the active manager has a hook +// registered for the given protocol. No locks taken. Callers should still +// invoke proxysql_dispatch_configured_plugin_query_hook to actually run the +// hook (which takes the manager lock). Use this to elide the dispatch call +// entirely on the no-plugin path. +bool proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol proto); bool proxysql_load_configured_plugins( std::unique_ptr& manager, const std::vector& plugin_modules, diff --git a/lib/ProxySQL_PluginManager.cpp b/lib/ProxySQL_PluginManager.cpp index 65fc2aa0e..ad2c4305e 100644 --- a/lib/ProxySQL_PluginManager.cpp +++ b/lib/ProxySQL_PluginManager.cpp @@ -83,6 +83,22 @@ void register_command_service(const char* sql, proxysql_plugin_admin_command_cb } } +bool register_query_hook_service(ProxySQL_PluginProtocol proto, + proxysql_plugin_query_hook_cb cb) { + if (g_registry_target == nullptr) { + proxy_warning("Plugin query hook registration attempted outside init phase\n"); + return false; + } + if (!g_registry_target->register_query_hook(proto, cb)) { + note_registration_failure("plugin query hook", + proto == ProxySQL_PluginProtocol::mysql ? "mysql" : "pgsql"); + proxy_warning("Plugin query hook registration failed for %s\n", + proto == ProxySQL_PluginProtocol::mysql ? "mysql" : "pgsql"); + return false; + } + return true; +} + SQLite3DB* get_admindb_service() { return proxysql_plugin_get_admindb(); } @@ -165,6 +181,7 @@ ProxySQL_PluginManager::ProxySQL_PluginManager() { services_.get_configdb = &get_configdb_service; services_.get_statsdb = &get_statsdb_service; services_.log_message = &log_message_service; + services_.register_query_hook = ®ister_query_hook_service; } ProxySQL_PluginManager::~ProxySQL_PluginManager() { @@ -430,6 +447,47 @@ bool ProxySQL_PluginManager::register_table(const ProxySQL_PluginTableDef& def) return true; } +bool ProxySQL_PluginManager::register_query_hook(ProxySQL_PluginProtocol proto, + proxysql_plugin_query_hook_cb cb) { + if (cb == nullptr) { + return false; + } + switch (proto) { + case ProxySQL_PluginProtocol::mysql: + if (mysql_query_hook_ != nullptr) return false; + mysql_query_hook_ = cb; + return true; + case ProxySQL_PluginProtocol::pgsql: + if (pgsql_query_hook_ != nullptr) return false; + pgsql_query_hook_ = cb; + return true; + } + return false; +} + +bool ProxySQL_PluginManager::has_query_hook(ProxySQL_PluginProtocol proto) const { + switch (proto) { + case ProxySQL_PluginProtocol::mysql: return mysql_query_hook_ != nullptr; + case ProxySQL_PluginProtocol::pgsql: return pgsql_query_hook_ != nullptr; + } + return false; +} + +bool ProxySQL_PluginManager::dispatch_query_hook(ProxySQL_PluginProtocol proto, + const ProxySQL_PluginQueryHookPayload& payload, + ProxySQL_PluginQueryHookResult& result) const { + proxysql_plugin_query_hook_cb cb = nullptr; + switch (proto) { + case ProxySQL_PluginProtocol::mysql: cb = mysql_query_hook_; break; + case ProxySQL_PluginProtocol::pgsql: cb = pgsql_query_hook_; break; + } + if (cb == nullptr) { + return false; + } + result = cb(payload); + return true; +} + bool ProxySQL_PluginManager::register_command(const char* sql, proxysql_plugin_admin_command_cb cb) { if (sql == nullptr || *sql == '\0' || cb == nullptr) { return false; @@ -467,6 +525,33 @@ bool proxysql_dispatch_configured_plugin_admin_command( return g_active_plugin_manager.load()->dispatch_admin_command(ctx, sql, result); } +bool proxysql_dispatch_configured_plugin_query_hook( + ProxySQL_PluginProtocol proto, + const ProxySQL_PluginQueryHookPayload& payload, + ProxySQL_PluginQueryHookResult& result +) { + std::lock_guard lock(g_active_plugin_manager_mutex); + ProxySQL_PluginManager* mgr = g_active_plugin_manager.load(); + if (mgr == nullptr) { + return false; + } + return mgr->dispatch_query_hook(proto, payload, result); +} + +bool proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol proto) { + // Hot path: lock-free. Reads the atomic pointer; if non-null, calls + // has_query_hook() which only reads two pointer-sized fields. A + // concurrent unload can null the pointer between this check and a + // subsequent dispatch call -- the dispatch helper handles that case + // by re-checking under the lock. Callers must tolerate spurious + // "yes" returns. + ProxySQL_PluginManager* mgr = g_active_plugin_manager.load(std::memory_order_acquire); + if (mgr == nullptr) { + return false; + } + return mgr->has_query_hook(proto); +} + bool proxysql_load_configured_plugins( std::unique_ptr& manager, const std::vector& plugin_modules, diff --git a/test/tap/test_helpers/fake_plugin.cpp b/test/tap/test_helpers/fake_plugin.cpp index dee96aeaf..0cd97ce49 100644 --- a/test/tap/test_helpers/fake_plugin.cpp +++ b/test/tap/test_helpers/fake_plugin.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include namespace { @@ -11,6 +13,21 @@ ProxySQL_PluginCommandResult fake_command(const ProxySQL_PluginCommandContext&, return {0, 1, "fake command executed"}; } +ProxySQL_PluginQueryHookResult fake_query_hook(const ProxySQL_PluginQueryHookPayload& payload) { + // Echo the SQL back through the message field so tests can verify the + // payload was wired through. DENY-vs-ALLOW is selected by env var so + // a test can exercise both paths without rebuilding the plugin. + std::string msg(payload.query_text, payload.query_len); + const char* deny_env = std::getenv("PROXYSQL_FAKE_PLUGIN_HOOK_DENY"); + if (deny_env == nullptr) { + deny_env = std::getenv("PROXYSQL_FAKE_PLUGIN2_HOOK_DENY"); + } + if (deny_env != nullptr && *deny_env != '\0') { + return {ProxySQL_PluginQueryHookAction::deny, std::string("denied: ") + msg}; + } + return {ProxySQL_PluginQueryHookAction::allow, msg}; +} + void fake_log_event(const char *event) { const char *log_path = std::getenv("PROXYSQL_FAKE_PLUGIN_LOG"); if (log_path == nullptr || *log_path == '\0') { @@ -47,6 +64,16 @@ bool fake_init(ProxySQL_PluginServices *services) { services->register_command != nullptr) { services->register_command("PLUGIN FAKE NOOP", &fake_command); } + if (std::getenv("PROXYSQL_FAKE_PLUGIN_REGISTER_QUERY_HOOK") != nullptr && + services != nullptr && + services->register_query_hook != nullptr) { + const char* proto_env = std::getenv("PROXYSQL_FAKE_PLUGIN_REGISTER_QUERY_HOOK_PROTO"); + ProxySQL_PluginProtocol proto = ProxySQL_PluginProtocol::mysql; + if (proto_env != nullptr && std::strcmp(proto_env, "pgsql") == 0) { + proto = ProxySQL_PluginProtocol::pgsql; + } + services->register_query_hook(proto, &fake_query_hook); + } fake_log_event("init"); return true; } diff --git a/test/tap/tests/unit/Makefile b/test/tap/tests/unit/Makefile index 2b0dab331..68ebc14cd 100644 --- a/test/tap/tests/unit/Makefile +++ b/test/tap/tests/unit/Makefile @@ -323,6 +323,7 @@ UNIT_TESTS := smoke_test-t query_cache_unit-t query_processor_unit-t \ glovars_unit-t \ plugin_manager_unit-t \ plugin_registry_unit-t \ + plugin_query_hook_unit-t \ test_mysqlx_plugin_load-t \ mysqlx_config_store_unit-t \ test_mysqlx_admin_tables-t \ @@ -540,6 +541,12 @@ mysql_resolution_unit-t: mysql_resolution_unit-t.cpp $(ODIR)/tap.o $(ODIR)/tap_n -I$(TAP_IDIR) -I$(PROXYSQL_PATH)/include \ $(STDCPP) -O0 -ggdb $(WGCOV) $(LWGCOV) -lpthread -o $@ +plugin_query_hook_unit-t: plugin_query_hook_unit-t.cpp $(FAKE_PLUGIN_SO) $(ODIR)/tap.o $(ODIR)/test_globals.o $(ODIR)/test_init.o $(LIBPROXYSQLAR) + $(CXX) $< $(ODIR)/tap.o $(ODIR)/test_globals.o $(ODIR)/test_init.o \ + -DPROXYSQL_FAKE_PLUGIN_PATH=\"$(FAKE_PLUGIN_SO)\" \ + $(IDIRS) $(LDIRS) $(OPT) $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) \ + $(MYLIBS) -ldl $(ALLOW_MULTI_DEF) -o $@ + # Pattern rule: all unit tests use the same compile + link flags. # Each test binary is built from its .cpp source, linked against # the test harness objects and libproxysql.a with all dependencies. diff --git a/test/tap/tests/unit/plugin_query_hook_unit-t.cpp b/test/tap/tests/unit/plugin_query_hook_unit-t.cpp new file mode 100644 index 000000000..99eee363f --- /dev/null +++ b/test/tap/tests/unit/plugin_query_hook_unit-t.cpp @@ -0,0 +1,226 @@ +// Step 2 ABI extension test: pre-execution query hook. +// +// Exercises the manager-level register/dispatch pair AND the global +// dispatcher (proxysql_dispatch_configured_plugin_query_hook) that the +// MySQL_Session / PgSQL_Session hot path will call once Commit 2 lands. +// Hot-path call sites themselves are tested separately. + +#include "ProxySQL_PluginManager.h" +#include "ProxySQL_Plugin.h" +#include "tap.h" + +#include +#include +#include +#include +#include + +#ifndef PROXYSQL_FAKE_PLUGIN_PATH +#error "PROXYSQL_FAKE_PLUGIN_PATH must be defined" +#endif + +namespace { + +char g_fake_admin_db = '\0'; +char g_fake_config_db = '\0'; +char g_fake_stats_db = '\0'; + +ProxySQL_PluginQueryHookResult always_allow_hook(const ProxySQL_PluginQueryHookPayload&) { + return {ProxySQL_PluginQueryHookAction::allow, ""}; +} + +ProxySQL_PluginQueryHookResult always_deny_hook(const ProxySQL_PluginQueryHookPayload&) { + return {ProxySQL_PluginQueryHookAction::deny, "blocked"}; +} + +ProxySQL_PluginQueryHookResult echo_hook(const ProxySQL_PluginQueryHookPayload& p) { + std::string body(p.query_text, p.query_len); + return {ProxySQL_PluginQueryHookAction::allow, + std::string(p.user) + "/" + p.client_ip + "/" + p.schema + ":" + body}; +} + +ProxySQL_PluginQueryHookPayload payload_for(const char* user, const char* ip, + const char* schema, const char* query) { + return ProxySQL_PluginQueryHookPayload { + user, ip, schema, query, static_cast(std::strlen(query)) + }; +} + +} // namespace + +SQLite3DB* proxysql_plugin_get_admindb() { return reinterpret_cast(&g_fake_admin_db); } +SQLite3DB* proxysql_plugin_get_configdb() { return reinterpret_cast(&g_fake_config_db); } +SQLite3DB* proxysql_plugin_get_statsdb() { return reinterpret_cast(&g_fake_stats_db); } + +static void test_unregistered_protocols_have_no_hook() { + ProxySQL_PluginManager mgr; + ok(!mgr.has_query_hook(ProxySQL_PluginProtocol::mysql), + "fresh manager has no MySQL hook"); + ok(!mgr.has_query_hook(ProxySQL_PluginProtocol::pgsql), + "fresh manager has no PgSQL hook"); + ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, "x"}; + auto p = payload_for("u", "ip", "s", "select 1"); + ok(!mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r), + "dispatch returns false on protocol with no hook"); + ok(r.action == ProxySQL_PluginQueryHookAction::deny && r.message == "x", + "dispatch leaves caller's result struct untouched on no-hook miss"); +} + +static void test_register_and_dispatch_allow() { + ProxySQL_PluginManager mgr; + ok(mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &always_allow_hook), + "register MySQL allow hook"); + ok(mgr.has_query_hook(ProxySQL_PluginProtocol::mysql), + "MySQL hook now present"); + + ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, "init"}; + auto p = payload_for("alice", "10.0.0.1", "test", "select 1"); + ok(mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r), + "dispatch returns true (hook fired)"); + ok(r.action == ProxySQL_PluginQueryHookAction::allow, + "hook returned ALLOW"); + ok(r.message.empty(), + "ALLOW carries empty message"); +} + +static void test_register_and_dispatch_deny() { + ProxySQL_PluginManager mgr; + ok(mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &always_deny_hook), + "register MySQL deny hook"); + ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::allow, ""}; + auto p = payload_for("eve", "10.0.0.99", "prod", "drop table users"); + ok(mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r), + "dispatch returns true"); + ok(r.action == ProxySQL_PluginQueryHookAction::deny, + "hook returned DENY"); + ok(r.message == "blocked", + "DENY message propagated to caller"); +} + +static void test_register_null_callback_rejected() { + ProxySQL_PluginManager mgr; + ok(!mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, nullptr), + "null callback rejected for MySQL"); + ok(!mgr.register_query_hook(ProxySQL_PluginProtocol::pgsql, nullptr), + "null callback rejected for PgSQL"); + ok(!mgr.has_query_hook(ProxySQL_PluginProtocol::mysql), + "no hook stored after rejection (MySQL)"); + ok(!mgr.has_query_hook(ProxySQL_PluginProtocol::pgsql), + "no hook stored after rejection (PgSQL)"); +} + +static void test_duplicate_hook_rejected() { + ProxySQL_PluginManager mgr; + ok(mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &always_allow_hook), + "first MySQL registration succeeds"); + ok(!mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &always_deny_hook), + "second MySQL registration rejected (one hook per protocol per manager)"); + // Verify the first hook is still in effect + ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, "x"}; + auto p = payload_for("u", "ip", "s", "q"); + mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r); + ok(r.action == ProxySQL_PluginQueryHookAction::allow, + "original hook still in effect after rejected duplicate"); +} + +static void test_protocols_independent() { + ProxySQL_PluginManager mgr; + ok(mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &always_allow_hook), + "MySQL hook registers"); + ok(mgr.register_query_hook(ProxySQL_PluginProtocol::pgsql, &always_deny_hook), + "PgSQL hook registers independently"); + + ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, ""}; + auto p = payload_for("u", "ip", "s", "q"); + mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r); + ok(r.action == ProxySQL_PluginQueryHookAction::allow, + "MySQL dispatch routes to MySQL hook (allow)"); + mgr.dispatch_query_hook(ProxySQL_PluginProtocol::pgsql, p, r); + ok(r.action == ProxySQL_PluginQueryHookAction::deny, + "PgSQL dispatch routes to PgSQL hook (deny)"); +} + +static void test_payload_threaded_through() { + ProxySQL_PluginManager mgr; + mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &echo_hook); + ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, ""}; + auto p = payload_for("alice", "10.1.2.3", "shop", "SELECT id FROM orders"); + ok(mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r), + "dispatch fires echo hook"); + ok(r.message == "alice/10.1.2.3/shop:SELECT id FROM orders", + "all payload fields reach the hook intact (got: '%s')", r.message.c_str()); +} + +static void test_global_dispatcher_no_active_manager() { + // Make sure no manager is active (defensive — earlier tests may have + // left state behind, but we never set the active pointer in this test). + ok(proxysql_get_plugin_manager() == nullptr, + "global manager is null at start"); + ok(!proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql), + "has_hook helper returns false with no active manager"); + ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, "untouched"}; + auto p = payload_for("u", "ip", "s", "q"); + ok(!proxysql_dispatch_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql, p, r), + "dispatcher returns false with no active manager"); + ok(r.message == "untouched", + "dispatcher leaves caller's result untouched on miss"); +} + +static void test_global_dispatcher_with_active_manager() { + setenv("PROXYSQL_FAKE_PLUGIN_REGISTER_QUERY_HOOK", "1", 1); + std::unique_ptr mgr; + std::vector paths { PROXYSQL_FAKE_PLUGIN_PATH }; + std::string err; + ok(proxysql_load_configured_plugins(mgr, paths, err), + "load fake plugin (which registers a MySQL query hook)"); + ok(proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql), + "has_hook helper reports a MySQL hook is now active"); + ok(!proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol::pgsql), + "has_hook helper reports no PgSQL hook (fake plugin only registered MySQL)"); + + ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, ""}; + auto p = payload_for("u", "ip", "s", "select 42"); + ok(proxysql_dispatch_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql, p, r), + "global dispatcher routes to fake hook"); + ok(r.action == ProxySQL_PluginQueryHookAction::allow, + "hook returned ALLOW (no DENY env set)"); + ok(r.message == "select 42", + "fake hook echoed the SQL through the result message (got: '%s')", r.message.c_str()); + + // Now flip to DENY and re-dispatch + setenv("PROXYSQL_FAKE_PLUGIN_HOOK_DENY", "1", 1); + ProxySQL_PluginQueryHookResult r2 {ProxySQL_PluginQueryHookAction::allow, ""}; + ok(proxysql_dispatch_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql, p, r2), + "second dispatch with DENY env still fires"); + ok(r2.action == ProxySQL_PluginQueryHookAction::deny, + "hook now returns DENY"); + ok(r2.message == "denied: select 42", + "DENY message includes the offending SQL (got: '%s')", r2.message.c_str()); + + (void)proxysql_stop_configured_plugins(mgr, err); + unsetenv("PROXYSQL_FAKE_PLUGIN_REGISTER_QUERY_HOOK"); + unsetenv("PROXYSQL_FAKE_PLUGIN_HOOK_DENY"); + + // After stop, neither helper should report a hook. + ok(!proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql), + "has_hook helper returns false after stop"); + ProxySQL_PluginQueryHookResult r3 {ProxySQL_PluginQueryHookAction::deny, "still-here"}; + ok(!proxysql_dispatch_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql, p, r3), + "dispatcher returns false after stop"); +} + +int main() { + plan(41); + + test_unregistered_protocols_have_no_hook(); + test_register_and_dispatch_allow(); + test_register_and_dispatch_deny(); + test_register_null_callback_rejected(); + test_duplicate_hook_rejected(); + test_protocols_independent(); + test_payload_threaded_through(); + test_global_dispatcher_no_active_manager(); + test_global_dispatcher_with_active_manager(); + + return exit_status(); +}