From 81fab6b95f7364b08859537da68576291d73527e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Jaramago=20Fern=C3=A1ndez?= Date: Tue, 13 Jul 2021 18:21:50 +0200 Subject: [PATCH] Added test performing all the operations for SQLite3 server required by #3508 --- test/tap/tests/test_sqlite3_server-t.cpp | 473 +++++++++++++++++++++++ 1 file changed, 473 insertions(+) create mode 100644 test/tap/tests/test_sqlite3_server-t.cpp diff --git a/test/tap/tests/test_sqlite3_server-t.cpp b/test/tap/tests/test_sqlite3_server-t.cpp new file mode 100644 index 000000000..860e86091 --- /dev/null +++ b/test/tap/tests/test_sqlite3_server-t.cpp @@ -0,0 +1,473 @@ +/** + * @file test_sqlite3_server-t.cpp + * @brief Test to perform multiple operations over ProxySQL SQLite3 server. + * @details It performs the following operations: + * - Connects to sqlite3 with a wrong username. + * - Connects to sqlite3 with a right username but wrong password. + * - Successfully connects to sqlite3 and runs: + * + SHOW SCHEMAS + * + SHOW DATABASES + * + SELECT DATABASE() + * + select DATABASE(), USER() limit 1 + * + select @@version_comment limit 1 + * + select @@character_set_client, @@character_set_connection, @@character_set_server, @@character_set_database limit 1 + * - Successfully connects to sqlite3 and runs a variety of queries: + * + CREATE TABLE, SHOW CREATE TABLE, INSERT, SELECT, DROP TABLE... + * + Queries that induce errors: syntax error, duplicate keys, etc... + * - Changes 'sqliteserver-mysql_ifaces' and tries to connect to the new interface. + * - Connects to ProxySQL Admin and performs the following operations: + * + LOAD|SAVE SQLITESERVER TO|FROM RUNTIME|MEMORY|DISK + * + * NOTE: 'sqliteserver-read_only' is completely omitted from this test because + * it's **currently unused**. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "tap.h" +#include "command_line.h" +#include "utils.h" + +using query_spec = std::tuple; + +const int sqlite3_port = 0; + +/** + * @brief Extract the current 'sqliteserver-mysql_ifaces' from ProxySQL config. + * @param proxysql_admin An already opened connection to ProxySQL Admin. + * @return EXIT_SUCCESS, or one of the following error codes: + * - EINVAL if supplied 'proxysql_admin' is NULL. + * - '-1' in case of ProxySQL returns an 'NULL' row for the query selecting + * the variable 'sqliteserver-read_only'. + * - EXIT_FAILURE in case other operation failed. + */ +int get_sqlite3_ifaces(MYSQL* proxysql_admin, std::string& sqlite3_ifaces) { + if (proxysql_admin == NULL) { + return EINVAL; + } + + int res = EXIT_FAILURE; + + MYSQL_QUERY( + proxysql_admin, + "SELECT * FROM global_variables WHERE Variable_name='sqliteserver-mysql_ifaces'" + ); + + MYSQL_RES* admin_res = mysql_store_result(proxysql_admin); + if (!admin_res) { + diag("'mysql_store_result' at line %d failed: %s", __LINE__, mysql_error(proxysql_admin)); + goto cleanup; + } + + { + MYSQL_ROW row = mysql_fetch_row(admin_res); + if (!row || row[0] == nullptr || row[1] == nullptr) { + diag("'mysql_fetch_row' at line %d returned 'NULL'", __LINE__); + res = -1; + goto cleanup; + } + + std::string _sqlite3_ifaces { row[1] }; + sqlite3_ifaces = _sqlite3_ifaces; + res = EXIT_SUCCESS; + } + +cleanup: + + return res; +} + +void fetch_and_discard_results(MYSQL_RES* result, bool verbose=false) { + MYSQL_ROW row = nullptr; + unsigned int num_fields = 0; + unsigned int i = 0; + unsigned int j = 0; + + num_fields = mysql_num_fields(result); + while ((row = mysql_fetch_row(result))) { + unsigned long *lengths = mysql_fetch_lengths(result); + + if (verbose) { + printf("# RowNum_%d: ", j); + } + + for(i = 0; i < num_fields; i++) { + if (verbose) { + printf("[%.*s] ", (int) lengths[i], row[i] ? row[i] : "NULL"); + } + } + + if (verbose) { + printf("\n"); + } + + j++; + } +} + +/** + * @brief Execute the supplied queries and check that the return codes are the + * ones specified. + * + * @param proxysql_sqlite3 An already opened MYSQL connection to ProxySQL + * SQLite3 server. + * @param queries The queries to be performed and check. + */ +void execute_and_check_queries(MYSQL* proxysql_sqlite3, const std::vector& queries) { + for (const auto& supp_query : queries) { + const std::string query = std::get<0>(supp_query); + const int exp_err_code = std::get<1>(supp_query); + + int query_err = mysql_query(proxysql_sqlite3, query.c_str()); + MYSQL_RES* result = mysql_store_result(proxysql_sqlite3); + if (result) { + fetch_and_discard_results(result, true); + mysql_free_result(result); + } + + int m_errno = mysql_errno(proxysql_sqlite3); + const char* m_error = mysql_error(proxysql_sqlite3); + + if (exp_err_code == 0) { + ok( + exp_err_code == m_errno, + "Query '%s' should succeed. Error code: (Expected:'%d' == Actual:'%d')", + query.c_str(), exp_err_code, m_errno + ); + } else { + ok( + exp_err_code == m_errno, + "Query '%s' should fail. Error code: (Expected:'%d' == Actual:'%d'), Err: '%s'", + query.c_str(), exp_err_code, m_errno, m_error + ); + } + } +} + +int extract_sqlite3_host_port(MYSQL* proxysql_admin, std::pair& host_port) { + if (proxysql_admin == nullptr) { return EINVAL; } + int res = EXIT_SUCCESS; + + std::string sqlite3_ifaces {}; + int ifaces_err = get_sqlite3_ifaces(proxysql_admin, sqlite3_ifaces); + + // ProxySQL is likely to have been launched without "--sqlite3-server" flag + if (ifaces_err == -1) { + diag("ProxySQL was launched without '--sqlite3-server' flag"); + res = EXIT_FAILURE; + return res; + } + + // Extract the correct port to connect to SQLite server + std::string::size_type colon_pos = sqlite3_ifaces.find(":"); + if (colon_pos == std::string::npos) { + diag("ProxySQL returned a malformed 'sqliteserver-mysql_ifaces': %s", sqlite3_ifaces.c_str()); + res = EXIT_FAILURE; + return res; + } + + std::string sqlite3_host { sqlite3_ifaces.substr(0, colon_pos) }; + std::string sqlite3_port { sqlite3_ifaces.substr(colon_pos + 1) }; + + // Check that port has valid conversion + char* end_pos = nullptr; + int i_sqlite3_port = std::strtol(sqlite3_port.c_str(), &end_pos, 10); + + if (errno == ERANGE || (end_pos != &sqlite3_port.back() + 1)) { + diag( + "ProxySQL returned a invalid port number within 'sqliteserver-mysql_ifaces': %s", + sqlite3_ifaces.c_str() + ); + res = EXIT_FAILURE; + return res; + } + + if (res == EXIT_SUCCESS) { + host_port = { sqlite3_host, i_sqlite3_port }; + } + + return res; +} + +/** + * @brief List of the pairs holding a series of queries that should be + * successfully performed against ProxySQL SQLite3 server. + */ +std::vector successful_queries { + std::make_tuple("SHOW SCHEMAS", 0), + std::make_tuple("SHOW DATABASES", 0), + std::make_tuple("SELECT DATABASE()", 0), + std::make_tuple("SELECT DATABASE(), USER() LIMIT 1", 0), + std::make_tuple("SELECT @@version_comment LIMIT 1", 0), + std::make_tuple( + "SELECT @@character_set_client, @@character_set_connection," + " @@character_set_server, @@character_set_database LIMIT 1", + 0 + ), + std::make_tuple( + "CREATE TABLE IF NOT EXISTS test_sqlite3_server_p0712(" + " c1 INTEGER PRIMARY KEY AUTOINCREMENT," + " c2 VARCHAR(100)," + " c3 VARCHAR(100)" + ")", + 0 + ), + std::make_tuple("SHOW CREATE TABLE test_sqlite3_server_p0712", 0), + std::make_tuple("SHOW TABLES", 0), + std::make_tuple( + "INSERT INTO test_sqlite3_server_p0712" + " (c2, c3) VALUES ('1234', '1234')", + 0 + ), + std::make_tuple( + "INSERT INTO test_sqlite3_server_p0712" + " (c2, c3) VALUES ('123555555', '12355555')", + 0 + ), + std::make_tuple( + "DELETE FROM test_sqlite3_server_p0712", + 0 + ), + std::make_tuple("DROP TABLE test_sqlite3_server_p0712", 0), + std::make_tuple("SHOW TABLES", 0), +}; + +/** + * @brief List of the pairs holding a series of queries in which *some* + * should fail when executed against ProxySQL SQLite3 server. + */ +std::vector unsuccessful_queries { + std::make_tuple("SHOW CHEMAS", 1045), + std::make_tuple("SHOW DAABASES", 1045), + std::make_tuple("SELECT DAABASE()", 1045), + std::make_tuple("SELECT DAABASE(), USER() LIMIT 1", 1045), + std::make_tuple("SHOW CREATE TABLE test_sqlite3_server_p0712", 0), + std::make_tuple( + "CREATE TABLE IF NOT EXISTS test_sqlite3_server_p0712(" + " c1 INTEGER PRIMARY KEY AUTOINCREMENT," + " c2 VARCHAR(100)," + " c3 VARCHAR(100)" + ")", + 0 + ), + std::make_tuple( + "INSERT INTO test_sqlite3_server_p0712" + " (c2, c3) VALUES ('1234', '1234')", + 0 + ), + std::make_tuple( + "INSERT INTO test_sqlite3_server_p0712" + " (c1, c2, c3) VALUES (1, '1235', '1235')", + 1045 + ), + std::make_tuple( + "USE foobar", + 1045 + ), + std::make_tuple( + "DROP TABLE test_sqlite3_server_p0712_non_existent", + 1045 + ), + std::make_tuple( + "DROP TABLE test_sqlite3_server_p0712", + 0 + ), +}; + +/** + * @brief Perform several admin queries to exercise more paths. + */ +std::vector admin_queries { + "LOAD SQLITESERVER VARIABLES FROM DISK", + "LOAD SQLITESERVER VARIABLES TO RUNTIME", + "SAVE SQLITESERVER VARIABLES FROM RUNTIME", + "SAVE SQLITESERVER VARIABLES TO DISK" +}; + +/** + * @brief Perform several admin queries to exercise more paths. + */ +std::vector sqlite_intf_queries { + "SET sqliteserver-mysql_ifaces='127.0.0.1:6035'", + "LOAD SQLITESERVER VARIABLES TO RUNTIME" +}; + +int main(int argc, char** argv) { + CommandLine cl; + + // plan as many tests as queries + plan( + 2 /* Fail to connect with wrong username and password */ + successful_queries.size() + + unsuccessful_queries.size() + admin_queries.size() + sqlite_intf_queries.size() + + 1 /* Connect to new setup interface */ + ); + + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return EXIT_FAILURE; + } + + MYSQL* proxysql_admin = mysql_init(NULL); + + // Connect to ProxySQL Admin and check current SQLite3 configuration + if ( + !mysql_real_connect( + proxysql_admin, cl.host, cl.admin_username, cl.admin_password, + NULL, cl.admin_port, NULL, 0 + ) + ) { + fprintf( + stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, + mysql_error(proxysql_admin) + ); + return EXIT_FAILURE; + } + + { + std::pair host_port {}; + int host_port_err = extract_sqlite3_host_port(proxysql_admin, host_port); + if (host_port_err) { + diag("Failed to get and parse 'sqliteserver-mysql_ifaces' at line '%d'", __LINE__); + goto cleanup; + } + + MYSQL* proxysql_sqlite3 = mysql_init(NULL); + + // Connect with invalid username + std::string inv_user_err {}; + bool failed_to_connect = false; + if ( + !mysql_real_connect( + proxysql_sqlite3, host_port.first.c_str(), "foobar_user", cl.password, + NULL, host_port.second, NULL, 0 + ) + ) { + inv_user_err = mysql_error(proxysql_sqlite3); + failed_to_connect = true; + } + + ok( + failed_to_connect, + "An invalid user should fail to connect to SQLite3 server, error was: %s", + inv_user_err.c_str() + ); + + // Reinitialize MYSQL handle + mysql_close(proxysql_sqlite3); + proxysql_sqlite3 = mysql_init(NULL); + + // Connect with invalid password + std::string inv_pass_err {}; + failed_to_connect = false; + if ( + !mysql_real_connect( + proxysql_sqlite3, host_port.first.c_str(), cl.username, "foobar_pass", + NULL, host_port.second, NULL, 0 + ) + ) { + inv_pass_err = mysql_error(proxysql_sqlite3); + failed_to_connect = true; + } + + ok( + failed_to_connect, + "An invalid user should fail to connect to SQLite3 server, error was: %s", + inv_pass_err.c_str() + ); + + // Reinitialize MYSQL handle + mysql_close(proxysql_sqlite3); + proxysql_sqlite3 = mysql_init(NULL); + + // Correctly connect to SQLite3 server + if ( + !mysql_real_connect( + proxysql_sqlite3, host_port.first.c_str(), cl.username, cl.password, + NULL, host_port.second, NULL, 0 + ) + ) { + fprintf( + stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, + mysql_error(proxysql_sqlite3) + ); + goto cleanup; + } + + diag("Started performing successful queries"); + execute_and_check_queries(proxysql_sqlite3, successful_queries); + + diag("Started performing failing queries"); + execute_and_check_queries(proxysql_sqlite3, unsuccessful_queries); + + // Reinitialize MYSQL handle + mysql_close(proxysql_sqlite3); + proxysql_sqlite3 = mysql_init(NULL); + + // Change SQLite interface and connect to new port + for (const auto& admin_query : sqlite_intf_queries) { + int query_err = mysql_query(proxysql_admin, admin_query.c_str()); + ok( + query_err == 0, "Admin query '%s' should succeed. Line: %d, Err: '%s'", + admin_query.c_str(), __LINE__, mysql_error(proxysql_admin) + ); + } + + // NOTE: Wait for ProxySQL to reconfigure, changing SQLite3 interface. + // Trying to perform a connection immediately after changing the + // interface could lead to 'EADDRINUSE' in ProxySQL side. + sleep(1); + + // Connect to the new interface + std::pair new_host_port {}; + int ext_intf_err = extract_sqlite3_host_port(proxysql_admin, new_host_port); + if (ext_intf_err) { + diag("Failed to get and parse 'sqliteserver-mysql_ifaces' at line '%d'", __LINE__); + goto cleanup; + } + + // Connect with invalid username + bool success_to_connect = true; + std::string new_intf_conn_err {}; + if ( + !mysql_real_connect( + proxysql_sqlite3, new_host_port.first.c_str(), cl.username, cl.password, + NULL, new_host_port.second, NULL, 0 + ) + ) { + new_intf_conn_err = mysql_error(proxysql_sqlite3); + success_to_connect = false; + } + + ok( + success_to_connect, + "A connection to the new selected interface should success, error was: '%s'", + new_intf_conn_err.c_str() + ); + + mysql_close(proxysql_sqlite3); + + // Perform the final Admin queries + for (const auto& admin_query : admin_queries) { + int query_err = mysql_query(proxysql_admin, admin_query.c_str()); + ok( + query_err == 0, "Admin query '%s' should succeed. Line: %d, Err: '%s'", + admin_query.c_str(), __LINE__, mysql_error(proxysql_admin) + ); + } + } + +cleanup: + + mysql_close(proxysql_admin); + + return exit_status(); +}