485 lines
18 KiB
C++
485 lines
18 KiB
C++
/*
|
|
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 <array>
|
|
#include <chrono>
|
|
#include <thread>
|
|
|
|
#include <gmock/gmock.h>
|
|
#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 <rapidjson/document.h>
|
|
#include <rapidjson/error/en.h>
|
|
#include "mysqlrouter/rest_client.h"
|
|
|
|
#include "rest_api_testutils.h"
|
|
|
|
#include "config_builder.h"
|
|
|
|
static const std::chrono::milliseconds kMaxRestEndpointNotAvailableCheckTime{
|
|
1500};
|
|
static const std::chrono::milliseconds kMaxRestEndpointNotAvailableStepTime{50};
|
|
|
|
static const std::string rest_api_openapi_json =
|
|
std::string(rest_api_basepath) + "/swagger.json";
|
|
|
|
// wait for the endpoint to return 404
|
|
static bool wait_endpoint_404(
|
|
RestClient &rest_client, const std::string &uri,
|
|
std::chrono::milliseconds max_wait_time) noexcept {
|
|
while (max_wait_time.count() > 0) {
|
|
auto req = rest_client.request_sync(HttpMethod::Get, uri);
|
|
|
|
if (req && req.get_response_code() != 0)
|
|
return (req.get_response_code() == 404);
|
|
|
|
auto wait_time =
|
|
std::min(kMaxRestEndpointNotAvailableStepTime, max_wait_time);
|
|
std::this_thread::sleep_for(wait_time);
|
|
|
|
max_wait_time -= wait_time;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void fetch_json(RestClient &rest_client, const std::string &uri,
|
|
JsonDocument &json_doc) {
|
|
request_json(rest_client, uri, HttpMethod::Get, HttpStatusCode::Ok, json_doc);
|
|
}
|
|
|
|
void request_json(RestClient &rest_client, const std::string &uri,
|
|
HttpMethod::type http_method,
|
|
HttpStatusCode::key_type http_status_code,
|
|
JsonDocument &json_doc,
|
|
const std::string &expected_content_type) {
|
|
SCOPED_TRACE("// make a http connections for " + uri);
|
|
auto req = rest_client.request_sync(http_method, uri);
|
|
|
|
SCOPED_TRACE("// checking HTTP response");
|
|
ASSERT_TRUE(req) << "HTTP Request to failed (early): " << req.error_msg()
|
|
<< std::endl;
|
|
|
|
ASSERT_GT(req.get_response_code(), 0u)
|
|
<< "HTTP Request failed: " << req.error_msg() << std::endl;
|
|
|
|
ASSERT_EQ(req.get_response_code(), http_status_code);
|
|
if (!expected_content_type.empty()) {
|
|
ASSERT_THAT(req.get_input_headers().get("Content-Type"),
|
|
::testing::StrEq(expected_content_type));
|
|
}
|
|
|
|
// HEAD doesn't return a body
|
|
if (http_method != HttpMethod::Head &&
|
|
http_status_code != HttpStatusCode::Unauthorized) {
|
|
auto resp_body = req.get_input_buffer();
|
|
ASSERT_GT(resp_body.length(), 0u);
|
|
auto resp_body_content = resp_body.pop_front(resp_body.length());
|
|
|
|
// parse json
|
|
std::string json_payload(resp_body_content.begin(),
|
|
resp_body_content.end());
|
|
|
|
// for methods Options, Connect and Trace libevent returns "not implemented"
|
|
// html
|
|
if (expected_content_type == "text/html") return;
|
|
|
|
json_doc.Parse(json_payload.c_str());
|
|
ASSERT_FALSE(json_doc.HasParseError())
|
|
<< rapidjson::GetParseError_En(json_doc.GetParseError()) << " at pos "
|
|
<< json_doc.GetErrorOffset() << " in document retrieved from " << uri
|
|
<< " :\n"
|
|
<< json_payload;
|
|
}
|
|
}
|
|
|
|
JsonValue *openapi_get_or_deref(JsonDocument &json_doc,
|
|
const JsonPointer &pointer) {
|
|
if (JsonValue *schm = pointer.Get(json_doc)) {
|
|
if (auto *ref = JsonPointer("/$ref").Get(*schm)) {
|
|
// we have a ref, follow it
|
|
return JsonPointer(ref->GetString()).Get(json_doc);
|
|
}
|
|
return schm;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void json_schema_validate(const JsonDocument &json_doc,
|
|
const JsonValue &schema) {
|
|
ASSERT_TRUE(schema.IsObject());
|
|
JsonSchemaDocument schema_doc(schema);
|
|
|
|
JsonSchemaValidator validator(schema_doc);
|
|
ASSERT_TRUE(json_doc.Accept(validator))
|
|
<< validator << "\n"
|
|
<< "schema: " << *validator.GetInvalidSchemaPointer().Get(schema) << "\n"
|
|
<< "document: " << json_doc << "\n";
|
|
}
|
|
|
|
void mark_object_additional_properties(JsonValue &v,
|
|
JsonDocument::AllocatorType &allocator) {
|
|
ASSERT_TRUE(v.IsObject()) << v;
|
|
if (v.HasMember("type")) {
|
|
ASSERT_TRUE(v["type"].IsString());
|
|
|
|
std::string v_type = v["type"].GetString();
|
|
|
|
if (v_type == "object") {
|
|
if (v.HasMember("properties")) {
|
|
ASSERT_TRUE(v["properties"].IsObject());
|
|
for (auto &m : v["properties"].GetObject()) {
|
|
mark_object_additional_properties(m.value, allocator);
|
|
}
|
|
}
|
|
if (!v.HasMember("additionalProperties")) {
|
|
v.AddMember("additionalProperties", false, allocator);
|
|
}
|
|
} else if (v_type == "array") {
|
|
if (v.HasMember("items")) {
|
|
mark_object_additional_properties(v["items"], allocator);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string http_method_to_string(const HttpMethod::type method) {
|
|
switch (method) {
|
|
case HttpMethod::Get:
|
|
return "GET";
|
|
case HttpMethod::Post:
|
|
return "POST";
|
|
case HttpMethod::Head:
|
|
return "HEAD";
|
|
case HttpMethod::Put:
|
|
return "PUT";
|
|
case HttpMethod::Delete:
|
|
return "DELETE";
|
|
case HttpMethod::Options:
|
|
return "OPTIONS";
|
|
case HttpMethod::Trace:
|
|
return "TRACE";
|
|
case HttpMethod::Connect:
|
|
return "CONNECT";
|
|
case HttpMethod::Patch:
|
|
return "PATCH";
|
|
}
|
|
|
|
return "UNKNOWN";
|
|
}
|
|
|
|
bool wait_for_rest_endpoint_ready(
|
|
const std::string &uri, const uint16_t http_port,
|
|
const std::string &username, const std::string &password,
|
|
const std::string &http_host, std::chrono::milliseconds max_wait_time,
|
|
const std::chrono::milliseconds step_time) noexcept {
|
|
IOContext io_ctx;
|
|
RestClient rest_client(io_ctx, http_host, http_port, username, password);
|
|
|
|
while (max_wait_time.count() > 0) {
|
|
auto req = rest_client.request_sync(HttpMethod::Get, uri);
|
|
|
|
if (req && req.get_response_code() != 0 && req.get_response_code() != 404)
|
|
return true;
|
|
|
|
auto wait_time = std::min(step_time, max_wait_time);
|
|
std::this_thread::sleep_for(wait_time);
|
|
|
|
max_wait_time -= wait_time;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
std::string RestApiComponentTest::create_password_file() {
|
|
const std::string userfile =
|
|
mysql_harness::Path(conf_dir_.name()).join("users").str();
|
|
{
|
|
auto &cmd =
|
|
launch_command(get_origin().join("mysqlrouter_passwd").str(),
|
|
{"set", userfile, kRestApiUsername}, EXIT_SUCCESS, true);
|
|
cmd.register_response("Please enter password",
|
|
std::string(kRestApiPassword) + "\n");
|
|
check_exit_code(cmd, EXIT_SUCCESS);
|
|
}
|
|
|
|
return userfile;
|
|
}
|
|
|
|
std::vector<std::string> RestApiComponentTest::get_restapi_config(
|
|
const std::string &component, const std::string &userfile,
|
|
const bool request_authentication, const std::string &realm_name) {
|
|
std::vector<ConfigBuilder::kv_type> authentication;
|
|
if (request_authentication) {
|
|
authentication.push_back({"require_realm", realm_name});
|
|
}
|
|
const std::vector<std::string> config_sections{
|
|
ConfigBuilder::build_section("http_server",
|
|
{
|
|
{"port", std::to_string(http_port_)},
|
|
}),
|
|
ConfigBuilder::build_section(component, authentication),
|
|
ConfigBuilder::build_section("http_auth_realm:somerealm",
|
|
{
|
|
{"backend", "somebackend"},
|
|
{"method", "basic"},
|
|
{"name", "Some Realm"},
|
|
}),
|
|
ConfigBuilder::build_section("http_auth_backend:somebackend",
|
|
{
|
|
{"backend", "file"},
|
|
{"filename", userfile},
|
|
}),
|
|
};
|
|
return config_sections;
|
|
}
|
|
|
|
static void verify_swagger_content(
|
|
const JsonDocument &openapi_json_doc,
|
|
const std::vector<SwaggerPath> &expected_paths) {
|
|
// swagger
|
|
ASSERT_TRUE(openapi_json_doc.HasMember("swagger"));
|
|
const auto &swagger = openapi_json_doc["swagger"];
|
|
ASSERT_TRUE(swagger.IsString());
|
|
ASSERT_STREQ(swagger.GetString(), "2.0");
|
|
|
|
// info
|
|
ASSERT_TRUE(openapi_json_doc.HasMember("info"));
|
|
const auto &info = openapi_json_doc["info"];
|
|
ASSERT_TRUE(info.IsObject());
|
|
|
|
// info/title
|
|
ASSERT_TRUE(info.HasMember("title"));
|
|
const auto &title = info["title"];
|
|
ASSERT_TRUE(title.IsString());
|
|
ASSERT_STREQ(title.GetString(), "MySQL Router");
|
|
|
|
// info/description
|
|
ASSERT_TRUE(info.HasMember("description"));
|
|
const auto &descr = info["description"];
|
|
ASSERT_TRUE(descr.IsString());
|
|
ASSERT_STREQ(descr.GetString(), "API of MySQL Router");
|
|
|
|
// info/version
|
|
ASSERT_TRUE(info.HasMember("version"));
|
|
const auto &version = info["version"];
|
|
ASSERT_TRUE(version.IsString());
|
|
ASSERT_STREQ(version.GetString(), kRestAPIVersion);
|
|
|
|
// paths
|
|
ASSERT_TRUE(openapi_json_doc.HasMember("paths"));
|
|
const auto &paths = openapi_json_doc["paths"];
|
|
ASSERT_TRUE(paths.IsObject());
|
|
for (const auto &expected_path : expected_paths) {
|
|
const char *path_name = expected_path.path_name.c_str();
|
|
ASSERT_TRUE(paths.HasMember(path_name));
|
|
const auto &path = paths[path_name];
|
|
ASSERT_TRUE(path.IsObject());
|
|
|
|
// /path/get
|
|
ASSERT_TRUE(path.HasMember("get"));
|
|
const auto &path_get = path["get"];
|
|
ASSERT_TRUE(path_get.IsObject());
|
|
|
|
// /path/get/description
|
|
ASSERT_TRUE(path_get.HasMember("description"));
|
|
const auto &path_get_desc = path_get["description"];
|
|
ASSERT_TRUE(path_get_desc.IsString());
|
|
ASSERT_STREQ(path_get_desc.GetString(), expected_path.description.c_str());
|
|
|
|
// /path/get/responses
|
|
ASSERT_TRUE(path_get.HasMember("responses"));
|
|
const auto &path_get_responses = path_get["responses"];
|
|
ASSERT_TRUE(path_get_responses.IsObject());
|
|
|
|
// /path/get/responses/200
|
|
ASSERT_TRUE(path_get_responses.HasMember("200"));
|
|
const auto &path_get_response_200 = path_get_responses["200"];
|
|
ASSERT_TRUE(path_get_response_200.IsObject());
|
|
ASSERT_TRUE(path_get_response_200.HasMember("description"));
|
|
const auto &path_get_response_200_desc =
|
|
path_get_response_200["description"];
|
|
ASSERT_TRUE(path_get_response_200_desc.IsString());
|
|
ASSERT_STREQ(path_get_response_200_desc.GetString(),
|
|
expected_path.response_200.c_str());
|
|
|
|
// /path/get/responses/404
|
|
if (expected_path.response_404.empty()) {
|
|
ASSERT_FALSE(path_get_responses.HasMember("404"));
|
|
} else {
|
|
ASSERT_TRUE(path_get_responses.HasMember("404"));
|
|
const auto &path_get_response_404 = path_get_responses["404"];
|
|
ASSERT_TRUE(path_get_response_404.IsObject());
|
|
ASSERT_TRUE(path_get_response_404.HasMember("description"));
|
|
const auto &path_get_response_404_desc =
|
|
path_get_response_404["description"];
|
|
ASSERT_TRUE(path_get_response_404_desc.IsString());
|
|
ASSERT_STREQ(path_get_response_404_desc.GetString(),
|
|
expected_path.response_404.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
void RestApiComponentTest::fetch_and_validate_schema_and_resource(
|
|
const RestApiTestParams &test_params, ProcessWrapper &http_server,
|
|
const std::string &http_hostname) {
|
|
#define STR(s) \
|
|
{ s, strlen(s), rapidjson::kPointerInvalidIndex }
|
|
|
|
const std::array<JsonPointer::Token, 6> schema_pointer_tokens{
|
|
{STR("paths"), STR(test_params.api_path.c_str()), STR("get"),
|
|
STR("responses"), STR("200"), STR("schema")}};
|
|
|
|
#undef STR
|
|
|
|
IOContext io_ctx;
|
|
RestClient rest_client(io_ctx, http_hostname, http_port_,
|
|
test_params.user_name, test_params.user_password);
|
|
|
|
// if 404 is expected make sure this is what we are getting and leave
|
|
if (test_params.status_code == HttpStatusCode::NotFound) {
|
|
ASSERT_TRUE(wait_endpoint_404(rest_client, test_params.uri,
|
|
kMaxRestEndpointNotAvailableCheckTime));
|
|
return;
|
|
}
|
|
|
|
SCOPED_TRACE("// wait for REST endpoint: " + test_params.uri);
|
|
ASSERT_TRUE(wait_for_rest_endpoint_ready(test_params.uri, http_port_,
|
|
test_params.user_name,
|
|
test_params.user_password))
|
|
<< http_server.get_full_output() << http_server.get_full_logfile();
|
|
|
|
for (HttpMethod::pos_type ndx = 0; ndx < HttpMethod::Pos::_LAST; ++ndx) {
|
|
if (test_params.methods.test(ndx)) {
|
|
const auto method = 1 << ndx;
|
|
|
|
SCOPED_TRACE("// fetching openapi spec");
|
|
|
|
JsonDocument openapi_json_doc;
|
|
{
|
|
// if we test for authorization failure this will still return Ok as
|
|
// accessing swagger.json does not require authorization,
|
|
// same with InternalError, BadRequest from a path, that does not affect
|
|
// the swagger
|
|
HttpStatusCode::key_type expected_code =
|
|
test_params.status_code == HttpStatusCode::Unauthorized ||
|
|
test_params.status_code == HttpStatusCode::BadRequest ||
|
|
test_params.status_code == HttpStatusCode::InternalError
|
|
? HttpStatusCode::Ok
|
|
: test_params.status_code;
|
|
std::string expected_content_type =
|
|
test_params.status_code == HttpStatusCode::Unauthorized ||
|
|
test_params.status_code == HttpStatusCode::BadRequest ||
|
|
test_params.status_code == HttpStatusCode::InternalError
|
|
? kContentTypeJson
|
|
: test_params.expected_content_type;
|
|
|
|
// also if the method is HEAD it's not really invalid method for
|
|
// swagger.json file, it's only invalid for the path (API call) itself
|
|
// later
|
|
if (method == HttpMethod::Head &&
|
|
test_params.status_code == HttpStatusCode::MethodNotAllowed) {
|
|
expected_code = HttpStatusCode::Ok;
|
|
expected_content_type = kContentTypeJson;
|
|
}
|
|
|
|
ASSERT_NO_FATAL_FAILURE(request_json(
|
|
rest_client, rest_api_openapi_json, method, expected_code,
|
|
openapi_json_doc, expected_content_type));
|
|
}
|
|
|
|
// verify response against the schema of the openapi spec
|
|
SCOPED_TRACE("// API call");
|
|
JsonDocument json_doc;
|
|
ASSERT_NO_FATAL_FAILURE(request_json(rest_client, test_params.uri, method,
|
|
test_params.status_code, json_doc,
|
|
test_params.expected_content_type));
|
|
|
|
SCOPED_TRACE("// validating schema");
|
|
if (HttpStatusCode::Ok == test_params.status_code) {
|
|
verify_swagger_content(openapi_json_doc, test_params.swagger_paths);
|
|
|
|
auto schema_pointer = JsonPointer(schema_pointer_tokens.data(),
|
|
schema_pointer_tokens.size());
|
|
// points to either a $ref or a schema object
|
|
auto *schema_val =
|
|
openapi_get_or_deref(openapi_json_doc, schema_pointer);
|
|
ASSERT_TRUE(schema_val != nullptr);
|
|
ASSERT_TRUE(schema_val->IsObject());
|
|
|
|
ASSERT_NO_FATAL_FAILURE(mark_object_additional_properties(
|
|
*schema_val, openapi_json_doc.GetAllocator()));
|
|
|
|
ASSERT_NO_FATAL_FAILURE(json_schema_validate(json_doc, *schema_val));
|
|
}
|
|
|
|
SCOPED_TRACE("// validating values");
|
|
for (const auto &kv : test_params.value_checks) {
|
|
validate_value(json_doc, kv.first, kv.second);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*static*/ const std::vector<
|
|
std::pair<std::string, RestApiTestParams::value_check_func>>
|
|
RestApiComponentTest::kProblemJsonMethodNotAllowed{
|
|
{"/status",
|
|
[](const JsonValue *value) -> void {
|
|
ASSERT_NE(value, nullptr);
|
|
|
|
ASSERT_TRUE(value->IsInt());
|
|
ASSERT_EQ(value->GetInt(), HttpStatusCode::MethodNotAllowed);
|
|
}},
|
|
{"/title",
|
|
[](const JsonValue *value) -> void {
|
|
ASSERT_NE(value, nullptr);
|
|
|
|
ASSERT_TRUE(value->IsString());
|
|
ASSERT_STREQ(value->GetString(), "HTTP Method not allowed");
|
|
}},
|
|
{"/detail",
|
|
[](const JsonValue *value) -> void {
|
|
ASSERT_NE(value, nullptr);
|
|
|
|
ASSERT_TRUE(value->IsString());
|
|
ASSERT_STREQ(value->GetString(),
|
|
"only HTTP Methods GET,HEAD are supported");
|
|
}},
|
|
};
|
|
|
|
void RestApiComponentTest::validate_value(
|
|
const JsonDocument &json_doc, const std::string &value_json_pointer,
|
|
const RestApiTestParams::value_check_func value_check) {
|
|
const auto jp = JsonPointer(value_json_pointer.c_str());
|
|
ASSERT_TRUE(jp.IsValid()) << value_json_pointer;
|
|
SCOPED_TRACE("// validating field: " + value_json_pointer);
|
|
ASSERT_NO_FATAL_FAILURE(value_check(jp.Get(json_doc)));
|
|
}
|