/** * @file config_write_unit-t.cpp * @brief Unit tests for ProxySQL_Config Write_*_to_configfile() functions. * * Tests the following functions: * - Write_MySQL_Users_to_configfile() * - Write_Scheduler_to_configfile() * - Write_Restapi_to_configfile() * - Write_MySQL_Query_Rules_to_configfile() * - Write_MySQL_Servers_to_configfile() * - Write_Global_Variables_to_configfile() * - Write_ProxySQL_Servers_to_configfile() * * Each test creates an in-memory SQLite3DB, populates it with the * appropriate table schema and test data, constructs a ProxySQL_Config, * and verifies the formatted output string. */ #include "tap.h" #include "test_globals.h" #include "test_init.h" #include "proxysql.h" #include "proxysql_config.h" #include "sqlite3db.h" #include "ProxySQL_Admin_Tables_Definitions.h" #include #include // ============================================================ // Helper: create in-memory SQLite3DB // ============================================================ static SQLite3DB* create_test_db() { SQLite3DB* db = new SQLite3DB(); db->open((char*)":memory:", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); return db; } // ============================================================ // Write_MySQL_Users_to_configfile() // ============================================================ static void test_write_mysql_users_empty() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_MYSQL_USERS); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_MySQL_Users_to_configfile(data); ok(rc == 0, "Write_MySQL_Users: returns 0 on empty table"); ok(data.find("mysql_users:") != std::string::npos, "Write_MySQL_Users: section header present for empty table"); ok(data.find("username") == std::string::npos, "Write_MySQL_Users: no user fields in empty table"); delete db; } static void test_write_mysql_users_single() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_MYSQL_USERS); db->execute("INSERT INTO mysql_users (username, password, active, use_ssl, " "default_hostgroup, default_schema, schema_locked, transaction_persistent, " "fast_forward, backend, frontend, max_connections, attributes, comment) " "VALUES ('testuser', 'testpass', 1, 0, 1, 'mydb', 0, 1, 0, 1, 1, 1000, '', 'test comment')"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_MySQL_Users_to_configfile(data); ok(rc == 0, "Write_MySQL_Users single: returns 0"); ok(data.find("mysql_users:") != std::string::npos, "Write_MySQL_Users single: section header present"); ok(data.find("\"testuser\"") != std::string::npos, "Write_MySQL_Users single: username value quoted"); ok(data.find("\"testpass\"") != std::string::npos, "Write_MySQL_Users single: password value quoted"); ok(data.find("default_hostgroup=1") != std::string::npos, "Write_MySQL_Users single: default_hostgroup unquoted integer"); ok(data.find("\"mydb\"") != std::string::npos, "Write_MySQL_Users single: default_schema quoted"); ok(data.find("\"test comment\"") != std::string::npos, "Write_MySQL_Users single: comment quoted"); delete db; } static void test_write_mysql_users_multiple() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_MYSQL_USERS); db->execute("INSERT INTO mysql_users (username, password, backend, frontend) " "VALUES ('user1', 'pass1', 1, 1)"); db->execute("INSERT INTO mysql_users (username, password, backend, frontend) " "VALUES ('user2', 'pass2', 1, 0)"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_MySQL_Users_to_configfile(data); ok(rc == 0, "Write_MySQL_Users multiple: returns 0"); ok(data.find("\"user1\"") != std::string::npos, "Write_MySQL_Users multiple: first user present"); ok(data.find("\"user2\"") != std::string::npos, "Write_MySQL_Users multiple: second user present"); // Multiple entries are comma-separated ok(data.find(",\n") != std::string::npos, "Write_MySQL_Users multiple: entries are comma-separated"); delete db; } // ============================================================ // Write_Scheduler_to_configfile() // ============================================================ static void test_write_scheduler_empty() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_SCHEDULER); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_Scheduler_to_configfile(data); ok(rc == 0, "Write_Scheduler empty: returns 0"); ok(data.find("scheduler:") != std::string::npos, "Write_Scheduler empty: section header present"); delete db; } static void test_write_scheduler_single() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_SCHEDULER); db->execute("INSERT INTO scheduler (id, active, interval_ms, filename, arg1, arg2, arg3, arg4, arg5, comment) " "VALUES (1, 1, 5000, '/usr/bin/check.sh', 'arg_one', NULL, NULL, NULL, NULL, 'health check')"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_Scheduler_to_configfile(data); ok(rc == 0, "Write_Scheduler single: returns 0"); ok(data.find("scheduler:") != std::string::npos, "Write_Scheduler single: section header present"); ok(data.find("id=1") != std::string::npos, "Write_Scheduler single: id unquoted"); ok(data.find("interval_ms=5000") != std::string::npos, "Write_Scheduler single: interval_ms present"); ok(data.find("\"/usr/bin/check.sh\"") != std::string::npos, "Write_Scheduler single: filename quoted"); ok(data.find("\"arg_one\"") != std::string::npos, "Write_Scheduler single: arg1 quoted"); ok(data.find("\"health check\"") != std::string::npos, "Write_Scheduler single: comment quoted"); delete db; } static void test_write_scheduler_null_args() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_SCHEDULER); db->execute("INSERT INTO scheduler (id, active, interval_ms, filename, comment) " "VALUES (2, 1, 1000, '/usr/bin/run.sh', '')"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_Scheduler_to_configfile(data); ok(rc == 0, "Write_Scheduler null args: returns 0"); // NULL args should not produce output (addField skips NULL) ok(data.find("arg1") == std::string::npos, "Write_Scheduler null args: arg1 absent when NULL"); ok(data.find("arg2") == std::string::npos, "Write_Scheduler null args: arg2 absent when NULL"); delete db; } // ============================================================ // Write_Restapi_to_configfile() // ============================================================ static void test_write_restapi_empty() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_RESTAPI_ROUTES); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_Restapi_to_configfile(data); ok(rc == 0, "Write_Restapi empty: returns 0"); ok(data.find("restapi_routes:") != std::string::npos, "Write_Restapi empty: section header present"); delete db; } static void test_write_restapi_single() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_RESTAPI_ROUTES); db->execute("INSERT INTO restapi_routes (id, active, timeout_ms, method, uri, script, comment) " "VALUES (1, 1, 5000, 'GET', '/v1/health', '/usr/bin/health.sh', 'health endpoint')"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_Restapi_to_configfile(data); ok(rc == 0, "Write_Restapi single: returns 0"); ok(data.find("id=1") != std::string::npos, "Write_Restapi single: id unquoted"); ok(data.find("timeout_ms=5000") != std::string::npos, "Write_Restapi single: timeout_ms unquoted"); // method is field[3] which uses empty dq="" so unquoted ok(data.find("method=") != std::string::npos, "Write_Restapi single: method present"); ok(data.find("\"/v1/health\"") != std::string::npos, "Write_Restapi single: uri quoted"); ok(data.find("\"/usr/bin/health.sh\"") != std::string::npos, "Write_Restapi single: script quoted"); ok(data.find("\"health endpoint\"") != std::string::npos, "Write_Restapi single: comment quoted"); delete db; } // ============================================================ // Write_MySQL_Query_Rules_to_configfile() // ============================================================ static void test_write_query_rules_empty() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_MYSQL_QUERY_RULES); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_MySQL_Query_Rules_to_configfile(data); ok(rc == 0, "Write_Query_Rules empty: returns 0"); ok(data.find("mysql_query_rules:") != std::string::npos, "Write_Query_Rules empty: section header present"); delete db; } static void test_write_query_rules_single() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_MYSQL_QUERY_RULES); db->execute("INSERT INTO mysql_query_rules " "(rule_id, active, username, schemaname, flagIN, match_digest, " "destination_hostgroup, apply, comment) " "VALUES (1, 1, 'admin', 'testdb', 0, '^SELECT', 10, 1, 'route selects')"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_MySQL_Query_Rules_to_configfile(data); ok(rc == 0, "Write_Query_Rules single: returns 0"); ok(data.find("mysql_query_rules:") != std::string::npos, "Write_Query_Rules single: section header present"); ok(data.find("rule_id=1") != std::string::npos, "Write_Query_Rules single: rule_id unquoted"); ok(data.find("active=1") != std::string::npos, "Write_Query_Rules single: active unquoted"); ok(data.find("\"admin\"") != std::string::npos, "Write_Query_Rules single: username quoted"); ok(data.find("\"^SELECT\"") != std::string::npos, "Write_Query_Rules single: match_digest quoted"); ok(data.find("destination_hostgroup=10") != std::string::npos, "Write_Query_Rules single: destination_hostgroup unquoted"); ok(data.find("apply=1") != std::string::npos, "Write_Query_Rules single: apply unquoted"); delete db; } static void test_write_query_rules_null_fields() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_MYSQL_QUERY_RULES); // Insert a minimal rule — many optional fields will be NULL db->execute("INSERT INTO mysql_query_rules (rule_id, active, apply) " "VALUES (5, 0, 0)"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_MySQL_Query_Rules_to_configfile(data); ok(rc == 0, "Write_Query_Rules nulls: returns 0"); ok(data.find("rule_id=5") != std::string::npos, "Write_Query_Rules nulls: rule_id present"); // NULL fields should not appear (addField skips them) ok(data.find("username") == std::string::npos, "Write_Query_Rules nulls: NULL username absent"); ok(data.find("match_digest") == std::string::npos, "Write_Query_Rules nulls: NULL match_digest absent"); ok(data.find("error_msg") == std::string::npos, "Write_Query_Rules nulls: NULL error_msg absent"); delete db; } // ============================================================ // Write_MySQL_Servers_to_configfile() // ============================================================ static void test_write_mysql_servers_empty() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_MYSQL_SERVERS); // Also create the sub-tables that Write_MySQL_Servers reads db->execute("CREATE TABLE mysql_replication_hostgroups " "(writer_hostgroup INT PRIMARY KEY, reader_hostgroup INT, check_type VARCHAR, comment VARCHAR)"); db->execute("CREATE TABLE mysql_group_replication_hostgroups " "(writer_hostgroup INT PRIMARY KEY, backup_writer_hostgroup INT, reader_hostgroup INT, " "offline_hostgroup INT, active INT, max_writers INT, writer_is_also_reader INT, " "max_transactions_behind INT, comment VARCHAR)"); db->execute("CREATE TABLE mysql_galera_hostgroups " "(writer_hostgroup INT PRIMARY KEY, backup_writer_hostgroup INT, reader_hostgroup INT, " "offline_hostgroup INT, active INT, max_writers INT, writer_is_also_reader INT, " "max_transactions_behind INT, comment VARCHAR)"); db->execute("CREATE TABLE mysql_aws_aurora_hostgroups " "(writer_hostgroup INT PRIMARY KEY, reader_hostgroup INT, active INT, aurora_port INT, " "domain_name VARCHAR, max_lag_ms INT, check_interval_ms INT, check_timeout_ms INT, " "writer_is_also_reader INT, new_reader_weight INT, add_lag_ms INT, min_lag_ms INT, " "lag_num_checks INT, comment VARCHAR)"); db->execute("CREATE TABLE mysql_hostgroup_attributes " "(hostgroup_id INT PRIMARY KEY, max_num_online_servers INT, autocommit INT, " "free_connections_pct INT, init_connect VARCHAR, multiplex INT, connection_warming INT, " "throttle_connections_per_sec INT, ignore_session_variables VARCHAR, " "hostgroup_settings VARCHAR, servers_defaults VARCHAR, comment VARCHAR)"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_MySQL_Servers_to_configfile(data); ok(rc == 0, "Write_MySQL_Servers empty: returns 0"); ok(data.find("mysql_servers:") != std::string::npos, "Write_MySQL_Servers empty: mysql_servers section present"); ok(data.find("mysql_replication_hostgroups:") != std::string::npos, "Write_MySQL_Servers empty: replication_hostgroups section present"); ok(data.find("mysql_group_replication_hostgroups:") != std::string::npos, "Write_MySQL_Servers empty: group_replication_hostgroups section present"); ok(data.find("mysql_galera_hostgroups:") != std::string::npos, "Write_MySQL_Servers empty: galera_hostgroups section present"); ok(data.find("mysql_aws_aurora_hostgroups:") != std::string::npos, "Write_MySQL_Servers empty: aws_aurora_hostgroups section present"); ok(data.find("mysql_hostgroup_attributes:") != std::string::npos, "Write_MySQL_Servers empty: hostgroup_attributes section present"); delete db; } static void test_write_mysql_servers_with_data() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_MYSQL_SERVERS); db->execute("INSERT INTO mysql_servers (hostgroup_id, hostname, port, gtid_port, status, " "weight, compression, max_connections, max_replication_lag, use_ssl, max_latency_ms, comment) " "VALUES (1, '127.0.0.1', 3306, 0, 'ONLINE', 100, 0, 500, 10, 1, 50, 'primary')"); // Create sub-tables (empty) to avoid query errors db->execute("CREATE TABLE mysql_replication_hostgroups " "(writer_hostgroup INT PRIMARY KEY, reader_hostgroup INT, check_type VARCHAR, comment VARCHAR)"); db->execute("CREATE TABLE mysql_group_replication_hostgroups " "(writer_hostgroup INT PRIMARY KEY, backup_writer_hostgroup INT, reader_hostgroup INT, " "offline_hostgroup INT, active INT, max_writers INT, writer_is_also_reader INT, " "max_transactions_behind INT, comment VARCHAR)"); db->execute("CREATE TABLE mysql_galera_hostgroups " "(writer_hostgroup INT PRIMARY KEY, backup_writer_hostgroup INT, reader_hostgroup INT, " "offline_hostgroup INT, active INT, max_writers INT, writer_is_also_reader INT, " "max_transactions_behind INT, comment VARCHAR)"); db->execute("CREATE TABLE mysql_aws_aurora_hostgroups " "(writer_hostgroup INT PRIMARY KEY, reader_hostgroup INT, active INT, aurora_port INT, " "domain_name VARCHAR, max_lag_ms INT, check_interval_ms INT, check_timeout_ms INT, " "writer_is_also_reader INT, new_reader_weight INT, add_lag_ms INT, min_lag_ms INT, " "lag_num_checks INT, comment VARCHAR)"); db->execute("CREATE TABLE mysql_hostgroup_attributes " "(hostgroup_id INT PRIMARY KEY, max_num_online_servers INT, autocommit INT, " "free_connections_pct INT, init_connect VARCHAR, multiplex INT, connection_warming INT, " "throttle_connections_per_sec INT, ignore_session_variables VARCHAR, " "hostgroup_settings VARCHAR, servers_defaults VARCHAR, comment VARCHAR)"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_MySQL_Servers_to_configfile(data); ok(rc == 0, "Write_MySQL_Servers data: returns 0"); ok(data.find("hostgroup_id=1") != std::string::npos, "Write_MySQL_Servers data: hostgroup_id unquoted"); ok(data.find("\"127.0.0.1\"") != std::string::npos, "Write_MySQL_Servers data: hostname quoted"); ok(data.find("port=3306") != std::string::npos, "Write_MySQL_Servers data: port unquoted"); ok(data.find("weight=100") != std::string::npos, "Write_MySQL_Servers data: weight unquoted"); ok(data.find("\"ONLINE\"") != std::string::npos, "Write_MySQL_Servers data: status quoted"); ok(data.find("use_ssl=1") != std::string::npos, "Write_MySQL_Servers data: use_ssl unquoted"); ok(data.find("\"primary\"") != std::string::npos, "Write_MySQL_Servers data: comment quoted"); delete db; } static void test_write_mysql_servers_replication_hostgroups() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_MYSQL_SERVERS); db->execute("CREATE TABLE mysql_replication_hostgroups " "(writer_hostgroup INT PRIMARY KEY, reader_hostgroup INT, check_type VARCHAR, comment VARCHAR)"); db->execute("INSERT INTO mysql_replication_hostgroups VALUES (10, 20, 'read_only', 'repl group')"); db->execute("CREATE TABLE mysql_group_replication_hostgroups " "(writer_hostgroup INT PRIMARY KEY, backup_writer_hostgroup INT, reader_hostgroup INT, " "offline_hostgroup INT, active INT, max_writers INT, writer_is_also_reader INT, " "max_transactions_behind INT, comment VARCHAR)"); db->execute("CREATE TABLE mysql_galera_hostgroups " "(writer_hostgroup INT PRIMARY KEY, backup_writer_hostgroup INT, reader_hostgroup INT, " "offline_hostgroup INT, active INT, max_writers INT, writer_is_also_reader INT, " "max_transactions_behind INT, comment VARCHAR)"); db->execute("CREATE TABLE mysql_aws_aurora_hostgroups " "(writer_hostgroup INT PRIMARY KEY, reader_hostgroup INT, active INT, aurora_port INT, " "domain_name VARCHAR, max_lag_ms INT, check_interval_ms INT, check_timeout_ms INT, " "writer_is_also_reader INT, new_reader_weight INT, add_lag_ms INT, min_lag_ms INT, " "lag_num_checks INT, comment VARCHAR)"); db->execute("CREATE TABLE mysql_hostgroup_attributes " "(hostgroup_id INT PRIMARY KEY, max_num_online_servers INT, autocommit INT, " "free_connections_pct INT, init_connect VARCHAR, multiplex INT, connection_warming INT, " "throttle_connections_per_sec INT, ignore_session_variables VARCHAR, " "hostgroup_settings VARCHAR, servers_defaults VARCHAR, comment VARCHAR)"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_MySQL_Servers_to_configfile(data); ok(rc == 0, "Write_Servers repl hg: returns 0"); ok(data.find("mysql_replication_hostgroups:") != std::string::npos, "Write_Servers repl hg: section header present"); ok(data.find("writer_hostgroup=10") != std::string::npos, "Write_Servers repl hg: writer_hostgroup present"); ok(data.find("reader_hostgroup=20") != std::string::npos, "Write_Servers repl hg: reader_hostgroup present"); ok(data.find("\"read_only\"") != std::string::npos, "Write_Servers repl hg: check_type quoted"); ok(data.find("\"repl group\"") != std::string::npos, "Write_Servers repl hg: comment quoted"); delete db; } // ============================================================ // Write_Global_Variables_to_configfile() // ============================================================ static void test_write_global_variables_empty() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_GLOBAL_VARIABLES); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_Global_Variables_to_configfile(data); ok(rc == 0, "Write_Global_Vars empty: returns 0"); ok(data.empty(), "Write_Global_Vars empty: no output for empty table"); delete db; } static void test_write_global_variables_single_prefix() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_GLOBAL_VARIABLES); db->execute("INSERT INTO global_variables VALUES ('mysql-max_connections', '2048')"); db->execute("INSERT INTO global_variables VALUES ('mysql-default_query_delay', '0')"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_Global_Variables_to_configfile(data); ok(rc == 0, "Write_Global_Vars single prefix: returns 0"); ok(data.find("mysql_variables") != std::string::npos, "Write_Global_Vars single prefix: section header uses underscore"); // The variable name should have the prefix stripped ok(data.find("max_connections=\"2048\"") != std::string::npos, "Write_Global_Vars single prefix: variable name has prefix stripped"); ok(data.find("default_query_delay=\"0\"") != std::string::npos, "Write_Global_Vars single prefix: second variable present"); // Section should start with { and end with } ok(data.find("{\n") != std::string::npos, "Write_Global_Vars single prefix: opening brace present"); ok(data.find("}\n") != std::string::npos, "Write_Global_Vars single prefix: closing brace present"); delete db; } static void test_write_global_variables_multiple_prefixes() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_GLOBAL_VARIABLES); db->execute("INSERT INTO global_variables VALUES ('admin-admin_credentials', 'admin:admin')"); db->execute("INSERT INTO global_variables VALUES ('mysql-max_connections', '1024')"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_Global_Variables_to_configfile(data); ok(rc == 0, "Write_Global_Vars multi prefix: returns 0"); ok(data.find("admin_variables") != std::string::npos, "Write_Global_Vars multi prefix: admin section present"); ok(data.find("mysql_variables") != std::string::npos, "Write_Global_Vars multi prefix: mysql section present"); ok(data.find("admin_credentials=\"admin:admin\"") != std::string::npos, "Write_Global_Vars multi prefix: admin var present with prefix stripped"); ok(data.find("max_connections=\"1024\"") != std::string::npos, "Write_Global_Vars multi prefix: mysql var present with prefix stripped"); delete db; } static void test_write_global_variables_empty_value() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_GLOBAL_VARIABLES); db->execute("INSERT INTO global_variables VALUES ('mysql-blank_val', '')"); db->execute("INSERT INTO global_variables VALUES ('mysql-nonempty_var', 'value')"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_Global_Variables_to_configfile(data); ok(rc == 0, "Write_Global_Vars empty val: returns 0"); // Empty values should be skipped (the code checks strlen) ok(data.find("blank_val") == std::string::npos, "Write_Global_Vars empty val: empty value variable skipped"); ok(data.find("nonempty_var=\"value\"") != std::string::npos, "Write_Global_Vars empty val: non-empty variable present"); delete db; } // ============================================================ // Write_ProxySQL_Servers_to_configfile() // ============================================================ static void test_write_proxysql_servers_empty() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_PROXYSQL_SERVERS); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_ProxySQL_Servers_to_configfile(data); ok(rc == 0, "Write_ProxySQL_Servers empty: returns 0"); ok(data.find("proxysql_servers:") != std::string::npos, "Write_ProxySQL_Servers empty: section header present"); delete db; } static void test_write_proxysql_servers_single() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_PROXYSQL_SERVERS); db->execute("INSERT INTO proxysql_servers VALUES ('proxy1.example.com', 6032, 100, 'node 1')"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_ProxySQL_Servers_to_configfile(data); ok(rc == 0, "Write_ProxySQL_Servers single: returns 0"); ok(data.find("\"proxy1.example.com\"") != std::string::npos, "Write_ProxySQL_Servers single: hostname quoted"); ok(data.find("port=6032") != std::string::npos, "Write_ProxySQL_Servers single: port unquoted"); ok(data.find("weight=100") != std::string::npos, "Write_ProxySQL_Servers single: weight unquoted"); ok(data.find("\"node 1\"") != std::string::npos, "Write_ProxySQL_Servers single: comment quoted"); delete db; } // ============================================================ // Edge cases: special characters and quoting // ============================================================ static void test_write_mysql_users_special_chars() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_MYSQL_USERS); // Username with a double-quote character to test escaping db->execute("INSERT INTO mysql_users (username, password, backend, frontend, comment) " "VALUES ('test\"user', 'pass', 1, 1, 'comment with \"quotes\"')"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_MySQL_Users_to_configfile(data); ok(rc == 0, "Write_MySQL_Users special chars: returns 0"); // addField escapes " to \" ok(data.find("\\\"") != std::string::npos, "Write_MySQL_Users special chars: double quotes escaped in output"); delete db; } static void test_write_scheduler_multiple() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_SCHEDULER); db->execute("INSERT INTO scheduler (id, active, interval_ms, filename, comment) " "VALUES (1, 1, 1000, '/check1.sh', 'first')"); db->execute("INSERT INTO scheduler (id, active, interval_ms, filename, comment) " "VALUES (2, 0, 2000, '/check2.sh', 'second')"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_Scheduler_to_configfile(data); ok(rc == 0, "Write_Scheduler multiple: returns 0"); ok(data.find("\"/check1.sh\"") != std::string::npos, "Write_Scheduler multiple: first entry present"); ok(data.find("\"/check2.sh\"") != std::string::npos, "Write_Scheduler multiple: second entry present"); ok(data.find(",\n") != std::string::npos, "Write_Scheduler multiple: entries comma-separated"); // Verify proper structure: opening and closing ok(data.find("(\n") != std::string::npos, "Write_Scheduler multiple: list opening paren"); ok(data.find("\n)\n") != std::string::npos, "Write_Scheduler multiple: list closing paren"); delete db; } static void test_write_restapi_multiple() { SQLite3DB* db = create_test_db(); db->execute(ADMIN_SQLITE_TABLE_RESTAPI_ROUTES); db->execute("INSERT INTO restapi_routes (id, active, timeout_ms, method, uri, script, comment) " "VALUES (1, 1, 1000, 'GET', '/v1/a', '/a.sh', 'route a')"); db->execute("INSERT INTO restapi_routes (id, active, timeout_ms, method, uri, script, comment) " "VALUES (2, 1, 2000, 'POST', '/v1/b', '/b.sh', 'route b')"); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_Restapi_to_configfile(data); ok(rc == 0, "Write_Restapi multiple: returns 0"); ok(data.find("\"/v1/a\"") != std::string::npos, "Write_Restapi multiple: first route URI present"); ok(data.find("\"/v1/b\"") != std::string::npos, "Write_Restapi multiple: second route URI present"); ok(data.find(",\n") != std::string::npos, "Write_Restapi multiple: entries comma-separated"); delete db; } // ============================================================ // Nonexistent table error handling // ============================================================ static void test_write_mysql_users_no_table() { SQLite3DB* db = create_test_db(); // Do NOT create the mysql_users table ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_MySQL_Users_to_configfile(data); ok(rc == -1, "Write_MySQL_Users no table: returns -1 on missing table"); delete db; } static void test_write_scheduler_no_table() { SQLite3DB* db = create_test_db(); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_Scheduler_to_configfile(data); ok(rc == -1, "Write_Scheduler no table: returns -1 on missing table"); delete db; } static void test_write_restapi_no_table() { SQLite3DB* db = create_test_db(); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_Restapi_to_configfile(data); ok(rc == -1, "Write_Restapi no table: returns -1 on missing table"); delete db; } static void test_write_query_rules_no_table() { SQLite3DB* db = create_test_db(); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_MySQL_Query_Rules_to_configfile(data); ok(rc == -1, "Write_Query_Rules no table: returns -1 on missing table"); delete db; } static void test_write_global_vars_no_table() { SQLite3DB* db = create_test_db(); ProxySQL_Config cfg(db); std::string data; int rc = cfg.Write_Global_Variables_to_configfile(data); ok(rc == -1, "Write_Global_Vars no table: returns -1 on missing table"); delete db; } // ============================================================ // Main // ============================================================ int main() { plan(111); test_init_minimal(); // Write_MySQL_Users_to_configfile test_write_mysql_users_empty(); // 3 test_write_mysql_users_single(); // 7 test_write_mysql_users_multiple(); // 4 test_write_mysql_users_special_chars(); // 2 // Write_Scheduler_to_configfile test_write_scheduler_empty(); // 2 test_write_scheduler_single(); // 7 test_write_scheduler_null_args(); // 3 test_write_scheduler_multiple(); // 6 // Write_Restapi_to_configfile test_write_restapi_empty(); // 2 test_write_restapi_single(); // 7 test_write_restapi_multiple(); // 4 // Write_MySQL_Query_Rules_to_configfile test_write_query_rules_empty(); // 2 test_write_query_rules_single(); // 8 test_write_query_rules_null_fields(); // 5 // Write_MySQL_Servers_to_configfile test_write_mysql_servers_empty(); // 7 test_write_mysql_servers_with_data(); // 8 test_write_mysql_servers_replication_hostgroups(); // 6 // Write_Global_Variables_to_configfile test_write_global_variables_empty(); // 2 test_write_global_variables_single_prefix(); // 6 test_write_global_variables_multiple_prefixes();// 5 test_write_global_variables_empty_value(); // 3 // Write_ProxySQL_Servers_to_configfile test_write_proxysql_servers_empty(); // 2 test_write_proxysql_servers_single(); // 5 // Error handling: missing tables test_write_mysql_users_no_table(); // 1 test_write_scheduler_no_table(); // 1 test_write_restapi_no_table(); // 1 test_write_query_rules_no_table(); // 1 test_write_global_vars_no_table(); // 1 test_cleanup_minimal(); return exit_status(); }