diff --git a/lib/Admin_Handler.cpp b/lib/Admin_Handler.cpp index b653f0befc..ff20242a00 100644 --- a/lib/Admin_Handler.cpp +++ b/lib/Admin_Handler.cpp @@ -1294,12 +1294,23 @@ bool admin_handler_command_load_or_save(char *query_no_space, unsigned int query int rows=0; if (query_no_space[5] == 'P' || query_no_space[5] == 'p') { rows=SPA->proxysql_config().Read_PgSQL_Users_from_configfile(); - proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded pgsql users from CONFIG\n"); + if (rows < 0) { + proxy_debug(PROXY_DEBUG_ADMIN, 4, "Failed to load pgsql users from CONFIG due to validation errors\n"); +SPA->send_error_msg_to_client(sess, "Configuration validation failed - check error log for details"); + } else { + proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded pgsql users from CONFIG\n"); + SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space); + } } else { rows=SPA->proxysql_config().Read_MySQL_Users_from_configfile(); - proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded mysql users from CONFIG\n"); + if (rows < 0) { + proxy_debug(PROXY_DEBUG_ADMIN, 4, "Failed to load mysql users from CONFIG due to validation errors\n"); + SPA->send_error_msg_to_client(sess, (char *)"Configuration validation failed - check error log for details"); + } else { + proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded mysql users from CONFIG\n"); + SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space); + } } - SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space); GloVars.confFile->CloseFile(); } else { proxy_debug(PROXY_DEBUG_ADMIN, 4, "Unable to open or parse config file %s\n", GloVars.config_file); @@ -1695,12 +1706,23 @@ bool admin_handler_command_load_or_save(char *query_no_space, unsigned int query int rows=0; if (is_pgsql) { rows=SPA->proxysql_config().Read_PgSQL_Servers_from_configfile(); - proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded pgsql servers from CONFIG\n"); + if (rows < 0) { + proxy_debug(PROXY_DEBUG_ADMIN, 4, "Failed to load pgsql servers from CONFIG due to validation errors\n"); + SPA->send_error_msg_to_client(sess, (char *)"Configuration validation failed - check error log for details"); + } else { + proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded pgsql servers from CONFIG\n"); + SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space); + } } else { rows=SPA->proxysql_config().Read_MySQL_Servers_from_configfile(); - proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded mysql servers from CONFIG\n"); + if (rows < 0) { + proxy_debug(PROXY_DEBUG_ADMIN, 4, "Failed to load mysql servers from CONFIG due to validation errors\n"); + SPA->send_error_msg_to_client(sess, (char *)"Configuration validation failed - check error log for details"); + } else { + proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded mysql servers from CONFIG\n"); + SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space); + } } - SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space); GloVars.confFile->CloseFile(); } else { proxy_debug(PROXY_DEBUG_ADMIN, 4, "Unable to open or parse config file %s\n", GloVars.config_file); @@ -1816,8 +1838,13 @@ bool admin_handler_command_load_or_save(char *query_no_space, unsigned int query ProxySQL_Admin *SPA=(ProxySQL_Admin *)pa; int rows=0; rows=SPA->proxysql_config().Read_ProxySQL_Servers_from_configfile(); - proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded ProxySQL servers from CONFIG\n"); - SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space); + if (rows < 0) { + proxy_debug(PROXY_DEBUG_ADMIN, 4, "Failed to load ProxySQL servers from CONFIG due to validation errors\n"); + SPA->send_error_msg_to_client(sess, (char *)"Configuration validation failed - check error log for details"); + } else { + proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded ProxySQL servers from CONFIG\n"); + SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space); + } GloVars.confFile->CloseFile(); } else { proxy_debug(PROXY_DEBUG_ADMIN, 4, "Unable to open or parse config file %s\n", GloVars.config_file); diff --git a/lib/ProxySQL_Config.cpp b/lib/ProxySQL_Config.cpp index 347cf3bdf6..5809d530ff 100644 --- a/lib/ProxySQL_Config.cpp +++ b/lib/ProxySQL_Config.cpp @@ -4,6 +4,8 @@ #include "cpp.h" #include +#include +#include const char* config_header = "########################################################################################\n" "# This config file is parsed using libconfig , and its grammar is described in:\n" @@ -144,13 +146,94 @@ int ProxySQL_Config::Write_MySQL_Users_to_configfile(std::string& data) { return 0; } +/** + * Generic template function for validating config entries to prevent duplicates and validate mandatory fields + * + * @tparam PKTuple - Type of the primary key tuple (e.g., std::tuple) + * @tparam ExtractorFunc - Function type that extracts primary key from a config Setting + * @param config_array - The libconfig Setting array to validate + * @param section_name - Name of the config section for error messages + * @param extractor - Function that extracts primary key tuple and validates mandatory fields + * @param admindb - Database connection for cleanup operations + * @param use_mysql_pragma - Whether to use MySQL-style PRAGMA commands + * @return 0 on success, -1 on validation failure + */ +template +int validate_config_entries(const Setting& config_array, const char* section_name, + ExtractorFunc extractor, SQLite3DB* admindb, bool use_mysql_pragma = true) { + int count = config_array.getLength(); + std::set pk_set; + + // PRE-VALIDATION: Check for duplicates and mandatory fields + for (int i = 0; i < count; i++) { + const Setting& entry = config_array[i]; + PKTuple pk_tuple; + std::string error_msg; + + // Use the extractor function to get primary key and validate mandatory fields + if (!extractor(entry, pk_tuple, error_msg)) { + proxy_error("Admin: %s\n", error_msg.c_str()); + if (use_mysql_pragma) { + admindb->execute("PRAGMA foreign_keys = OFF"); + admindb->execute("PRAGMA foreign_keys = ON"); + } else { + admindb->execute("PRAGMA foreign_keys = ON"); + } + return -1; + } + + // Check for duplicates within config file + if (pk_set.find(pk_tuple) != pk_set.end()) { + // Call extractor again to get detailed error message for duplicate + std::string duplicate_error; + extractor(entry, pk_tuple, duplicate_error); // This populates pk_tuple again + proxy_error("Admin: duplicate entry found in %s config section\n", section_name); + if (use_mysql_pragma) { + admindb->execute("PRAGMA foreign_keys = OFF"); + admindb->execute("PRAGMA foreign_keys = ON"); + } else { + admindb->execute("PRAGMA foreign_keys = ON"); + } + return -1; + } + pk_set.insert(pk_tuple); + } + + return 0; // Validation passed +} + int ProxySQL_Config::Read_MySQL_Users_from_configfile() { const Setting& root = GloVars.confFile->cfg.getRoot(); if (root.exists("mysql_users")==false) return 0; const Setting &mysql_users = root["mysql_users"]; int count = mysql_users.getLength(); + + // Define extractor function for MySQL users validation + auto mysql_users_extractor = [](const Setting& user, std::tuple& pk_tuple, std::string& error_msg) -> bool { + std::string username; + int backend = 1; // default backend value + + // Validate mandatory fields + if (user.lookupValue("username", username) == false) { + error_msg = "detected a mysql_users in config file without a mandatory username"; + return false; + } + user.lookupValue("backend", backend); // Check if backend is specified + + pk_tuple = std::make_tuple(username, backend); + return true; + }; + + // Use template validation function + int validation_result = validate_config_entries>( + mysql_users, "mysql_users", mysql_users_extractor, admindb, true); + + if (validation_result != 0) { + return validation_result; + } + + // If validation passed, proceed with existing INSERT OR REPLACE logic //fprintf(stderr, "Found %d users\n",count); - int i; int rows=0; admindb->execute("PRAGMA foreign_keys = OFF"); char *q=(char *)"INSERT OR REPLACE INTO mysql_users (username, password, active, use_ssl, default_hostgroup, default_schema, schema_locked, transaction_persistent, fast_forward, max_connections, attributes, comment) VALUES ('%s', '%s', %d, %d, %d, '%s', %d, %d, %d, %d, '%s','%s')"; @@ -1022,6 +1105,41 @@ int ProxySQL_Config::Read_MySQL_Servers_from_configfile() { if (root.exists("mysql_servers")==true) { const Setting &mysql_servers = root["mysql_servers"]; int count = mysql_servers.getLength(); + + // Define extractor function for MySQL servers validation + auto mysql_servers_extractor = [](const Setting& server, std::tuple& pk_tuple, std::string& error_msg) -> bool { + std::string address; + int port = 3306; + int hostgroup; + + // Validate mandatory fields + if (server.lookupValue("address", address) == false) { + if (server.lookupValue("hostname", address) == false) { + error_msg = "detected a mysql_servers in config file without a mandatory hostname"; + return false; + } + } + server.lookupValue("port", port); + if (server.lookupValue("hostgroup", hostgroup) == false) { + if (server.lookupValue("hostgroup_id", hostgroup) == false) { + error_msg = "detected a mysql_servers in config file without a mandatory hostgroup_id"; + return false; + } + } + + pk_tuple = std::make_tuple(hostgroup, address, port); + return true; + }; + + // Use template validation function + int validation_result = validate_config_entries>( + mysql_servers, "mysql_servers", mysql_servers_extractor, admindb, false); + + if (validation_result != 0) { + return validation_result; + } + + // If validation passed, proceed with existing INSERT OR REPLACE logic //fprintf(stderr, "Found %d servers\n",count); char *q=(char *)"INSERT OR REPLACE INTO mysql_servers (hostname, port, gtid_port, hostgroup_id, compression, weight, status, max_connections, max_replication_lag, use_ssl, max_latency_ms, comment) VALUES (\"%s\", %d, %d, %d, %d, %d, \"%s\", %d, %d, %d, %d, '%s')"; for (i=0; i< count; i++) { @@ -1465,6 +1583,37 @@ int ProxySQL_Config::Read_ProxySQL_Servers_from_configfile() { if (root.exists("proxysql_servers")==true) { const Setting & proxysql_servers = root["proxysql_servers"]; int count = proxysql_servers.getLength(); + + // Define extractor function for ProxySQL servers validation + auto proxysql_servers_extractor = [](const Setting& server, std::tuple& pk_tuple, std::string& error_msg) -> bool { + std::string address; + int port; + + // Validate mandatory fields + if (server.lookupValue("address", address) == false) { + if (server.lookupValue("hostname", address) == false) { + error_msg = "detected a proxysql_servers in config file without a mandatory hostname"; + return false; + } + } + if (server.lookupValue("port", port) == false) { + error_msg = "detected a proxysql_servers in config file without a mandatory port"; + return false; + } + + pk_tuple = std::make_tuple(address, port); + return true; + }; + + // Use template validation function + int validation_result = validate_config_entries>( + proxysql_servers, "proxysql_servers", proxysql_servers_extractor, admindb, false); + + if (validation_result != 0) { + return validation_result; + } + + // If validation passed, proceed with existing INSERT OR REPLACE logic //fprintf(stderr, "Found %d servers\n",count); char *q=(char *)"INSERT OR REPLACE INTO proxysql_servers (hostname, port, weight, comment) VALUES (\"%s\", %d, %d, '%s')"; for (i=0; i< count; i++) { @@ -1629,6 +1778,41 @@ int ProxySQL_Config::Read_PgSQL_Servers_from_configfile() { if (root.exists("pgsql_servers") == true) { const Setting& pgsql_servers = root["pgsql_servers"]; int count = pgsql_servers.getLength(); + + // Define extractor function for PostgreSQL servers validation + auto pgsql_servers_extractor = [](const Setting& server, std::tuple& pk_tuple, std::string& error_msg) -> bool { + std::string address; + int port = 5432; + int hostgroup; + + // Validate mandatory fields + if (server.lookupValue("address", address) == false) { + if (server.lookupValue("hostname", address) == false) { + error_msg = "detected a pgsql_servers in config file without a mandatory hostname"; + return false; + } + } + server.lookupValue("port", port); + if (server.lookupValue("hostgroup", hostgroup) == false) { + if (server.lookupValue("hostgroup_id", hostgroup) == false) { + error_msg = "detected a pgsql_servers in config file without a mandatory hostgroup_id"; + return false; + } + } + + pk_tuple = std::make_tuple(hostgroup, address, port); + return true; + }; + + // Use template validation function + int validation_result = validate_config_entries>( + pgsql_servers, "pgsql_servers", pgsql_servers_extractor, admindb, false); + + if (validation_result != 0) { + return validation_result; + } + + // If validation passed, proceed with existing INSERT OR REPLACE logic //fprintf(stderr, "Found %d servers\n",count); char* q = (char*)"INSERT OR REPLACE INTO pgsql_servers (hostname, port, hostgroup_id, compression, weight, status, max_connections, max_replication_lag, use_ssl, max_latency_ms, comment) VALUES (\"%s\", %d, %d, %d, %d, \"%s\", %d, %d, %d, %d, '%s')"; for (i = 0; i < count; i++) { @@ -1782,8 +1966,33 @@ int ProxySQL_Config::Read_PgSQL_Users_from_configfile() { if (root.exists("pgsql_users") == false) return 0; const Setting& pgsql_users = root["pgsql_users"]; int count = pgsql_users.getLength(); + + // Define extractor function for PostgreSQL users validation + auto pgsql_users_extractor = [](const Setting& user, std::tuple& pk_tuple, std::string& error_msg) -> bool { + std::string username; + int backend = 1; // default backend value + + // Validate mandatory fields + if (user.lookupValue("username", username) == false) { + error_msg = "detected a pgsql_users in config file without a mandatory username"; + return false; + } + user.lookupValue("backend", backend); // Check if backend is specified + + pk_tuple = std::make_tuple(username, backend); + return true; + }; + + // Use template validation function + int validation_result = validate_config_entries>( + pgsql_users, "pgsql_users", pgsql_users_extractor, admindb, true); + + if (validation_result != 0) { + return validation_result; + } + + // If validation passed, proceed with existing INSERT OR REPLACE logic //fprintf(stderr, "Found %d users\n",count); - int i; int rows = 0; admindb->execute("PRAGMA foreign_keys = OFF"); char* q = (char*)"INSERT OR REPLACE INTO pgsql_users (username, password, active, use_ssl, default_hostgroup, transaction_persistent, fast_forward, max_connections, attributes, comment) VALUES ('%s', '%s', %d, %d, %d, %d, %d, %d, '%s','%s')"; diff --git a/test/tap/tests/test_config_validation_comprehensive-t.cpp b/test/tap/tests/test_config_validation_comprehensive-t.cpp new file mode 100644 index 0000000000..df34c0beb6 --- /dev/null +++ b/test/tap/tests/test_config_validation_comprehensive-t.cpp @@ -0,0 +1,645 @@ +/** + * @file test_config_validation_comprehensive-t.cpp + * @brief Comprehensive test for all config validation changes + * + * This test validates the comprehensive changes made to prevent primary key violations + * and mandatory field validation across all config sections: + * 1. MySQL Servers validation (original fix) + * 2. MySQL Users validation + * 3. ProxySQL Servers validation + * 4. PostgreSQL Servers validation + * 5. PostgreSQL Users validation + */ + +#include +#include +#include +#include +#include + +#include "mysql.h" +#include "mysqld_error.h" + +#include "tap.h" +#include "utils.h" +#include "command_line.h" + +using std::string; +using std::vector; +using std::fstream; + +/** + * Create config file with duplicate MySQL users to test validation + */ +void create_invalid_mysql_users_config(const string& config_file_path) { + fstream config_file; + config_file.open(config_file_path, std::ios::out); + + config_file << "datadir=\"/tmp\"\n"; + config_file << "errorlog=\"/tmp/proxysql.log\"\n"; + config_file << "\n"; + config_file << "mysql_users:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " username=\"testuser\"\n"; + config_file << " password=\"testpass\"\n"; + config_file << " backend=1\n"; + config_file << " default_hostgroup=0\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " username=\"testuser\"\n"; // Same username + config_file << " password=\"different\"\n"; + config_file << " backend=1\n"; // Same backend - DUPLICATE! + config_file << " default_hostgroup=1\n"; + config_file << " }\n"; + config_file << ")\n"; + + config_file.close(); +} + +/** + * Create config file with missing username in MySQL users + */ +void create_invalid_mysql_users_missing_username(const string& config_file_path) { + fstream config_file; + config_file.open(config_file_path, std::ios::out); + + config_file << "datadir=\"/tmp\"\n"; + config_file << "errorlog=\"/tmp/proxysql.log\"\n"; + config_file << "\n"; + config_file << "mysql_users:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " username=\"validuser\"\n"; + config_file << " password=\"testpass\"\n"; + config_file << " backend=1\n"; + config_file << " default_hostgroup=0\n"; + config_file << " },\n"; + config_file << " {\n"; + // Missing username field - should cause validation error + config_file << " password=\"testpass2\"\n"; + config_file << " backend=2\n"; + config_file << " default_hostgroup=1\n"; + config_file << " }\n"; + config_file << ")\n"; + + config_file.close(); +} + +/** + * Create config file with duplicate ProxySQL servers + */ +void create_invalid_proxysql_servers_config(const string& config_file_path) { + fstream config_file; + config_file.open(config_file_path, std::ios::out); + + config_file << "datadir=\"/tmp\"\n"; + config_file << "errorlog=\"/tmp/proxysql.log\"\n"; + config_file << "\n"; + config_file << "proxysql_servers:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; + config_file << " port=6032\n"; + config_file << " weight=1000\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; // Same address + config_file << " port=6032\n"; // Same port - DUPLICATE! + config_file << " weight=900\n"; + config_file << " }\n"; + config_file << ")\n"; + + config_file.close(); +} + +/** + * Create config file with missing port in ProxySQL servers + */ +void create_invalid_proxysql_servers_missing_port(const string& config_file_path) { + fstream config_file; + config_file.open(config_file_path, std::ios::out); + + config_file << "datadir=\"/tmp\"\n"; + config_file << "errorlog=\"/tmp/proxysql.log\"\n"; + config_file << "\n"; + config_file << "proxysql_servers:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; + config_file << " port=6032\n"; + config_file << " weight=1000\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " address=\"192.168.1.1\"\n"; + // Missing port field - should cause validation error + config_file << " weight=900\n"; + config_file << " }\n"; + config_file << ")\n"; + + config_file.close(); +} + +/** + * Create config file with duplicate PostgreSQL servers + */ +void create_invalid_pgsql_servers_config(const string& config_file_path) { + fstream config_file; + config_file.open(config_file_path, std::ios::out); + + config_file << "datadir=\"/tmp\"\n"; + config_file << "errorlog=\"/tmp/proxysql.log\"\n"; + config_file << "\n"; + config_file << "pgsql_servers:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; + config_file << " port=5432\n"; + config_file << " hostgroup=0\n"; + config_file << " weight=1000\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; // Same address + config_file << " port=5432\n"; // Same port + config_file << " hostgroup=0\n"; // Same hostgroup - DUPLICATE! + config_file << " weight=900\n"; + config_file << " }\n"; + config_file << ")\n"; + + config_file.close(); +} + +/** + * Create config file with missing hostgroup in PostgreSQL servers + */ +void create_invalid_pgsql_servers_missing_hostgroup(const string& config_file_path) { + fstream config_file; + config_file.open(config_file_path, std::ios::out); + + config_file << "datadir=\"/tmp\"\n"; + config_file << "errorlog=\"/tmp/proxysql.log\"\n"; + config_file << "\n"; + config_file << "pgsql_servers:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; + config_file << " port=5432\n"; + config_file << " hostgroup=0\n"; + config_file << " weight=1000\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " address=\"192.168.1.1\"\n"; + config_file << " port=5433\n"; + // Missing hostgroup field - should cause validation error + config_file << " weight=900\n"; + config_file << " }\n"; + config_file << ")\n"; + + config_file.close(); +} + +/** + * Create config file with duplicate PostgreSQL users + */ +void create_invalid_pgsql_users_config(const string& config_file_path) { + fstream config_file; + config_file.open(config_file_path, std::ios::out); + + config_file << "datadir=\"/tmp\"\n"; + config_file << "errorlog=\"/tmp/proxysql.log\"\n"; + config_file << "\n"; + config_file << "pgsql_users:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " username=\"pguser\"\n"; + config_file << " password=\"pgpass\"\n"; + config_file << " backend=1\n"; + config_file << " default_hostgroup=0\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " username=\"pguser\"\n"; // Same username + config_file << " password=\"different\"\n"; + config_file << " backend=1\n"; // Same backend - DUPLICATE! + config_file << " default_hostgroup=1\n"; + config_file << " }\n"; + config_file << ")\n"; + + config_file.close(); +} + +/** + * Create config file with missing username in PostgreSQL users + */ +void create_invalid_pgsql_users_missing_username(const string& config_file_path) { + fstream config_file; + config_file.open(config_file_path, std::ios::out); + + config_file << "datadir=\"/tmp\"\n"; + config_file << "errorlog=\"/tmp/proxysql.log\"\n"; + config_file << "\n"; + config_file << "pgsql_users:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " username=\"validpguser\"\n"; + config_file << " password=\"pgpass\"\n"; + config_file << " backend=1\n"; + config_file << " default_hostgroup=0\n"; + config_file << " },\n"; + config_file << " {\n"; + // Missing username field - should cause validation error + config_file << " password=\"pgpass2\"\n"; + config_file << " backend=2\n"; + config_file << " default_hostgroup=1\n"; + config_file << " }\n"; + config_file << ")\n"; + + config_file.close(); +} + +/** + * Create valid config file with all sections + */ +void create_valid_comprehensive_config(const string& config_file_path) { + fstream config_file; + config_file.open(config_file_path, std::ios::out); + + config_file << "datadir=\"/tmp\"\n"; + config_file << "errorlog=\"/tmp/proxysql.log\"\n"; + config_file << "\n"; + config_file << "mysql_servers:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; + config_file << " port=3306\n"; + config_file << " hostgroup=0\n"; + config_file << " weight=900\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; + config_file << " port=3307\n"; // Different port - VALID + config_file << " hostgroup=0\n"; + config_file << " weight=800\n"; + config_file << " }\n"; + config_file << ")\n"; + config_file << "\n"; + config_file << "mysql_users:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " username=\"user1\"\n"; + config_file << " password=\"pass1\"\n"; + config_file << " backend=1\n"; + config_file << " default_hostgroup=0\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " username=\"user2\"\n"; // Different username - VALID + config_file << " password=\"pass2\"\n"; + config_file << " backend=1\n"; + config_file << " default_hostgroup=0\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " username=\"user1\"\n"; // Same username + config_file << " password=\"pass3\"\n"; + config_file << " backend=2\n"; // Different backend - VALID + config_file << " default_hostgroup=1\n"; + config_file << " }\n"; + config_file << ")\n"; + config_file << "\n"; + config_file << "proxysql_servers:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; + config_file << " port=6032\n"; + config_file << " weight=1000\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " address=\"192.168.1.1\"\n"; // Different address - VALID + config_file << " port=6032\n"; + config_file << " weight=900\n"; + config_file << " }\n"; + config_file << ")\n"; + config_file << "\n"; + config_file << "pgsql_servers:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; + config_file << " port=5432\n"; + config_file << " hostgroup=0\n"; + config_file << " weight=1000\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; + config_file << " port=5433\n"; // Different port - VALID + config_file << " hostgroup=0\n"; + config_file << " weight=900\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " address=\"192.168.1.1\"\n"; // Different address - VALID + config_file << " port=5432\n"; + config_file << " hostgroup=1\n"; // Different hostgroup - VALID + config_file << " weight=800\n"; + config_file << " }\n"; + config_file << ")\n"; + config_file << "\n"; + config_file << "pgsql_users:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " username=\"pguser1\"\n"; + config_file << " password=\"pgpass1\"\n"; + config_file << " backend=1\n"; + config_file << " default_hostgroup=0\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " username=\"pguser2\"\n"; // Different username - VALID + config_file << " password=\"pgpass2\"\n"; + config_file << " backend=1\n"; + config_file << " default_hostgroup=0\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " username=\"pguser1\"\n"; // Same username + config_file << " password=\"pgpass3\"\n"; + config_file << " backend=2\n"; // Different backend - VALID + config_file << " default_hostgroup=1\n"; + config_file << " }\n"; + config_file << ")\n"; + + config_file.close(); +} + +/** + * Test MySQL users validation + */ +int test_mysql_users_validation(MYSQL* admin, const string& config_file_path) { + diag("Testing MySQL users validation"); + + // Test duplicate users + create_invalid_mysql_users_config(config_file_path); + + string set_config_cmd = "SET mysql-config_file='" + config_file_path + "'"; + MYSQL_QUERY_T(admin, set_config_cmd.c_str()); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + int query_result = mysql_query(admin, "LOAD MYSQL USERS FROM CONFIG"); + ok(query_result != 0, "LOAD MYSQL USERS FROM CONFIG should fail with duplicate users"); + + if (query_result != 0) { + const char* error_msg = mysql_error(admin); + diag("Error message: %s", error_msg); + ok(strstr(error_msg, "validation failed") != nullptr || + strstr(error_msg, "Configuration validation failed") != nullptr || + strstr(error_msg, "duplicate user entry") != nullptr, + "Error message should indicate user validation failure"); + } else { + ok(false, "Error message should indicate user validation failure"); + } + + // Test missing username + create_invalid_mysql_users_missing_username(config_file_path); + + MYSQL_QUERY_T(admin, set_config_cmd.c_str()); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + query_result = mysql_query(admin, "LOAD MYSQL USERS FROM CONFIG"); + ok(query_result != 0, "LOAD MYSQL USERS FROM CONFIG should fail with missing username"); + + return EXIT_SUCCESS; +} + +/** + * Test ProxySQL servers validation + */ +int test_proxysql_servers_validation(MYSQL* admin, const string& config_file_path) { + diag("Testing ProxySQL servers validation"); + + // Test duplicate servers + create_invalid_proxysql_servers_config(config_file_path); + + string set_config_cmd = "SET mysql-config_file='" + config_file_path + "'"; + MYSQL_QUERY_T(admin, set_config_cmd.c_str()); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + int query_result = mysql_query(admin, "LOAD PROXYSQL SERVERS FROM CONFIG"); + ok(query_result != 0, "LOAD PROXYSQL SERVERS FROM CONFIG should fail with duplicate servers"); + + if (query_result != 0) { + const char* error_msg = mysql_error(admin); + diag("Error message: %s", error_msg); + ok(strstr(error_msg, "validation failed") != nullptr || + strstr(error_msg, "Configuration validation failed") != nullptr || + strstr(error_msg, "duplicate server entry") != nullptr, + "Error message should indicate server validation failure"); + } else { + ok(false, "Error message should indicate server validation failure"); + } + + // Test missing port + create_invalid_proxysql_servers_missing_port(config_file_path); + + MYSQL_QUERY_T(admin, set_config_cmd.c_str()); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + query_result = mysql_query(admin, "LOAD PROXYSQL SERVERS FROM CONFIG"); + ok(query_result != 0, "LOAD PROXYSQL SERVERS FROM CONFIG should fail with missing port"); + + return EXIT_SUCCESS; +} + +/** + * Test PostgreSQL servers validation + */ +int test_pgsql_servers_validation(MYSQL* admin, const string& config_file_path) { + diag("Testing PostgreSQL servers validation"); + + // Test duplicate servers + create_invalid_pgsql_servers_config(config_file_path); + + string set_config_cmd = "SET mysql-config_file='" + config_file_path + "'"; + MYSQL_QUERY_T(admin, set_config_cmd.c_str()); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + int query_result = mysql_query(admin, "LOAD PGSQL SERVERS FROM CONFIG"); + ok(query_result != 0, "LOAD PGSQL SERVERS FROM CONFIG should fail with duplicate servers"); + + if (query_result != 0) { + const char* error_msg = mysql_error(admin); + diag("Error message: %s", error_msg); + ok(strstr(error_msg, "validation failed") != nullptr || + strstr(error_msg, "Configuration validation failed") != nullptr || + strstr(error_msg, "duplicate entry") != nullptr, + "Error message should indicate server validation failure"); + } else { + ok(false, "Error message should indicate server validation failure"); + } + + // Test missing hostgroup + create_invalid_pgsql_servers_missing_hostgroup(config_file_path); + + MYSQL_QUERY_T(admin, set_config_cmd.c_str()); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + query_result = mysql_query(admin, "LOAD PGSQL SERVERS FROM CONFIG"); + ok(query_result != 0, "LOAD PGSQL SERVERS FROM CONFIG should fail with missing hostgroup"); + + return EXIT_SUCCESS; +} + +/** + * Test PostgreSQL users validation + */ +int test_pgsql_users_validation(MYSQL* admin, const string& config_file_path) { + diag("Testing PostgreSQL users validation"); + + // Test duplicate users + create_invalid_pgsql_users_config(config_file_path); + + string set_config_cmd = "SET mysql-config_file='" + config_file_path + "'"; + MYSQL_QUERY_T(admin, set_config_cmd.c_str()); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + int query_result = mysql_query(admin, "LOAD PGSQL USERS FROM CONFIG"); + ok(query_result != 0, "LOAD PGSQL USERS FROM CONFIG should fail with duplicate users"); + + if (query_result != 0) { + const char* error_msg = mysql_error(admin); + diag("Error message: %s", error_msg); + ok(strstr(error_msg, "validation failed") != nullptr || + strstr(error_msg, "Configuration validation failed") != nullptr || + strstr(error_msg, "duplicate user entry") != nullptr, + "Error message should indicate user validation failure"); + } else { + ok(false, "Error message should indicate user validation failure"); + } + + // Test missing username + create_invalid_pgsql_users_missing_username(config_file_path); + + MYSQL_QUERY_T(admin, set_config_cmd.c_str()); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + query_result = mysql_query(admin, "LOAD PGSQL USERS FROM CONFIG"); + ok(query_result != 0, "LOAD PGSQL USERS FROM CONFIG should fail with missing username"); + + return EXIT_SUCCESS; +} + +/** + * Test that comprehensive valid config loads successfully + */ +int test_comprehensive_valid_config(MYSQL* admin, const string& config_file_path) { + diag("Testing comprehensive valid configuration loading"); + + create_valid_comprehensive_config(config_file_path); + + string set_config_cmd = "SET mysql-config_file='" + config_file_path + "'"; + MYSQL_QUERY_T(admin, set_config_cmd.c_str()); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + // Clear existing data first + MYSQL_QUERY_T(admin, "DELETE FROM mysql_servers"); + MYSQL_QUERY_T(admin, "DELETE FROM mysql_users"); + MYSQL_QUERY_T(admin, "DELETE FROM proxysql_servers"); + MYSQL_QUERY_T(admin, "DELETE FROM pgsql_servers"); + MYSQL_QUERY_T(admin, "DELETE FROM pgsql_users"); + MYSQL_QUERY_T(admin, "LOAD MYSQL SERVERS TO RUNTIME"); + MYSQL_QUERY_T(admin, "LOAD MYSQL USERS TO RUNTIME"); + MYSQL_QUERY_T(admin, "LOAD PROXYSQL SERVERS TO RUNTIME"); + MYSQL_QUERY_T(admin, "LOAD PGSQL SERVERS TO RUNTIME"); + MYSQL_QUERY_T(admin, "LOAD PGSQL USERS TO RUNTIME"); + + // Load MySQL servers + int query_result = mysql_query(admin, "LOAD MYSQL SERVERS FROM CONFIG"); + ok(query_result == 0, "LOAD MYSQL SERVERS FROM CONFIG should succeed with valid config"); + + // Load MySQL users + query_result = mysql_query(admin, "LOAD MYSQL USERS FROM CONFIG"); + ok(query_result == 0, "LOAD MYSQL USERS FROM CONFIG should succeed with valid config"); + + // Load ProxySQL servers + query_result = mysql_query(admin, "LOAD PROXYSQL SERVERS FROM CONFIG"); + ok(query_result == 0, "LOAD PROXYSQL SERVERS FROM CONFIG should succeed with valid config"); + + // Load PostgreSQL servers + query_result = mysql_query(admin, "LOAD PGSQL SERVERS FROM CONFIG"); + ok(query_result == 0, "LOAD PGSQL SERVERS FROM CONFIG should succeed with valid config"); + + // Load PostgreSQL users + query_result = mysql_query(admin, "LOAD PGSQL USERS FROM CONFIG"); + ok(query_result == 0, "LOAD PGSQL USERS FROM CONFIG should succeed with valid config"); + + // Verify data was loaded + MYSQL_QUERY_T(admin, "SELECT * FROM mysql_servers"); + MYSQL_RES* result = mysql_store_result(admin); + int server_count = mysql_num_rows(result); + mysql_free_result(result); + ok(server_count == 2, "Should have loaded 2 MySQL servers"); + + MYSQL_QUERY_T(admin, "SELECT * FROM mysql_users"); + result = mysql_store_result(admin); + int user_count = mysql_num_rows(result); + mysql_free_result(result); + ok(user_count == 3, "Should have loaded 3 MySQL users"); + + MYSQL_QUERY_T(admin, "SELECT * FROM proxysql_servers"); + result = mysql_store_result(admin); + int proxysql_count = mysql_num_rows(result); + mysql_free_result(result); + ok(proxysql_count == 2, "Should have loaded 2 ProxySQL servers"); + + MYSQL_QUERY_T(admin, "SELECT * FROM pgsql_servers"); + result = mysql_store_result(admin); + int pgsql_server_count = mysql_num_rows(result); + mysql_free_result(result); + ok(pgsql_server_count == 3, "Should have loaded 3 PostgreSQL servers"); + + MYSQL_QUERY_T(admin, "SELECT * FROM pgsql_users"); + result = mysql_store_result(admin); + int pgsql_user_count = mysql_num_rows(result); + mysql_free_result(result); + ok(pgsql_user_count == 3, "Should have loaded 3 PostgreSQL users"); + + return EXIT_SUCCESS; +} + +int main(int argc, char** argv) { + CommandLine cl; + + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return EXIT_FAILURE; + } + + plan(23); // Expecting 23 test assertions + + MYSQL* mysql = mysql_init(NULL); + if (!mysql) { + fprintf(stderr, "Failed to initialize MySQL client\n"); + return exit_status(); + } + + if (!mysql_real_connect(mysql, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) { + fprintf(stderr, "Failed to connect to database: Error: %s\n", mysql_error(mysql)); + return exit_status(); + } + + string config_file_path = "test_comprehensive_config.cnf"; + + // Test MySQL users validation + test_mysql_users_validation(mysql, config_file_path); + + // Test ProxySQL servers validation + test_proxysql_servers_validation(mysql, config_file_path); + + // Test PostgreSQL servers validation + test_pgsql_servers_validation(mysql, config_file_path); + + // Test PostgreSQL users validation + test_pgsql_users_validation(mysql, config_file_path); + + // Test comprehensive valid configuration + test_comprehensive_valid_config(mysql, config_file_path); + + // Clean up + unlink(config_file_path.c_str()); + mysql_close(mysql); + + return exit_status(); +} \ No newline at end of file diff --git a/test/tap/tests/test_mysql_servers_config_validation-t.cpp b/test/tap/tests/test_mysql_servers_config_validation-t.cpp new file mode 100644 index 0000000000..3b4780a82e --- /dev/null +++ b/test/tap/tests/test_mysql_servers_config_validation-t.cpp @@ -0,0 +1,318 @@ +/** + * @file test_mysql_servers_config_validation-t.cpp + * @brief Test pre-validation for PK violations in LOAD MYSQL SERVERS FROM CONFIG + * + * This test validates the changes made to prevent primary key violations + * when loading MySQL servers from configuration file. It tests: + * 1. Duplicate primary key detection (hostgroup_id + hostname + port) + * 2. Mandatory field validation (hostname, hostgroup_id) + * 3. Proper error responses for validation failures + * 4. Atomic operation behavior + */ + +#include +#include +#include +#include +#include + +#include "mysql.h" +#include "mysqld_error.h" + +#include "tap.h" +#include "utils.h" +#include "command_line.h" + +using std::string; +using std::vector; +using std::fstream; + +/** + * Create a config file with duplicate primary keys to test validation + */ +void create_invalid_config_file_with_duplicates(const string& config_file_path) { + fstream config_file; + config_file.open(config_file_path, std::ios::out); + + config_file << "datadir=\"/tmp\"\n"; + config_file << "errorlog=\"/tmp/proxysql.log\"\n"; + config_file << "\n"; + config_file << "mysql_servers:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; + config_file << " port=3306\n"; + config_file << " hostgroup=0\n"; + config_file << " weight=900\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; // Same address + config_file << " port=3306\n"; // Same port + config_file << " hostgroup=0\n"; // Same hostgroup - DUPLICATE! + config_file << " weight=800\n"; + config_file << " }\n"; + config_file << ")\n"; + + config_file.close(); +} + +/** + * Create a valid config file for testing successful loading + */ +void create_valid_config_file(const string& config_file_path) { + fstream config_file; + config_file.open(config_file_path, std::ios::out); + + config_file << "datadir=\"/tmp\"\n"; + config_file << "errorlog=\"/tmp/proxysql.log\"\n"; + config_file << "\n"; + config_file << "mysql_servers:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; + config_file << " port=3306\n"; + config_file << " hostgroup=0\n"; + config_file << " weight=900\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; + config_file << " port=3307\n"; // Different port - VALID + config_file << " hostgroup=0\n"; + config_file << " weight=800\n"; + config_file << " },\n"; + config_file << " {\n"; + config_file << " address=\"192.168.1.1\"\n"; // Different address - VALID + config_file << " port=3306\n"; + config_file << " hostgroup=0\n"; + config_file << " weight=700\n"; + config_file << " }\n"; + config_file << ")\n"; + + config_file.close(); +} + +/** + * Create a config file with missing mandatory fields + */ +void create_invalid_config_file_missing_fields(const string& config_file_path) { + fstream config_file; + config_file.open(config_file_path, std::ios::out); + + config_file << "datadir=\"/tmp\"\n"; + config_file << "errorlog=\"/tmp/proxysql.log\"\n"; + config_file << "\n"; + config_file << "mysql_servers:\n"; + config_file << "(\n"; + config_file << " {\n"; + config_file << " address=\"127.0.0.1\"\n"; + config_file << " port=3306\n"; + config_file << " hostgroup=0\n"; + config_file << " weight=900\n"; + config_file << " },\n"; + config_file << " {\n"; + // Missing address field - should cause validation error + config_file << " port=3307\n"; + config_file << " hostgroup=1\n"; + config_file << " weight=800\n"; + config_file << " }\n"; + config_file << ")\n"; + + config_file.close(); +} + +/** + * Test that duplicate primary keys are properly detected and rejected + */ +int test_duplicate_pk_validation(MYSQL* admin, const string& config_file_path) { + diag("Testing duplicate primary key validation"); + + // Create config file with duplicate entries + create_invalid_config_file_with_duplicates(config_file_path); + + // Set the config file path + string set_config_cmd = "SET mysql-config_file='" + config_file_path + "'"; + MYSQL_QUERY_T(admin, set_config_cmd.c_str()); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + // Attempt to load servers from config - should fail + int query_result = mysql_query(admin, "LOAD MYSQL SERVERS FROM CONFIG"); + + // Should return error (non-zero) + ok(query_result != 0, "LOAD MYSQL SERVERS FROM CONFIG should fail with duplicate PK"); + + if (query_result != 0) { + const char* error_msg = mysql_error(admin); + diag("Error message: %s", error_msg); + + // Check that error message contains validation failure information + ok(strstr(error_msg, "validation failed") != nullptr || + strstr(error_msg, "Configuration validation failed") != nullptr, + "Error message should indicate validation failure"); + } else { + // If query succeeded when it shouldn't have, fail the test + ok(false, "Error message should indicate validation failure"); + } + + return EXIT_SUCCESS; +} + +/** + * Test that missing mandatory fields are properly detected + */ +int test_missing_fields_validation(MYSQL* admin, const string& config_file_path) { + diag("Testing missing mandatory fields validation"); + + // Create config file with missing fields + create_invalid_config_file_missing_fields(config_file_path); + + // Set the config file path + string set_config_cmd = "SET mysql-config_file='" + config_file_path + "'"; + MYSQL_QUERY_T(admin, set_config_cmd.c_str()); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + // Attempt to load servers from config - should fail + int query_result = mysql_query(admin, "LOAD MYSQL SERVERS FROM CONFIG"); + + // Should return error (non-zero) + ok(query_result != 0, "LOAD MYSQL SERVERS FROM CONFIG should fail with missing mandatory fields"); + + if (query_result != 0) { + const char* error_msg = mysql_error(admin); + diag("Error message: %s", error_msg); + + // Check that error message contains validation failure information + ok(strstr(error_msg, "validation failed") != nullptr || + strstr(error_msg, "Configuration validation failed") != nullptr, + "Error message should indicate validation failure"); + } else { + // If query succeeded when it shouldn't have, fail the test + ok(false, "Error message should indicate validation failure"); + } + + return EXIT_SUCCESS; +} + +/** + * Test that valid configuration loads successfully + */ +int test_valid_config_loading(MYSQL* admin, const string& config_file_path) { + diag("Testing valid configuration loading"); + + // Create valid config file + create_valid_config_file(config_file_path); + + // Set the config file path + string set_config_cmd = "SET mysql-config_file='" + config_file_path + "'"; + MYSQL_QUERY_T(admin, set_config_cmd.c_str()); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + // Clear existing servers first + MYSQL_QUERY_T(admin, "DELETE FROM mysql_servers"); + MYSQL_QUERY_T(admin, "LOAD MYSQL SERVERS TO RUNTIME"); + + // Load servers from config - should succeed + int query_result = mysql_query(admin, "LOAD MYSQL SERVERS FROM CONFIG"); + + // Should succeed (return 0) + ok(query_result == 0, "LOAD MYSQL SERVERS FROM CONFIG should succeed with valid config"); + + if (query_result == 0) { + // Verify that servers were actually loaded + MYSQL_QUERY_T(admin, "SELECT * FROM mysql_servers"); + MYSQL_RES* result = mysql_store_result(admin); + int num_rows = mysql_num_rows(result); + mysql_free_result(result); + + ok(num_rows == 3, "Should have loaded 3 servers from valid config"); + diag("Loaded %d servers from config", num_rows); + } else { + const char* error_msg = mysql_error(admin); + diag("Unexpected error: %s", error_msg); + ok(false, "Should have loaded 3 servers from valid config"); + } + + return EXIT_SUCCESS; +} + +/** + * Test atomic operation behavior - either all entries load or none do + */ +int test_atomic_operation(MYSQL* admin, const string& config_file_path) { + diag("Testing atomic operation behavior"); + + // Clear existing servers + MYSQL_QUERY_T(admin, "DELETE FROM mysql_servers"); + MYSQL_QUERY_T(admin, "LOAD MYSQL SERVERS TO RUNTIME"); + + // Verify no servers exist + MYSQL_QUERY_T(admin, "SELECT * FROM mysql_servers"); + MYSQL_RES* result = mysql_store_result(admin); + int initial_count = mysql_num_rows(result); + mysql_free_result(result); + + ok(initial_count == 0, "Should start with no servers"); + + // Create config file with duplicate entries + create_invalid_config_file_with_duplicates(config_file_path); + + string set_config_cmd = "SET mysql-config_file='" + config_file_path + "'"; + MYSQL_QUERY_T(admin, set_config_cmd.c_str()); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + // Attempt to load - should fail + int query_result = mysql_query(admin, "LOAD MYSQL SERVERS FROM CONFIG"); + ok(query_result != 0, "Config loading should fail due to validation errors"); + + // Verify that NO servers were added (atomic operation) + MYSQL_QUERY_T(admin, "SELECT * FROM mysql_servers"); + result = mysql_store_result(admin); + int final_count = mysql_num_rows(result); + mysql_free_result(result); + + ok(final_count == 0, "No servers should be added when validation fails (atomic operation)"); + diag("Server count after failed load: %d", final_count); + + return EXIT_SUCCESS; +} + +int main(int argc, char** argv) { + CommandLine cl; + + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return EXIT_FAILURE; + } + + plan(9); // Expecting 9 test assertions + + MYSQL* mysql = mysql_init(NULL); + if (!mysql) { + fprintf(stderr, "Failed to initialize MySQL client\n"); + return exit_status(); + } + + if (!mysql_real_connect(mysql, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) { + fprintf(stderr, "Failed to connect to database: Error: %s\n", mysql_error(mysql)); + return exit_status(); + } + + string config_file_path = "test_proxysql_config.cnf"; + + // Test 1: Duplicate primary key validation + test_duplicate_pk_validation(mysql, config_file_path); + + // Test 2: Missing mandatory fields validation + test_missing_fields_validation(mysql, config_file_path); + + // Test 3: Valid configuration loading + test_valid_config_loading(mysql, config_file_path); + + // Test 4: Atomic operation behavior + test_atomic_operation(mysql, config_file_path); + + // Clean up + unlink(config_file_path.c_str()); + mysql_close(mysql); + + return exit_status(); +} \ No newline at end of file