/* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, version 2.0, as published by the Free Software Foundation. This program is also distributed with certain software (including but not limited to OpenSSL) that is licensed under separate terms, as designated in a particular file or component or in included license documentation. The authors of MySQL hereby grant you an additional permission to link the program and your derivative works with the separately licensed software that they have included with MySQL. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include #include #include #ifdef RAPIDJSON_NO_SIZETYPEDEFINE // if we build within the server, it will set RAPIDJSON_NO_SIZETYPEDEFINE // globally and require to include my_rapidjson_size_t.h #include "my_rapidjson_size_t.h" #endif #include #include #include #include #include #include "config_builder.h" #include "dim.h" #include "mysql/harness/logging/registry.h" #include "mysql/harness/utility/string.h" // ::join #include "mysql_session.h" #include "process_launcher.h" #include "rest_api_testutils.h" #include "router_component_test.h" #include "router_config.h" #include "mysqlrouter/rest_client.h" using namespace std::chrono_literals; class RestRouterApiTest : public RestApiComponentTest, public ::testing::WithParamInterface {}; static const std::vector kRouterSwaggerPaths{ {"/router/status", "Get status of the application", "status of application", ""}, }; /** * @test check /router/status * * - start router with rest_router module loaded * - GET /router/status * - check response code is 200 and output matches openapi spec */ TEST_P(RestRouterApiTest, ensure_openapi) { const std::string http_hostname = "127.0.0.1"; const std::string http_uri = GetParam().uri + GetParam().api_path; const std::string userfile = create_password_file(); auto config_sections = get_restapi_config("rest_router", userfile, true); // rest_api section is always needed config_sections.push_back(ConfigBuilder::build_section("rest_api", {})); const std::string conf_file{create_config_file( conf_dir_.name(), mysql_harness::join(config_sections, "\n"))}; ProcessWrapper &http_server{launch_router({"-c", conf_file})}; fetch_and_validate_schema_and_resource(GetParam(), http_server); } // **************************************************************************** // Request the resource(s) using supported methods with authentication enabled // and valid credentials // **************************************************************************** static const RestApiTestParams rest_api_valid_methods[]{ {"router_status_get", std::string(rest_api_basepath) + "/router/status", "/router/status", HttpMethod::Get, HttpStatusCode::Ok, kContentTypeJson, kRestApiUsername, kRestApiPassword, /*request_authentication =*/true, { {"/hostname", [](const JsonValue *value) -> void { ASSERT_NE(value, nullptr); ASSERT_TRUE(value->IsString()); // hostname shouldn't be empty ASSERT_GT(value->GetStringLength(), 0); }}, {"/processId", [](const JsonValue *value) -> void { ASSERT_NE(value, nullptr); ASSERT_TRUE(value->IsInt()); ASSERT_GT(value->GetInt(), 0); }}, {"/productEdition", [](const JsonValue *value) -> void { ASSERT_NE(value, nullptr); ASSERT_TRUE(value->IsString()); ASSERT_STREQ(value->GetString(), MYSQL_ROUTER_VERSION_EDITION) << value->GetString(); }}, {"/timeStarted", [](const JsonValue *value) -> void { ASSERT_NE(value, nullptr); ASSERT_TRUE(value->IsString()); ASSERT_TRUE(pattern_found(value->GetString(), kTimestampPattern)) << value->GetString(); }}, {"/version", [](const JsonValue *value) -> void { ASSERT_NE(value, nullptr); ASSERT_TRUE(value->IsString()); ASSERT_TRUE(pattern_found(value->GetString(), MYSQL_ROUTER_VERSION)) << value->GetString(); }}, }, kRouterSwaggerPaths}, {"router_status_no_params", std::string(rest_api_basepath) + "/router/status?someparam", "/router/status", HttpMethod::Get, HttpStatusCode::BadRequest, kContentTypeJsonProblem, kRestApiUsername, kRestApiPassword, /*request_authentication =*/true, {}, kRouterSwaggerPaths}, }; INSTANTIATE_TEST_CASE_P( ValidMethods, RestRouterApiTest, ::testing::ValuesIn(rest_api_valid_methods), [](const ::testing::TestParamInfo &info) { return info.param.test_name; }); // **************************************************************************** // Request the resource(s) using supported methods with authentication enabled // and invalid credentials // **************************************************************************** static const RestApiTestParams rest_api_valid_methods_invalid_auth_params[]{ {"router_status_invalid_auth", std::string(rest_api_basepath) + "/router/status", "/router/status", HttpMethod::Get, HttpStatusCode::Unauthorized, kContentTypeHtmlCharset, kRestApiUsername, "invalid password", /*request_authentication =*/true, {}, kRouterSwaggerPaths}, }; INSTANTIATE_TEST_CASE_P( ValidMethodsInvalidAuth, RestRouterApiTest, ::testing::ValuesIn(rest_api_valid_methods_invalid_auth_params), [](const ::testing::TestParamInfo &info) { return info.param.test_name; }); // **************************************************************************** // Request the resource(s) using unsupported methods with authentication enabled // and valid credentials // **************************************************************************** static const RestApiTestParams rest_api_invalid_methods_params[]{ {"router_status_invalid_methods", std::string(rest_api_basepath) + "/router/status", "/router/status", HttpMethod::Post | HttpMethod::Delete | HttpMethod::Patch | HttpMethod::Head | HttpMethod::Trace | HttpMethod::Options | HttpMethod::Connect, HttpStatusCode::MethodNotAllowed, kContentTypeJsonProblem, kRestApiUsername, kRestApiPassword, /*request_authentication =*/true, RestApiComponentTest::kProblemJsonMethodNotAllowed, kRouterSwaggerPaths}, }; INSTANTIATE_TEST_CASE_P( InvalidMethods, RestRouterApiTest, ::testing::ValuesIn(rest_api_invalid_methods_params), [](const ::testing::TestParamInfo &info) { return info.param.test_name; }); // **************************************************************************** // Configuration errors scenarios // **************************************************************************** /** * @test Start router with the REST routing API plugin [rest_router] and * [http_server] enabled but not the [rest_api] plugin. * */ TEST_F(RestRouterApiTest, rest_api_secion_missing) { const std::string userfile = create_password_file(); auto config_sections = get_restapi_config("rest_router", userfile, /*request_authentication=*/true); const std::string conf_file{create_config_file( conf_dir_.name(), mysql_harness::join(config_sections, "\n"))}; auto &router = launch_router({"-c", conf_file}, EXIT_FAILURE); const auto wait_for_process_exit_timeout{10000ms}; check_exit_code(router, EXIT_FAILURE, wait_for_process_exit_timeout); const std::string router_output = router.get_full_output(); EXPECT_NE(router_output.find("Plugin 'rest_router' needs plugin 'rest_api' " "which is missing in the configuration"), router_output.npos) << router_output; } /** * @test Add [rest_router] twice to the configuration file. Start router. Expect * router to fail providing an error about the duplicate section. * */ TEST_F(RestRouterApiTest, rest_router_section_twice) { const std::string userfile = create_password_file(); auto config_sections = get_restapi_config("rest_router", userfile, /*request_authentication=*/true); // [rest_api] is always required config_sections.push_back(ConfigBuilder::build_section("rest_api", {})); // force [rest_router] twice in the config config_sections.push_back(ConfigBuilder::build_section("rest_router", {})); const std::string conf_file{create_config_file( conf_dir_.name(), mysql_harness::join(config_sections, "\n"))}; auto &router = launch_router({"-c", conf_file}, EXIT_FAILURE); const auto wait_for_process_exit_timeout{10000ms}; check_exit_code(router, EXIT_FAILURE, wait_for_process_exit_timeout); const std::string router_output = router.get_full_output(); EXPECT_NE(router_output.find( "Configuration error: Section 'rest_router' already exists"), router_output.npos) << router_output; } /** * @test Enable [rest_router] using a section key such as [rest_router:A]. Start * router. Expect router to fail providing an error about the use of an * unsupported section key. * */ TEST_F(RestRouterApiTest, rest_router_section_has_key) { const std::string userfile = create_password_file(); auto config_sections = get_restapi_config("rest_router:A", userfile, /*request_authentication=*/true); // [rest_api] is always required config_sections.push_back(ConfigBuilder::build_section("rest_api", {})); const std::string conf_file{create_config_file( conf_dir_.name(), mysql_harness::join(config_sections, "\n"))}; auto &router = launch_router({"-c", conf_file}, EXIT_FAILURE); const auto wait_for_process_exit_timeout{10000ms}; check_exit_code(router, EXIT_FAILURE, wait_for_process_exit_timeout); const std::string router_output = router.get_full_logfile(); EXPECT_NE( router_output.find("plugin 'rest_router' init failed: [rest_router] " "section does not expect a key, found 'A'"), router_output.npos) << router_output; } /** * @test Try to disable authentication although a REST API endpoint/plugin * defines authentication as a MUST. * */ TEST_F(RestRouterApiTest, router_api_no_auth) { const std::string userfile = create_password_file(); auto config_sections = get_restapi_config("rest_router", userfile, /*request_authentication=*/false); // [rest_api] is always required config_sections.push_back(ConfigBuilder::build_section("rest_api", {})); const std::string conf_file{create_config_file( conf_dir_.name(), mysql_harness::join(config_sections, "\n"))}; auto &router = launch_router({"-c", conf_file}, EXIT_FAILURE); const auto wait_for_process_exit_timeout{10000ms}; check_exit_code(router, EXIT_FAILURE, wait_for_process_exit_timeout); const std::string router_output = router.get_full_logfile(); EXPECT_NE(router_output.find("plugin 'rest_router' init failed: option " "require_realm in [rest_router] is required"), router_output.npos) << router_output; } /** * @test Enable authentication for the plugin in question. Reference a realm * that does not exist in the configuration file. */ TEST_F(RestRouterApiTest, invalid_realm) { const std::string userfile = create_password_file(); auto config_sections = get_restapi_config( "rest_router", userfile, /*request_authentication=*/true, "invalidrealm"); // [rest_api] is always required config_sections.push_back(ConfigBuilder::build_section("rest_api", {})); const std::string conf_file{create_config_file( conf_dir_.name(), mysql_harness::join(config_sections, "\n"))}; auto &router = launch_router({"-c", conf_file}, EXIT_FAILURE); const auto wait_for_process_exit_timeout{10000ms}; check_exit_code(router, EXIT_FAILURE, wait_for_process_exit_timeout); const std::string router_output = router.get_full_logfile(); EXPECT_NE( router_output.find("Configuration error: unknown authentication " "realm for [rest_router] '': invalidrealm, known " "realm(s): somerealm"), router_output.npos) << router_output; } int main(int argc, char *argv[]) { init_windows_sockets(); ProcessManager::set_origin(Path(argv[0]).dirname()); ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }