diff --git a/test/tap/tests/pgsql-options_startup_params-t.cpp b/test/tap/tests/pgsql-options_startup_params-t.cpp new file mode 100644 index 000000000..e503a8fc3 --- /dev/null +++ b/test/tap/tests/pgsql-options_startup_params-t.cpp @@ -0,0 +1,433 @@ +/** + * @file pgsql-options_startup_params-t.cpp + * @brief Comprehensive test for startup parameter handling in options + * + * Tests: + * - All ProxySQL tracked variables with spaces (success/failure) + * - Untracked parameters + * - Edge cases (escaped spaces, multiple spaces, trailing spaces) + * - Strict verification of error handling + */ + +#include +#include +#include +#include +#include +#include "libpq-fe.h" +#include "command_line.h" +#include "tap.h" +#include "utils.h" + +CommandLine cl; +using PGConnPtr = std::unique_ptr; + +struct TestCase { + std::string name; + std::string options; + bool should_succeed; // true = expect connection success + std::string expected_error; // substring expected in error message (if should_fail) + std::string description; +}; + +// Helper to create connection +PGConnPtr createConnection(const std::string& options, std::string& error_msg) { + std::stringstream ss; + ss << "host=" << cl.pgsql_host << " port=" << cl.pgsql_port; + ss << " user=" << cl.pgsql_username << " password=" << cl.pgsql_password; + ss << " sslmode=disable"; + if (!options.empty()) { + ss << " options='" << options << "'"; + } + + PGconn* conn = PQconnectdb(ss.str().c_str()); + if (PQstatus(conn) != CONNECTION_OK) { + error_msg = PQerrorMessage(conn); + PQfinish(conn); + return PGConnPtr(nullptr, &PQfinish); + } + error_msg.clear(); + return PGConnPtr(conn, &PQfinish); +} + +// Helper to get variable value +std::string getVar(PGconn* conn, const char* name) { + PGresult* res = PQexec(conn, (std::string("SHOW ") + name).c_str()); + if (!res || PQresultStatus(res) != PGRES_TUPLES_OK) { + PQclear(res); + return ""; + } + char* val = PQgetvalue(res, 0, 0); + std::string result = val ? val : ""; + PQclear(res); + return result; +} + +int main(int argc, char** argv) { + if (cl.getEnv()) return exit_status(); + + // Build comprehensive test cases + std::vector tests; + + // 0. client_encoding (has alias "names") + tests.push_back({"client_encoding", + "-c client_encoding=LATIN1", + true, "", "client_encoding"}); + tests.push_back({"names_alias", + "-c names=UTF8", + true, "", "names alias for client_encoding"}); + + // 1. DateStyle with escaped space + // For escaped space: C++ "\\\\" becomes actual "\\" which libpq sees as "\ " (escaped space) + tests.push_back({"datestyle_escaped", + "-c DateStyle=SQL,\\\\ DMY", + true, "", "DateStyle with escaped space"}); + tests.push_back({"datestyle_postgres", + "-c DateStyle=Postgres,\\\\ DMY", + true, "", "DateStyle Postgres format"}); + tests.push_back({"datestyle_iso_mdy", + "-c DateStyle=ISO,\\\\ MDY", + true, "", "DateStyle ISO MDY"}); + + // 2. IntervalStyle + tests.push_back({"intervalstyle_iso8601", + "-c IntervalStyle=iso_8601", + true, "", "IntervalStyle iso_8601"}); + tests.push_back({"intervalstyle_postgres", + "-c IntervalStyle=postgres", + true, "", "IntervalStyle postgres"}); + + // 3. standard_conforming_strings + tests.push_back({"standard_conforming_strings_off", + "-c standard_conforming_strings=off", + true, "", "standard_conforming_strings=off"}); + tests.push_back({"standard_conforming_strings_on", + "-c standard_conforming_strings=on", + true, "", "standard_conforming_strings=on"}); + + // 4. TimeZone (has alias "TIME ZONE") + tests.push_back({"timezone_pst", + "-c TimeZone=PST8PDT", + true, "", "TimeZone PST8PDT"}); + tests.push_back({"timezone_est", + "-c TimeZone=EST5EDT", + true, "", "TimeZone EST5EDT"}); + tests.push_back({"timezone_utc", + "-c TimeZone=UTC", + true, "", "TimeZone UTC"}); + tests.push_back({"time_zone_alias", + "-c TimeZone=GMT", + true, "", "TimeZone alias"}); + + // Non-critical params (PGSQL_NAME_LAST_LOW_WM+1 to PGSQL_NAME_LAST_HIGH_WM-1) + + // 6. allow_in_place_tablespaces + tests.push_back({"allow_in_place_tablespaces", + "-c allow_in_place_tablespaces=on", + true, "", "allow_in_place_tablespaces"}); + + // 7. bytea_output + tests.push_back({"bytea_output_escape", + "-c bytea_output=escape", + true, "", "bytea_output=escape"}); + tests.push_back({"bytea_output_hex", + "-c bytea_output=hex", + true, "", "bytea_output=hex"}); + + // 8. client_min_messages + tests.push_back({"client_min_messages", + "-c client_min_messages=warning", + true, "", "client_min_messages"}); + + // 9-15. enable_* parameters + tests.push_back({"enable_bitmapscan", + "-c enable_bitmapscan=off", + true, "", "enable_bitmapscan"}); + tests.push_back({"enable_hashjoin", + "-c enable_hashjoin=off", + true, "", "enable_hashjoin"}); + tests.push_back({"enable_indexscan", + "-c enable_indexscan=off", + true, "", "enable_indexscan"}); + tests.push_back({"enable_nestloop", + "-c enable_nestloop=off", + true, "", "enable_nestloop"}); + tests.push_back({"enable_seqscan", + "-c enable_seqscan=off", + true, "", "enable_seqscan"}); + tests.push_back({"enable_sort", + "-c enable_sort=off", + true, "", "enable_sort"}); + + // 16. escape_string_warning + tests.push_back({"escape_string_warning", + "-c escape_string_warning=off", + true, "", "escape_string_warning"}); + + // 17. extra_float_digits + tests.push_back({"extra_float_digits_2", + "-c extra_float_digits=2", + true, "", "extra_float_digits"}); + + // 18. maintenance_work_mem (with space in value, must escape) + tests.push_back({"maintenance_work_mem", + "-c maintenance_work_mem=64MB", + true, "", "maintenance_work_mem"}); + + // 19. search_path (has NO_STRIP_VALUE flag, handles commas specially) + tests.push_back({"searchpath_simple", + "-c search_path=public", + true, "", "search_path simple"}); + tests.push_back({"searchpath_escaped_space", + "-c search_path=public,\\\\ private", + true, "", "search_path with escaped space"}); + tests.push_back({"searchpath_quoted", + "-c search_path=\"\\$user\",public", + true, "", "search_path with quoted user"}); + + // 20. synchronous_commit + tests.push_back({"synchronous_commit", + "-c synchronous_commit=off", + true, "", "synchronous_commit"}); + + // Multiple parameters with proper escaping + tests.push_back({"multiple_critical", + "-c DateStyle=SQL,\\\\ DMY -c TimeZone=PST8PDT -c client_encoding=UTF8", + true, "", "Multiple critical params with escaped space"}); + tests.push_back({"multiple_all", + "-c DateStyle=ISO,\\\\ MDY -c TimeZone=UTC -c client_encoding=UTF8 -c enable_seqscan=off -c bytea_output=escape", + true, "", "Multiple params: critical + non-critical"}); + + // ============================================================ + // FAILURE CASES: Unescaped spaces (should fail) + // ============================================================ + + // DateStyle with unescaped space (the main bug case) + tests.push_back({"datestyle_unescaped", + "-c DateStyle=SQL, DMY", + false, "unescaped space", "DateStyle with unescaped space (should fail)"}); + + // TimeZone with unescaped space + tests.push_back({"timezone_unescaped", + "-c TimeZone=New York", + false, "unescaped space", "TimeZone with unescaped space"}); + + // search_path with unescaped space + tests.push_back({"searchpath_unescaped", + "-c search_path=public, private", + false, "unescaped space", "search_path with unescaped space"}); + + // maintenance_work_mem with unescaped space + tests.push_back({"maintenance_work_mem_unescaped", + "-c maintenance_work_mem=128 MB", + false, "unescaped space", "maintenance_work_mem with unescaped space"}); + + // Trailing space after value + tests.push_back({"trailing_space", + "-c DateStyle=ISO, MDY ", + false, "unescaped space", "Trailing space after value"}); + + // Multiple spaces between options (one unescaped in value) + tests.push_back({"multi_space_fail", + "-c DateStyle=Postgres, DMY -c TimeZone=PST", + false, "unescaped space", "Multiple spaces with unescaped space in first value"}); + + // bytea_output with unescaped space + tests.push_back({"bytea_unescaped", + "-c bytea_output=escape mode", + false, "unescaped space", "bytea_output with unescaped space"}); + + // client_min_messages with unescaped space + tests.push_back({"client_min_messages_unescaped", + "-c client_min_messages=log debug", + false, "unescaped space", "client_min_messages with unescaped space"}); + + // ============================================================ + // UNTRACKED PARAMETERS: Should work but lock hostgroup + // ============================================================ + + // Valid untracked parameter + tests.push_back({"untracked_valid", + "-c geqo=off", + true, "", "Untracked parameter (geqo)"}); + tests.push_back({"untracked_join_collapse_limit", + "-c join_collapse_limit=8", + true, "", "Untracked parameter (join_collapse_limit)"}); + + // ============================================================ + // EDGE CASES: Various escape scenarios + // ============================================================ + + // Multiple spaces needing escape + tests.push_back({"multi_space_escape", + "-c search_path=a,\\\\ b,\\\\ c", + true, "", "Multiple escaped spaces"}); + + // Space at start of value (escaped) + tests.push_back({"space_at_start", + "-c search_path=\\\\ public", + true, "", "Escaped space at start of value"}); + + // ============================================================ + // SPECIAL CASES: Empty and malformed + // ============================================================ + + // Empty options + tests.push_back({"empty_options", + "", + true, "", "Empty options (no options)"}); + + // Just spaces in options + tests.push_back({"only_spaces", + " ", + true, "", "Options with only spaces"}); + + // No equals sign (malformed - should be skipped) + tests.push_back({"no_equals", + "-c DateStyle", + true, "", "Malformed option without ="}); + + // Empty value after equals + tests.push_back({"empty_value", + "-c DateStyle=", + true, "", "Empty value after equals"}); + + // ============================================================ + // PROXY VALIDATION: Verify values are actually applied + // ============================================================ + + // Verify DateStyle is actually set + // This will be verified separately after connection + + const int num_tests = tests.size() + 4; // +4 for verification tests + plan(num_tests); + + int pass_count = 0; + int fail_count = 0; + + // Run all test cases + for (const auto& tc : tests) { + diag("\n=== Test: %s ===", tc.name.c_str()); + diag("Options: '%s'", tc.options.c_str()); + diag("Description: %s", tc.description.c_str()); + + std::string error_msg; + PGConnPtr conn = createConnection(tc.options, error_msg); + bool connected = (conn != nullptr); + + bool test_passed; + if (tc.should_succeed) { + test_passed = connected; + if (!test_passed) { + diag("FAILED: Expected success but got error: %s", error_msg.c_str()); + } + } else { + test_passed = !connected; + if (!test_passed) { + diag("FAILED: Expected failure but connection succeeded"); + } else if (!tc.expected_error.empty()) { + // Verify error message contains expected text + if (error_msg.find(tc.expected_error) == std::string::npos) { + diag("WARNING: Error message doesn't contain '%s'", tc.expected_error.c_str()); + diag("Actual error: %s", error_msg.c_str()); + } + } + } + + ok(test_passed, "%s", tc.description.c_str()); + + if (test_passed) pass_count++; + else fail_count++; + } + + // Additional verification: Check that values are actually applied + diag("\n=== Verification: Check values are actually applied ==="); + + // Test 1: Verify escaped DateStyle is applied correctly + { + diag("\n--- Verifying DateStyle with escaped space is correct ---"); + std::string error; + PGConnPtr conn = createConnection("-c DateStyle=SQL,\\\\ DMY", error); + if (conn) { + std::string val = getVar(conn.get(), "DateStyle"); + bool correct = (val.find("SQL") != std::string::npos && val.find("DMY") != std::string::npos); + diag("Expected: SQL, DMY"); + diag("Got: %s", val.c_str()); + ok(correct, "DateStyle escaped space: value correctly applied"); + } else { + diag("Connection failed: %s", error.c_str()); + ok(false, "DateStyle escaped space: value correctly applied"); + } + } + + // Test 2: Verify partial value auto-completion works + { + diag("\n--- Verifying partial DateStyle (no escape needed) ---"); + std::string error; + PGConnPtr conn = createConnection("-c DateStyle=MDY", error); + if (conn) { + std::string val = getVar(conn.get(), "DateStyle"); + // Should be completed to "ISO, MDY" (format from default, order from param) + bool has_order = (val.find("MDY") != std::string::npos); + diag("Got: %s (should contain MDY)", val.c_str()); + ok(has_order, "DateStyle partial value auto-completed"); + } else { + diag("Connection failed: %s", error.c_str()); + ok(false, "DateStyle partial value auto-completed"); + } + } + + // Test 3: Verify RESET reverts to startup value + { + diag("\n--- Verifying RESET reverts to startup value ---"); + std::string error; + PGConnPtr conn = createConnection("-c DateStyle=SQL,\\\\ DMY", error); + if (conn) { + std::string startup_val = getVar(conn.get(), "DateStyle"); + diag("Startup value: %s", startup_val.c_str()); + + // SET to different value + PGresult* set_res = PQexec(conn.get(), "SET DateStyle = 'Postgres, MDY'"); + PQclear(set_res); + std::string set_val = getVar(conn.get(), "DateStyle"); + diag("After SET: %s", set_val.c_str()); + + // RESET + PGresult* reset_res = PQexec(conn.get(), "RESET DateStyle"); + PQclear(reset_res); + std::string reset_val = getVar(conn.get(), "DateStyle"); + diag("After RESET: %s", reset_val.c_str()); + + bool reverted = (reset_val == startup_val); + ok(reverted, "RESET reverts to startup value"); + } else { + diag("Connection failed: %s", error.c_str()); + ok(false, "RESET reverts to startup value"); + } + } + + // Test 4: Verify search_path with escaped space + { + diag("\n--- Verifying search_path with escaped space ---"); + std::string error; + PGConnPtr conn = createConnection("-c search_path=public,\\\\ private", error); + if (conn) { + std::string val = getVar(conn.get(), "search_path"); + // search_path should contain both schemas + bool has_public = (val.find("public") != std::string::npos); + bool has_private = (val.find("private") != std::string::npos); + diag("Expected: public, private"); + diag("Got: %s", val.c_str()); + ok(has_public && has_private, "search_path with escaped space applied"); + } else { + diag("Connection failed: %s", error.c_str()); + ok(false, "search_path with escaped space applied"); + } + } + + // Summary + diag("\n=== SUMMARY ==="); + diag("Passed: %d, Failed: %d", pass_count, fail_count); + + return exit_status(); +}