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