1 /*
2 Copyright (c) 2019, 2020, Oracle and/or its affiliates.
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License, version 2.0,
6 as published by the Free Software Foundation.
7
8 This program is also distributed with certain software (including
9 but not limited to OpenSSL) that is licensed under separate terms,
10 as designated in a particular file or component or in included license
11 documentation. The authors of MySQL hereby grant you an additional
12 permission to link the program and your derivative works with the
13 separately licensed software that they have included with MySQL.
14
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
23 */
24
25 #include <array>
26 #include <thread>
27
28 #ifdef RAPIDJSON_NO_SIZETYPEDEFINE
29 // if we build within the server, it will set RAPIDJSON_NO_SIZETYPEDEFINE
30 // globally and require to include my_rapidjson_size_t.h
31 #include "my_rapidjson_size_t.h"
32 #endif
33
34 #include <gmock/gmock.h>
35 #include <rapidjson/document.h>
36 #include <rapidjson/pointer.h>
37 #include <rapidjson/schema.h>
38 #include <rapidjson/stringbuffer.h>
39
40 #include "config_builder.h"
41 #include "dim.h"
42 #include "mysql/harness/utility/string.h" // ::join
43 #include "mysql_session.h"
44 #include "rest_api_testutils.h"
45 #include "router_component_test.h"
46 #include "tcp_port_pool.h"
47 #include "temp_dir.h"
48
49 #include "mysqlrouter/rest_client.h"
50
51 using namespace std::chrono_literals;
52
53 static const std::string http_auth_realm_name("somerealm");
54 static const std::string http_auth_backend_name("somebackend");
55
56 // init_keyring() creates it
57 static const std::string keyring_username("mysql_router1_user");
58
59 static const std::string metadata_cache_section_name("gr_shard_1");
60
61 class RestApiTestBase : public RestApiComponentTest {
62 protected:
SetUp()63 void SetUp() override {
64 RouterComponentTest::SetUp();
65 default_section_ = get_DEFAULT_defaults();
66 init_keyring(default_section_, conf_dir_.name());
67 }
68
69 std::string passwd_filename_;
70 std::map<std::string, std::string> default_section_;
71 };
72
73 static const std::vector<SwaggerPath> kMetadataSwaggerPaths{
74 {"/metadata/{metadataName}/config",
75 "Get config of the metadata cache of a replicaset of a cluster",
76 "config of metadata cache", "cache not found"},
77 {"/metadata/{metadataName}/status",
78 "Get status of the metadata cache of a replicaset of a cluster",
79 "status of metadata cache", "cache not found"},
80 {"/metadata", "Get list of the metadata cache instances",
81 "list of the metadata cache instances", ""},
82 };
83
84 // global variables used by the test
85 size_t g_refresh_failed = 0, g_refresh_succeeded = 0;
86 std::string g_time_last_refresh_succeeded, g_time_last_refresh_failed;
87
88 class RestMetadataCacheApiWithoutClusterTest
89 : public RestApiTestBase,
90 public ::testing::WithParamInterface<RestApiTestParams> {};
91
92 // precondition to these tests is that we can start a router agianst a
93 // metadata-cluster which has no nodes. But with Bug#28352482 (no empty
94 // bootstrap_server_addresses) fixed we can't bring the metadata into that state
95 // anymore. We just won't start.
96 //
97 // An empty dynamic_config file will also not allow to start.
98 //
99 // In case that functionally ever comes back, we'll leave this code around, but
100 // disabled.
TEST_P(RestMetadataCacheApiWithoutClusterTest,DISABLED_ensure_openapi)101 TEST_P(RestMetadataCacheApiWithoutClusterTest, DISABLED_ensure_openapi) {
102 const std::string http_hostname = "127.0.0.1";
103 const std::string userfile = create_password_file();
104 const std::string http_uri = GetParam().uri;
105
106 auto config_sections = get_restapi_config("rest_routing", userfile,
107 GetParam().request_authentication);
108 config_sections.push_back(ConfigBuilder::build_section(
109 "rest_metadata_cache", {
110 {"require_realm", http_auth_realm_name},
111 }));
112 config_sections.push_back(ConfigBuilder::build_section(
113 "metadata_cache:" + metadata_cache_section_name,
114 {
115 {"user", keyring_username},
116 {"ttl", "0.2"},
117 }));
118
119 std::string conf_file{create_config_file(
120 conf_dir_.name(), mysql_harness::join(config_sections, "\n"),
121 &default_section_)};
122 ProcessWrapper &http_server{launch_router({"-c", conf_file})};
123
124 g_refresh_failed = 0;
125 g_time_last_refresh_failed = "";
126
127 fetch_and_validate_schema_and_resource(GetParam(), http_server);
128
129 // this part is relevant only for Get OK, otherwise let's avoid useless sleep
130 if (GetParam().status_code == HttpMethod::Get &&
131 GetParam().methods == HttpStatusCode::Ok) {
132 // sleep a while to make the counters and timestamps change
133 std::this_thread::sleep_for(std::chrono::seconds(1));
134
135 // check the resources again, we want to compare them against the previous
136 // ones
137 fetch_and_validate_schema_and_resource(GetParam(), http_server);
138 }
139 }
140
141 static const RestApiTestParams rest_api_params_without_cluster[]{
142 // The scope of WL#12441 was limited and does not include those
143 // {"cluster_list_no_cluster",
144 // std::string(rest_api_basepath) + "/clusters/",
145 // "/clusters",
146 // HttpMethod::Get,
147 // HttpStatusCode::Ok,
148 // kContentTypeJson,
149 // kRestApiUsername,
150 // kRestApiPassword,
151 // /*request_authentication =*/true,
152 // {
153 // {"/items",
154 // [](const JsonValue *value) {
155 // ASSERT_NE(value, nullptr);
156 // ASSERT_TRUE(value->IsArray());
157 // ASSERT_EQ(value->GetArray().Size(), 0);
158 // }},
159 // }},
160 // {"cluster_nodes_no_cluster",
161 // std::string(rest_api_basepath) + "/clusters/71286871562387612/nodes",
162 // "/clusters/{clusterName}/nodes",
163 // HttpMethod::Get,
164 // HttpStatusCode::Ok,
165 // kContentTypeJson,
166 // kRestApiUsername,
167 // kRestApiPassword,
168 // /*request_authentication =*/true,
169 // {
170 // {"/items",
171 // [](const JsonValue *value) {
172 // ASSERT_NE(value, nullptr);
173 // ASSERT_TRUE(value->IsArray());
174 // ASSERT_EQ(value->GetArray().Size(), 0);
175 // }},
176 // }},
177 {"metadata_list_no_cluster",
178 std::string(rest_api_basepath) + "/metadata/",
179 "/metadata",
180 HttpMethod::Get,
181 HttpStatusCode::Ok,
182 kContentTypeJson,
183 kRestApiUsername,
184 kRestApiPassword,
185 /*request_authentication =*/true,
186 {
187 {"/items",
__anoncfb8e2e40102() 188 [](const JsonValue *value) -> void {
189 ASSERT_NE(value, nullptr);
190 ASSERT_TRUE(value->IsArray());
191 ASSERT_EQ(value->GetArray().Size(), 1);
192 }},
193 {"/items/0/name",
__anoncfb8e2e40202() 194 [](const JsonValue *value) -> void {
195 ASSERT_NE(value, nullptr);
196 ASSERT_TRUE(value->IsString());
197 ASSERT_EQ(value->GetString(), metadata_cache_section_name);
198 }},
199 },
200 kMetadataSwaggerPaths},
201 {"metadata_status_no_cluster",
202 std::string(rest_api_basepath) + "/metadata/" +
203 metadata_cache_section_name + "/status",
204 "/metadata/{metadataName}/status",
205 HttpMethod::Get,
206 HttpStatusCode::Ok,
207 kContentTypeJson,
208 kRestApiUsername,
209 kRestApiPassword,
210 /*request_authentication =*/true,
211 {
212 {"/refreshFailed",
__anoncfb8e2e40302() 213 [](const JsonValue *value) -> void {
214 ASSERT_NE(value, nullptr);
215 ASSERT_TRUE(value->IsInt());
216 ASSERT_GT(value->GetInt(), 0);
217
218 // check if it is more than last time we checked
219 ASSERT_GT(value->GetInt(), g_refresh_failed);
220 g_refresh_failed = static_cast<size_t>(value->GetInt());
221 }},
222 {"/refreshSucceeded",
__anoncfb8e2e40402() 223 [](const JsonValue *value) -> void {
224 ASSERT_NE(value, nullptr);
225 ASSERT_TRUE(value->IsInt());
226 ASSERT_EQ(value->GetInt(), 0);
227 }},
228 {"/timeLastRefreshFailed",
__anoncfb8e2e40502() 229 [](const JsonValue *value) -> void {
230 ASSERT_NE(value, nullptr);
231 ASSERT_TRUE(value->IsString());
232 ASSERT_TRUE(pattern_found(value->GetString(), kTimestampPattern))
233 << value->GetString();
234
235 // check if it is later than last time we checked
236 // timestamp format is YY-MM-DDThh:mm:ss.milisecZ (which we check
237 // above) so lexical string comparison should be fine
238 const std::string currentLastRefreshFailed = value->GetString();
239 ASSERT_TRUE(currentLastRefreshFailed > g_time_last_refresh_failed);
240 // save the current value
241 g_time_last_refresh_failed = currentLastRefreshFailed;
242 }},
243 {"/timeLastRefreshSucceeded",
__anoncfb8e2e40602() 244 [](const JsonValue *value) -> void { ASSERT_EQ(value, nullptr); }},
245 },
246 kMetadataSwaggerPaths},
247 {"metadata_config_no_cluster",
248 std::string(rest_api_basepath) + "/metadata/" +
249 metadata_cache_section_name + "/config",
250 "/metadata/{metadataName}/config",
251 HttpMethod::Get,
252 HttpStatusCode::Ok,
253 kContentTypeJson,
254 kRestApiUsername,
255 kRestApiPassword,
256 /*request_authentication =*/true,
257 {
258 {"/clusterName",
__anoncfb8e2e40702() 259 [](const JsonValue *value) -> void {
260 ASSERT_NE(value, nullptr);
261 ASSERT_TRUE(value->IsString());
262 ASSERT_STREQ(value->GetString(), "");
263 }},
264 {"/groupReplicationId",
__anoncfb8e2e40802() 265 [](const JsonValue *value) -> void {
266 ASSERT_NE(value, nullptr);
267 ASSERT_TRUE(value->IsString());
268 ASSERT_STREQ(value->GetString(), "");
269 }},
270 {"/timeRefreshInMs",
__anoncfb8e2e40902() 271 [](const JsonValue *value) -> void {
272 ASSERT_NE(value, nullptr);
273 ASSERT_TRUE(value->IsInt());
274 ASSERT_EQ(value->GetInt(), 200);
275 }},
276 },
277 kMetadataSwaggerPaths},
278 };
279
280 INSTANTIATE_TEST_SUITE_P(
281 Spec, RestMetadataCacheApiWithoutClusterTest,
282 ::testing::ValuesIn(rest_api_params_without_cluster),
__anoncfb8e2e40a02(const ::testing::TestParamInfo<RestApiTestParams> &info) 283 [](const ::testing::TestParamInfo<RestApiTestParams> &info) {
284 return info.param.test_name;
285 });
286
287 /**
288 * with cluster.
289 */
290 class RestMetadataCacheApiTest
291 : public RestApiTestBase,
292 public ::testing::WithParamInterface<RestApiTestParams> {
293 protected:
294 const uint16_t metadata_server_port_{port_pool_.get_next_available()};
295 };
296
297 /**
298 * wait until metadata has fetched data once.
299 *
300 * @param http_hostname hostname of the http server
301 * @param http_port TCP port of the http server
302 * @param user_name username to authenticate against http server
303 * @param user_password password to authenticate against http server
304 * @param metadata_status_uri URI to metadata status, like
305 * /baseuri/metadata/{section}/status
306 *
307 * uses googletest's ASSERT macros to signal failure
308 */
wait_metadata_fetched(const std::string & http_hostname,uint16_t http_port,const std::string & user_name,const std::string & user_password,const std::string & metadata_status_uri,std::chrono::milliseconds timeout=1s)309 static void wait_metadata_fetched(const std::string &http_hostname,
310 uint16_t http_port,
311 const std::string &user_name,
312 const std::string &user_password,
313 const std::string &metadata_status_uri,
314 std::chrono::milliseconds timeout = 1s) {
315 ASSERT_GT(timeout, 0ms);
316
317 // wait for metadata-cache to finish its first fetch
318 IOContext io_ctx;
319 RestClient rest_client(io_ctx, http_hostname, http_port, user_name,
320 user_password);
321
322 ASSERT_NO_FATAL_FAILURE(wait_for_rest_endpoint_ready(
323 metadata_status_uri, http_port, user_name, user_password));
324
325 const char refresh_sucesseed_json_pointer[] = "/refreshSucceeded";
326 const size_t max_rounds = 10;
327 size_t rounds{};
328 do {
329 JsonDocument json_doc;
330 ASSERT_NO_FATAL_FAILURE(
331 fetch_json(rest_client, metadata_status_uri, json_doc));
332
333 const auto jp = JsonPointer(refresh_sucesseed_json_pointer);
334 ASSERT_TRUE(jp.IsValid());
335 const auto *val = jp.Get(json_doc);
336
337 ASSERT_TRUE(val != nullptr);
338
339 ASSERT_TRUE(val->IsInt());
340 if (val->GetInt() > 0) {
341 break;
342 }
343
344 // only try a few times before we give up
345 ASSERT_LT(rounds, max_rounds)
346 << metadata_status_uri << " " << refresh_sucesseed_json_pointer
347 << " stayed at 0 for too long";
348
349 std::this_thread::sleep_for(timeout / max_rounds);
350 ++rounds;
351 } while (true);
352 }
353
TEST_P(RestMetadataCacheApiTest,ensure_openapi)354 TEST_P(RestMetadataCacheApiTest, ensure_openapi) {
355 const std::string http_hostname = "127.0.0.1";
356 const std::string http_uri = GetParam().uri;
357
358 auto &md_server = ProcessManager::launch_mysql_server_mock(
359 get_data_dir().join("metadata_1_node_repeat.js").str(),
360 metadata_server_port_, EXIT_SUCCESS, false);
361
362 const std::string userfile = create_password_file();
363
364 auto config_sections = get_restapi_config("rest_routing", userfile,
365 GetParam().request_authentication);
366
367 config_sections.push_back(ConfigBuilder::build_section(
368 "rest_metadata_cache", {
369 {"require_realm", http_auth_realm_name},
370 }));
371
372 config_sections.push_back(ConfigBuilder::build_section(
373 "metadata_cache:" + metadata_cache_section_name,
374 {
375 {"user", keyring_username},
376 // name of the cluster in the mock's metadata
377 {"metadata_cluster", "test"},
378 {"ttl", "0.2"},
379 {"bootstrap_server_addresses",
380 "mysql://127.0.0.1:" + std::to_string(metadata_server_port_)},
381 }));
382
383 std::string conf_file{create_config_file(
384 conf_dir_.name(), mysql_harness::join(config_sections, "\n"),
385 &default_section_)};
386
387 // delay the wait until we really need it.
388 ASSERT_NO_FATAL_FAILURE(
389 check_port_ready(md_server, metadata_server_port_, 5000ms));
390 auto &router_proc{launch_router({"-c", conf_file})};
391
392 g_refresh_succeeded = 0;
393 g_time_last_refresh_failed = "";
394
395 if (GetParam().methods == HttpMethod::Get &&
396 GetParam().status_code == HttpStatusCode::Ok) {
397 // waits until /refreshSucceeded increments at least once
398 ASSERT_NO_FATAL_FAILURE(
399 wait_metadata_fetched(http_hostname, http_port_, GetParam().user_name,
400 GetParam().user_password,
401 rest_api_basepath + "/metadata/" +
402 metadata_cache_section_name + "/status"));
403 }
404
405 EXPECT_NO_FATAL_FAILURE(
406 fetch_and_validate_schema_and_resource(GetParam(), router_proc));
407
408 // this part is relevant only for Get OK, otherwise let's avoid useless sleep
409 if (GetParam().methods == HttpMethod::Get &&
410 GetParam().status_code == HttpStatusCode::Ok) {
411 // sleep ~2*TTL to make the counters and timestamps change
412 std::this_thread::sleep_for(500ms);
413
414 // check the resources again, we want to compare them against the previous
415 // ones
416 EXPECT_NO_FATAL_FAILURE(
417 fetch_and_validate_schema_and_resource(GetParam(), router_proc));
418 }
419 }
420
421 // ****************************************************************************
422 // Request the resource(s) using supported methods with authentication enabled
423 // and valid credentials
424 // ****************************************************************************
425
426 static const RestApiTestParams rest_api_valid_methods[]{
427 // The socpe of WL#12441 was limited and does not include those
428 // {"cluster_list",
429 // std::string(rest_api_basepath) + "/clusters/",
430 // "/clusters",
431 // HttpMethod::Get,
432 // HttpStatusCode::Ok,
433 // kContentTypeJson,
434 // kRestApiUsername,
435 // kRestApiPassword,
436 // /*request_authentication =*/true,
437 // {
438 // {"/items",
439 // [](const JsonValue *value) {
440 // ASSERT_NE(value, nullptr);
441 // ASSERT_TRUE(value->IsArray());
442 // ASSERT_EQ(value->GetArray().Size(), 0);
443 // }},
444 // }},
445 // {"cluster_nodes",
446 // std::string(rest_api_basepath) + "/clusters/71286871562387612/nodes",
447 // "/clusters/{clusterName}/nodes",
448 // HttpMethod::Get,
449 // HttpStatusCode::Ok,
450 // kContentTypeJson,
451 // kRestApiUsername,
452 // kRestApiPassword,
453 // /*request_authentication =*/true,
454 // {
455 // {"/items",
456 // [](const JsonValue *value) {
457 // ASSERT_NE(value, nullptr);
458 // ASSERT_TRUE(value->IsArray());
459 // ASSERT_EQ(value->GetArray().Size(), 0);
460 // }},
461 // }},
462 {"metadata_list",
463 std::string(rest_api_basepath) + "/metadata/",
464 "/metadata",
465 HttpMethod::Get,
466 HttpStatusCode::Ok,
467 kContentTypeJson,
468 kRestApiUsername,
469 kRestApiPassword,
470 /*request_authentication =*/true,
471 {
472 {"/items",
__anoncfb8e2e40b02() 473 [](const JsonValue *value) -> void {
474 ASSERT_NE(value, nullptr);
475 ASSERT_TRUE(value->IsArray());
476 ASSERT_EQ(value->GetArray().Size(), 1);
477 }},
478 {"/items/0/name",
__anoncfb8e2e40c02() 479 [](const JsonValue *value) -> void {
480 ASSERT_NE(value, nullptr);
481 ASSERT_TRUE(value->IsString());
482 ASSERT_EQ(value->GetString(), metadata_cache_section_name);
483 }},
484 },
485 kMetadataSwaggerPaths},
486 {"metadata_status",
487 std::string(rest_api_basepath) + "/metadata/" +
488 metadata_cache_section_name + "/status",
489 "/metadata/{metadataName}/status",
490 HttpMethod::Get,
491 HttpStatusCode::Ok,
492 kContentTypeJson,
493 kRestApiUsername,
494 kRestApiPassword,
495 /*request_authentication =*/true,
496 {
497 {"/refreshFailed",
__anoncfb8e2e40d02() 498 [](const JsonValue *value) -> void {
499 ASSERT_NE(value, nullptr);
500 ASSERT_TRUE(value->IsInt());
501 ASSERT_EQ(value->GetInt(), 0);
502 }},
503 {"/refreshSucceeded",
__anoncfb8e2e40e02() 504 [](const JsonValue *value) -> void {
505 ASSERT_NE(value, nullptr);
506 ASSERT_TRUE(value->IsInt());
507
508 // check if it is more than last time we checked
509 ASSERT_GT(value->GetInt(), g_refresh_succeeded);
510 g_refresh_succeeded = static_cast<size_t>(value->GetInt());
511 }},
512 {"/timeLastRefreshSucceeded",
__anoncfb8e2e40f02() 513 [](const JsonValue *value) -> void {
514 ASSERT_NE(value, nullptr);
515 ASSERT_TRUE(value->IsString());
516 ASSERT_TRUE(pattern_found(value->GetString(), kTimestampPattern))
517 << value->GetString();
518
519 // check if it is later than last time we checked
520 // timestamp format is YY-MM-DDThh:mm:ss.milisecZ (which we check
521 // above) so lexical string comparison should be fine
522 const std::string currentLastRefreshSucceeded = value->GetString();
523 ASSERT_TRUE(currentLastRefreshSucceeded >
524 g_time_last_refresh_succeeded);
525 // save the current value
526 g_time_last_refresh_succeeded = currentLastRefreshSucceeded;
527 }},
528 {"/timeLastRefreshFailed",
__anoncfb8e2e41002() 529 [](const JsonValue *value) -> void { ASSERT_EQ(value, nullptr); }},
530 {"/lastRefreshHostname",
__anoncfb8e2e41102() 531 [](const JsonValue *value) -> void {
532 ASSERT_NE(value, nullptr);
533 ASSERT_TRUE(value->IsString());
534 ASSERT_STRNE(value->GetString(), "");
535 }},
536 {"/lastRefreshPort",
__anoncfb8e2e41202() 537 [](const JsonValue *value) -> void {
538 ASSERT_NE(value, nullptr);
539 ASSERT_TRUE(value->IsInt());
540 ASSERT_GT(value->GetInt(), 0);
541 }},
542 },
543 kMetadataSwaggerPaths},
544 {"metadata_config",
545 std::string(rest_api_basepath) + "/metadata/" +
546 metadata_cache_section_name + "/config",
547 "/metadata/{metadataName}/config",
548 HttpMethod::Get,
549 HttpStatusCode::Ok,
550 kContentTypeJson,
551 kRestApiUsername,
552 kRestApiPassword,
553 /*request_authentication =*/true,
554 {
555 {"/clusterName",
__anoncfb8e2e41302() 556 [](const JsonValue *value) -> void {
557 ASSERT_NE(value, nullptr);
558 ASSERT_TRUE(value->IsString());
559 ASSERT_STREQ(value->GetString(), "test");
560 }},
561 {"/groupReplicationId",
__anoncfb8e2e41402() 562 [](const JsonValue *value) -> void {
563 ASSERT_NE(value, nullptr);
564 ASSERT_TRUE(value->IsString());
565 ASSERT_STREQ(value->GetString(), "");
566 }},
567 {"/timeRefreshInMs",
__anoncfb8e2e41502() 568 [](const JsonValue *value) -> void {
569 ASSERT_NE(value, nullptr);
570 ASSERT_TRUE(value->IsUint64());
571 ASSERT_EQ(value->GetUint64(), 200);
572 }},
573 {"/nodes",
__anoncfb8e2e41602() 574 [](const JsonValue *value) -> void {
575 ASSERT_NE(value, nullptr);
576 ASSERT_TRUE(value->IsArray());
577 auto nodes = value->GetArray();
578 ASSERT_GT(nodes.Size(), 0);
579 for (unsigned i = 0; i < nodes.Size(); ++i) {
580 ASSERT_TRUE(nodes[i].IsObject());
581 const auto &node = nodes[i].GetObject();
582
583 ASSERT_TRUE(node.HasMember("hostname"));
584 ASSERT_TRUE(node["hostname"].IsString());
585 ASSERT_STRNE(node["hostname"].GetString(), "");
586
587 ASSERT_TRUE(node.HasMember("port"));
588 ASSERT_TRUE(node["port"].IsInt());
589 ASSERT_GT(node["port"].GetInt(), 0);
590 }
591 }},
592 },
593 kMetadataSwaggerPaths},
594 };
595
596 INSTANTIATE_TEST_SUITE_P(
597 ValidMethods, RestMetadataCacheApiTest,
598 ::testing::ValuesIn(rest_api_valid_methods),
__anoncfb8e2e41702(const ::testing::TestParamInfo<RestApiTestParams> &info) 599 [](const ::testing::TestParamInfo<RestApiTestParams> &info) {
600 return info.param.test_name;
601 });
602
603 // ****************************************************************************
604 // Request non-existing resource(s) using supported methods with authentication
605 // enabled and valid credentials
606 // ****************************************************************************
607
608 static const RestApiTestParams rest_api_non_existig_resouces[]{
609 {"metadata_status_non_existing",
610 std::string(rest_api_basepath) + "/metadata/NON_EXISTING/status",
611 "/metadata/{metadataName}/status",
612 HttpMethod::Get,
613 HttpStatusCode::NotFound,
614 kContentTypeJson,
615 kRestApiUsername,
616 kRestApiPassword,
617 /*request_authentication =*/true,
618 {},
619 kMetadataSwaggerPaths},
620 {"metadata_config_non_existing",
621 std::string(rest_api_basepath) + "/metadata/NON_EXISTING/config",
622 "/metadata/{metadataName}/config",
623 HttpMethod::Get,
624 HttpStatusCode::NotFound,
625 kContentTypeJson,
626 kRestApiUsername,
627 kRestApiPassword,
628 /*request_authentication =*/true,
629 {},
630 kMetadataSwaggerPaths},
631 {"metadata_unsupported_param",
632 std::string(rest_api_basepath) + "/metadata/?limit=10",
633 "/metadata",
634 HttpMethod::Get,
635 HttpStatusCode::BadRequest,
636 kContentTypeJsonProblem,
637 kRestApiUsername,
638 kRestApiPassword,
639 /*request_authentication =*/true,
640 {},
641 kMetadataSwaggerPaths},
642 {"metadata_status_unsupported_param",
643 std::string(rest_api_basepath) + "/metadata/" +
644 metadata_cache_section_name +
645 "/status?refreshFailed=0&refreshSucceeded=1",
646 "/metadata/{metadataName}/status",
647 HttpMethod::Get,
648 HttpStatusCode::BadRequest,
649 kContentTypeJsonProblem,
650 kRestApiUsername,
651 kRestApiPassword,
652 /*request_authentication =*/true,
653 {},
654 kMetadataSwaggerPaths},
655 {"metadata_config_unsupported_param",
656 std::string(rest_api_basepath) + "/metadata/" +
657 metadata_cache_section_name +
658 "/config?refreshFailed=0&refreshSucceeded=1",
659 "/metadata/{metadataName}/config",
660 HttpMethod::Get,
661 HttpStatusCode::BadRequest,
662 kContentTypeJsonProblem,
663 kRestApiUsername,
664 kRestApiPassword,
665 /*request_authentication =*/true,
666 {},
667 kMetadataSwaggerPaths},
668 };
669
670 INSTANTIATE_TEST_SUITE_P(
671 NoNexistingResources, RestMetadataCacheApiTest,
672 ::testing::ValuesIn(rest_api_non_existig_resouces),
__anoncfb8e2e41802(const ::testing::TestParamInfo<RestApiTestParams> &info) 673 [](const ::testing::TestParamInfo<RestApiTestParams> &info) {
674 return info.param.test_name;
675 });
676
677 // ****************************************************************************
678 // Request the resource(s) using supported methods with authentication enabled
679 // and invalid credentials
680 // ****************************************************************************
681
682 static const RestApiTestParams rest_api_valid_methods_invalid_auth_params[]{
683 // The socpe of WL#12441 was limited and does not include those
684 // {"cluster_list_invalid_auth",
685 // std::string(rest_api_basepath) + "/clusters/",
686 // "/clusters",
687 // HttpMethod::Get,
688 // HttpStatusCode::Unauthorized,
689 // kContentTypeHtmlCharset,
690 // kRestApiUsername,
691 // "invalid password",
692 // /*request_authentication =*/true,
693 // {}},
694 // {"cluster_nodes_invalid_auth",
695 // std::string(rest_api_basepath) + "/clusters/71286871562387612/nodes",
696 // "/clusters/{clusterName}/nodes",
697 // HttpMethod::Get,
698 // HttpStatusCode::Unauthorized,
699 // kContentTypeHtmlCharset,
700 // kRestApiUsername,
701 // "invalid password",
702 // /*request_authentication =*/true,
703 // {}},
704 {"metadata_list_invalid_auth",
705 std::string(rest_api_basepath) + "/metadata/",
706 "/metadata",
707 HttpMethod::Get,
708 HttpStatusCode::Unauthorized,
709 kContentTypeHtmlCharset,
710 kRestApiUsername,
711 "invalid password",
712 /*request_authentication =*/true,
713 {},
714 kMetadataSwaggerPaths},
715 {"metadata_status_invalid_auth",
716 std::string(rest_api_basepath) + "/metadata/" +
717 metadata_cache_section_name + "/status",
718 "/metadata/{metadataName}/status",
719 HttpMethod::Get,
720 HttpStatusCode::Unauthorized,
721 kContentTypeHtmlCharset,
722 kRestApiUsername,
723 "invalid password",
724 /*request_authentication =*/true,
725 {},
726 kMetadataSwaggerPaths},
727 {"metadata_config_invalid_auth",
728 std::string(rest_api_basepath) + "/metadata/" +
729 metadata_cache_section_name + "/config",
730 "/metadata/{metadataName}/config",
731 HttpMethod::Get,
732 HttpStatusCode::Unauthorized,
733 kContentTypeHtmlCharset,
734 kRestApiUsername,
735 "invalid password",
736 /*request_authentication =*/true,
737 {},
738 kMetadataSwaggerPaths},
739 };
740
741 INSTANTIATE_TEST_SUITE_P(
742 ValidMethodsInvalidAuth, RestMetadataCacheApiTest,
743 ::testing::ValuesIn(rest_api_valid_methods_invalid_auth_params),
__anoncfb8e2e41902(const ::testing::TestParamInfo<RestApiTestParams> &info) 744 [](const ::testing::TestParamInfo<RestApiTestParams> &info) {
745 return info.param.test_name;
746 });
747
748 // ****************************************************************************
749 // Request the resource(s) using unsupported methods with authentication enabled
750 // and valid credentials
751 // ****************************************************************************
752
753 static const RestApiTestParams rest_api_invalid_methods_params[]{
754 // The socpe of WL#12441 was limited and does not include those
755 // {"cluster_list_invalid_methods",
756 // std::string(rest_api_basepath) + "/clusters/",
757 // "/clusters",
758 // HttpMethod::Post | HttpMethod::Delete | HttpMethod::Patch,
759 // HttpStatusCode::MethodNotAllowed,
760 // kContentTypeJsonProblem,
761 // kRestApiUsername,
762 // kRestApiPassword,
763 // /*request_authentication =*/true,
764 // {
765 // {"/status",
766 // [](const JsonValue *value) -> void {
767 // ASSERT_NE(value, nullptr);
768
769 // ASSERT_TRUE(value->IsInt());
770 // ASSERT_EQ(value->GetInt(), HttpStatusCode::MethodNotAllowed);
771 // }},
772 // }},
773
774 // {"cluster_nodes_invalid_methods",
775 // std::string(rest_api_basepath) + "/clusters/71286871562387612/nodes",
776 // "/clusters/{clusterName}/nodes",
777 // HttpMethod::Post | HttpMethod::Delete | HttpMethod::Patch,
778 // HttpStatusCode::MethodNotAllowed,
779 // kContentTypeJsonProblem,
780 // kRestApiUsername,
781 // kRestApiPassword,
782 // /*request_authentication =*/true,
783 // {
784 // {"/status",
785 // [](const JsonValue *value) -> void {
786 // ASSERT_NE(value, nullptr);
787
788 // ASSERT_TRUE(value->IsInt());
789 // ASSERT_EQ(value->GetInt(), HttpStatusCode::MethodNotAllowed);
790 // }},
791 // }},
792
793 {"metadata_list_invalid_methods",
794 std::string(rest_api_basepath) + "/metadata/", "/metadata",
795 HttpMethod::Post | HttpMethod::Delete | HttpMethod::Patch |
796 HttpMethod::Head | HttpMethod::Trace | HttpMethod::Options |
797 HttpMethod::Connect,
798 HttpStatusCode::MethodNotAllowed, kContentTypeJsonProblem,
799 kRestApiUsername, kRestApiPassword,
800 /*request_authentication =*/true,
801 RestApiComponentTest::kProblemJsonMethodNotAllowed, kMetadataSwaggerPaths},
802
803 {"metadata_status_invalid_methods",
804 std::string(rest_api_basepath) + "/metadata/" +
805 metadata_cache_section_name + "/status",
806 "/metadata/{metadataName}/status",
807 HttpMethod::Post | HttpMethod::Delete | HttpMethod::Patch |
808 HttpMethod::Head | HttpMethod::Trace | HttpMethod::Options |
809 HttpMethod::Connect,
810 HttpStatusCode::MethodNotAllowed, kContentTypeJsonProblem,
811 kRestApiUsername, kRestApiPassword,
812 /*request_authentication =*/true,
813 RestApiComponentTest::kProblemJsonMethodNotAllowed, kMetadataSwaggerPaths},
814
815 {"metadata_config_invalid_methods",
816 std::string(rest_api_basepath) + "/metadata/" +
817 metadata_cache_section_name + "/config",
818 "/metadata/{metadataName}/config",
819 HttpMethod::Post | HttpMethod::Delete | HttpMethod::Patch |
820 HttpMethod::Head | HttpMethod::Trace | HttpMethod::Options |
821 HttpMethod::Connect,
822 HttpStatusCode::MethodNotAllowed, kContentTypeJsonProblem,
823 kRestApiUsername, kRestApiPassword,
824 /*request_authentication =*/true,
825 RestApiComponentTest::kProblemJsonMethodNotAllowed, kMetadataSwaggerPaths},
826 };
827
828 INSTANTIATE_TEST_SUITE_P(
829 InvalidMethods, RestMetadataCacheApiTest,
830 ::testing::ValuesIn(rest_api_invalid_methods_params),
__anoncfb8e2e41a02(const ::testing::TestParamInfo<RestApiTestParams> &info) 831 [](const ::testing::TestParamInfo<RestApiTestParams> &info) {
832 return info.param.test_name;
833 });
834
835 // ****************************************************************************
836 // Configuration errors scenarios
837 // ****************************************************************************
838
839 /**
840 * @test Try to disable authentication although a REST API endpoint/plugin
841 * defines authentication as a MUST.
842 *
843 */
TEST_F(RestMetadataCacheApiTest,metadata_cache_api_no_auth)844 TEST_F(RestMetadataCacheApiTest, metadata_cache_api_no_auth) {
845 const std::string userfile = create_password_file();
846 auto config_sections = get_restapi_config("rest_metadata_cache", userfile,
847 /*request_authentication=*/false);
848
849 const std::string conf_file{create_config_file(
850 conf_dir_.name(), mysql_harness::join(config_sections, "\n"))};
851 auto &router = launch_router({"-c", conf_file}, EXIT_FAILURE);
852
853 // wait until process failed by itself and check the error-msg
854 const auto wait_for_process_exit_timeout{10000ms};
855 check_exit_code(router, EXIT_FAILURE, wait_for_process_exit_timeout);
856
857 const std::string router_output = router.get_full_logfile();
858 EXPECT_THAT(router_output,
859 ::testing::HasSubstr(
860 "plugin 'rest_metadata_cache' init failed: option "
861 "require_realm in [rest_metadata_cache] is required"))
862 << router_output;
863 }
864
865 /**
866 * @test Enable authentication for the plugin in question. Reference a realm
867 * that does not exist in the configuration file.
868 */
TEST_F(RestMetadataCacheApiTest,invalid_realm)869 TEST_F(RestMetadataCacheApiTest, invalid_realm) {
870 const std::string userfile = create_password_file();
871 auto config_sections =
872 get_restapi_config("rest_metadata_cache", userfile,
873 /*request_authentication=*/true, "invalidrealm");
874
875 const std::string conf_file{create_config_file(
876 conf_dir_.name(), mysql_harness::join(config_sections, "\n"))};
877 auto &router = launch_router({"-c", conf_file}, EXIT_FAILURE);
878
879 // wait until process failed by itself and check the error-msg
880 const auto wait_for_process_exit_timeout{10000ms};
881 check_exit_code(router, EXIT_FAILURE, wait_for_process_exit_timeout);
882
883 const std::string router_output = router.get_full_logfile();
884 EXPECT_THAT(
885 router_output,
886 ::testing::HasSubstr(
887 "Configuration error: The option 'require_realm=invalidrealm' "
888 "in [rest_metadata_cache] does not match any http_auth_realm."))
889 << router_output;
890 }
891
892 /**
893 * @test Start router with the REST routing API plugin [rest_metadata_cache],
894 * [http_plugin] and [metadata_cache] enabled but not the [rest_api] plugin.
895 *
896 */
TEST_F(RestMetadataCacheApiTest,metadata_cache_api_no_rest_api)897 TEST_F(RestMetadataCacheApiTest, metadata_cache_api_no_rest_api) {
898 const std::string userfile = create_password_file();
899 auto config_sections = get_restapi_config("rest_metadata_cache", userfile,
900 /*request_authentication=*/true);
901
902 const std::string conf_file{create_config_file(
903 conf_dir_.name(), mysql_harness::join(config_sections, "\n"))};
904 launch_router({"-c", conf_file}, EXIT_SUCCESS);
905
906 // wait until signal handler is up before we let the teardown of the test
907 // terminate the router and check its exit-code
908 //
909 // should be removed once we have another way to know that the process is
910 // ready to receive a shutdown signal.
911 std::this_thread::sleep_for(100ms);
912 }
913
914 /**
915 * @test Start router with the REST routing API plugin [rest_metadata_cache] and
916 * [http_plugin] enabled but not the [metadata_cache] plugin.
917 *
918 * disabled for now as we can't declare the requirement in the plugin
919 * structures yet: "requires any metadata-cache", but only "that named
920 * metadata-cache section"
921 */
TEST_F(RestMetadataCacheApiTest,DISABLED_metadata_cache_api_no_mdc_section)922 TEST_F(RestMetadataCacheApiTest, DISABLED_metadata_cache_api_no_mdc_section) {
923 const std::string userfile = create_password_file();
924 auto config_sections = get_restapi_config("rest_metadata_cache", userfile,
925 /*request_authentication=*/true);
926
927 const std::string conf_file{create_config_file(
928 conf_dir_.name(), mysql_harness::join(config_sections, "\n"))};
929 auto &router = launch_router({"-c", conf_file});
930
931 const auto wait_for_process_exit_timeout{10000ms};
932 check_exit_code(router, EXIT_FAILURE, wait_for_process_exit_timeout);
933
934 const std::string router_output = router.get_full_output();
935 EXPECT_THAT(router_output,
936 ::testing::HasSubstr("Plugin 'rest_metadata_cache' needs plugin "
937 "'metadata_cache' which is missing in the "
938 "configuration"))
939 << router_output;
940 }
941
942 /**
943 * @test Add [rest_metadata_cache] twice to the configuration file. Start
944 * router. Expect router to fail providing an error about the duplicate section.
945 *
946 */
TEST_F(RestMetadataCacheApiTest,rest_metadata_cache_section_twice)947 TEST_F(RestMetadataCacheApiTest, rest_metadata_cache_section_twice) {
948 const std::string userfile = create_password_file();
949 auto config_sections = get_restapi_config("rest_metadata_cache", userfile,
950 /*request_authentication=*/true);
951
952 // force [rest_metadata_cache] twice in the config
953 config_sections.push_back(
954 ConfigBuilder::build_section("rest_metadata_cache", {}));
955
956 const std::string conf_file{create_config_file(
957 conf_dir_.name(), mysql_harness::join(config_sections, "\n"))};
958 auto &router = launch_router({"-c", conf_file}, EXIT_FAILURE);
959
960 const auto wait_for_process_exit_timeout{10000ms};
961 check_exit_code(router, EXIT_FAILURE, wait_for_process_exit_timeout);
962
963 const std::string router_output = router.get_full_output();
964 EXPECT_THAT(
965 router_output,
966 ::testing::HasSubstr(
967 "Configuration error: Section 'rest_metadata_cache' already exists"))
968 << router_output;
969 }
970
971 /**
972 * @test Enable [rest_metadata_cache] using a section key such as
973 * [rest_metadata_cache:A]. Start router. Expect router to fail providing an
974 * error about the use of an unsupported section key.
975 *
976 */
TEST_F(RestMetadataCacheApiTest,rest_metadata_cache_section_has_key)977 TEST_F(RestMetadataCacheApiTest, rest_metadata_cache_section_has_key) {
978 const std::string userfile = create_password_file();
979 auto config_sections = get_restapi_config("rest_metadata_cache:A", userfile,
980 /*request_authentication=*/true);
981
982 const std::string conf_file{create_config_file(
983 conf_dir_.name(), mysql_harness::join(config_sections, "\n"))};
984 auto &router = launch_router({"-c", conf_file}, EXIT_FAILURE);
985
986 const auto wait_for_process_exit_timeout{10000ms};
987 check_exit_code(router, EXIT_FAILURE, wait_for_process_exit_timeout);
988
989 const std::string router_output = router.get_full_logfile();
990 EXPECT_THAT(
991 router_output,
992 ::testing::HasSubstr(
993 "plugin 'rest_metadata_cache' init failed: [rest_metadata_cache] "
994 "section does not expect a key, found 'A'"))
995 << router_output;
996 }
997
main(int argc,char * argv[])998 int main(int argc, char *argv[]) {
999 init_windows_sockets();
1000 ProcessManager::set_origin(Path(argv[0]).dirname());
1001 ::testing::InitGoogleTest(&argc, argv);
1002 return RUN_ALL_TESTS();
1003 }
1004