| #include <fstream> | |
| #include <functional> | |
| #include <memory> | |
| #include <string> | |
| #include <string_view> | |
| #include <system_error> | |
| #include <utility> | |
| #include <gmock/gmock.h> // IWYU pragma: keep | |
| #include <gtest/gtest.h> // IWYU pragma: keep | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "boost/asio/io_context.hpp" // NOLINT | |
| #include "boost/beast/http/status.hpp" // NOLINT | |
| #include "boost/beast/http/verb.hpp" // NOLINT | |
| #include "redfish_devpath_injector.hpp" | |
| #include "app.hpp" | |
| #include "app_singleton.hpp" | |
| #include "http_request.hpp" | |
| #include "http_response.hpp" | |
| #include "routing.hpp" | |
| #include "async_resp.hpp" | |
| #include "macros.hpp" | |
| #include "query.hpp" | |
| #include <nlohmann/json.hpp> | |
| // IWYU pragma: no_include <gtest/gtest-message.h> | |
| // IWYU pragma: no_include <gtest/gtest-test-part.h> | |
| // IWYU pragma: no_include "gtest/gtest_pred_impl.h" | |
| // IWYU pragma: no_include <gmock/gmock-matchers.h> | |
| // IWYU pragma: no_include <gmock/gmock-more-matchers.h> | |
| // IWYU pragma: no_include <gtest/gtest-matchers.h> | |
| namespace crow { | |
| namespace { | |
| std::string WriteTmpFile(std::string_view name, std::string_view content) { | |
| std::string path = absl::StrCat(testing::TempDir(), "/", name); | |
| std::ofstream out(path); | |
| out << content; | |
| out.close(); | |
| return path; | |
| } | |
| using ::testing::Eq; | |
| using ::testing::IsEmpty; | |
| using ::testing::Pointee; | |
| using ::testing::UnorderedElementsAre; | |
| TEST(GetRoutes, TestEmptyRoutes) { | |
| App app(/*allowSessionEmpty=*/true); | |
| app.validate(); | |
| EXPECT_THAT(app.getRoutes(), IsEmpty()); | |
| } | |
| // Tests that static urls are correctly passed | |
| TEST(GetRoutes, TestOneRoute) { | |
| App app(/*allowSessionEmpty=*/true); | |
| BMCWEB_ROUTE(app, "/")([]() { return boost::beast::http::status::ok; }); | |
| // TODO: "/" doesn't get reported in |getRoutes| today. Uncomment this once | |
| // it is fixed | |
| // EXPECT_THAT(app.getRoutes(), | |
| // testing::ElementsAre(Pointee(Eq("/")))); | |
| } | |
| // Tests that static urls are correctly passed | |
| TEST(GetRoutes, TestlotsOfRoutes) { | |
| App app(/*allowSessionEmpty=*/true); | |
| BMCWEB_ROUTE(app, "/")([]() { return boost::beast::http::status::ok; }); | |
| BMCWEB_ROUTE(app, "/foo")([]() { return boost::beast::http::status::ok; }); | |
| BMCWEB_ROUTE(app, "/bar")([]() { return boost::beast::http::status::ok; }); | |
| BMCWEB_ROUTE(app, "/baz")([]() { return boost::beast::http::status::ok; }); | |
| BMCWEB_ROUTE(app, "/boo")([]() { return boost::beast::http::status::ok; }); | |
| BMCWEB_ROUTE(app, "/moo")([]() { return boost::beast::http::status::ok; }); | |
| app.validate(); | |
| // TODO: "/" doesn't get reported in |getRoutes| today. Uncomment this once | |
| // it is fixed | |
| EXPECT_THAT(app.getRoutes(), | |
| UnorderedElementsAre( | |
| // Pointee(Eq("/")), | |
| Pointee(Eq("/foo")), Pointee(Eq("/bar")), Pointee(Eq("/baz")), | |
| Pointee(Eq("/boo")), Pointee(Eq("/moo")))); | |
| } | |
| } // namespace | |
| TEST(AppDevpathInjectorTest, InjectExpandRequests) { | |
| std::string config_content = R"pb( | |
| redfish_to_uhm_mapping { | |
| key { redfish_url_pattern: "/redfish/v1/Chassis/ChassisA" } | |
| uhm_monitored_component_key { barepath: "/phys/ChassisA" } | |
| redfish_expose_config { | |
| expose_oem_google_devpath: true | |
| set_plc_as_placeholder: false | |
| } | |
| } | |
| redfish_to_uhm_mapping { | |
| key { | |
| redfish_url_pattern: | |
| "/redfish/v1/Chassis/ChassisA/ThermalSubsystem/Fans/fan0" | |
| } | |
| uhm_monitored_component_key { barepath: "/phys/ChassisA/fan0" } | |
| redfish_expose_config { | |
| expose_oem_google_devpath: true | |
| set_plc_as_placeholder: false | |
| } | |
| } | |
| )pb"; | |
| std::string config_path = | |
| WriteTmpFile("app_expand_config.textproto", config_content); | |
| // Instantiate the injector at the main/test function level | |
| absl::StatusOr<std::unique_ptr< | |
| devpath_plugin::RedfishDevpathInjector>> injector = | |
| devpath_plugin::RedfishDevpathInjector::Create(config_path); | |
| ASSERT_TRUE(injector.ok()); | |
| // Initialize App and pass the raw pointer of the injector | |
| auto io = | |
| std::make_shared<boost::asio::io_context>(); | |
| App app(true, io, nullptr, (*injector).get()); | |
| ASSERT_EQ(app.GetDevpathInjector(), (*injector).get()); | |
| crow::globalBmcWebApp = &app; | |
| // Track if sub-route plugins ran | |
| bool chassis_plugin_called = false; | |
| bool fan_plugin_called = false; | |
| // Register routes on the app. | |
| BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>") | |
| .methods(boost::beast::http::verb::get)( | |
| [&app](const Request& req, | |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, | |
| const std::string& /*chassisId*/) { | |
| if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { | |
| return; | |
| } | |
| asyncResp->res.jsonValue = { | |
| {"@odata.id", "/redfish/v1/Chassis/ChassisA"}, | |
| {"Id", "ChassisA"}, | |
| {"Name", "ChassisA"}, | |
| {"ThermalSubsystem", | |
| {{"@odata.id", | |
| "/redfish/v1/Chassis/ChassisA/ThermalSubsystem"}}} | |
| }; | |
| }); | |
| // Use REDFISH_HANDLER_APPEND to inject a plugin on the chassis route | |
| REDFISH_HANDLER_APPEND( | |
| "/redfish/v1/Chassis/<str>", boost::beast::http::verb::get, | |
| [&chassis_plugin_called]( | |
| const Request& /*req*/, | |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, | |
| const std::string& /*chassisId*/) { | |
| chassis_plugin_called = true; | |
| asyncResp->res.jsonValue["ChassisPluginIndicator"] = | |
| "chassis_plugin_worked"; | |
| }); | |
| BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/ThermalSubsystem") | |
| .methods(boost::beast::http::verb::get)( | |
| [&app](const Request& req, | |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, | |
| const std::string& /*chassisId*/) { | |
| if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { | |
| return; | |
| } | |
| asyncResp->res.jsonValue = { | |
| {"@odata.id", "/redfish/v1/Chassis/ChassisA/ThermalSubsystem"}, | |
| {"Id", "ThermalSubsystem"}, | |
| {"Name", "Thermal Subsystem"}, | |
| {"Fans", | |
| {{"@odata.id", | |
| "/redfish/v1/Chassis/ChassisA/ThermalSubsystem/Fans"}}} | |
| }; | |
| }); | |
| BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/ThermalSubsystem/Fans") | |
| .methods(boost::beast::http::verb::get)( | |
| [&app](const Request& req, | |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, | |
| const std::string& /*chassisId*/) { | |
| if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { | |
| return; | |
| } | |
| asyncResp->res.jsonValue = { | |
| {"@odata.id", | |
| "/redfish/v1/Chassis/ChassisA/ThermalSubsystem/Fans"}, | |
| {"Members", | |
| {{{"@odata.id", | |
| "/redfish/v1/Chassis/ChassisA/ThermalSubsystem/Fans/" | |
| "fan0"}}}}, | |
| {"Members@odata.count", 1}, | |
| {"Name", "Fan Collection"} | |
| }; | |
| }); | |
| BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/ThermalSubsystem/Fans/<str>") | |
| .methods(boost::beast::http::verb::get)( | |
| [&app](const Request& req, | |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, | |
| const std::string& /*chassisId*/, | |
| const std::string& /*fanId*/) { | |
| if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { | |
| return; | |
| } | |
| asyncResp->res.jsonValue = { | |
| {"@odata.id", | |
| "/redfish/v1/Chassis/ChassisA/ThermalSubsystem/Fans/fan0"}, | |
| {"Id", "fan0"}, | |
| {"Name", "fan0"} | |
| }; | |
| }); | |
| // Use REDFISH_HANDLER_APPEND to inject a plugin on the fan endpoint | |
| REDFISH_HANDLER_APPEND( | |
| "/redfish/v1/Chassis/<str>/ThermalSubsystem/Fans/<str>", | |
| boost::beast::http::verb::get, | |
| [&fan_plugin_called]( | |
| const Request& /*req*/, | |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, | |
| const std::string& /*chassisId*/, const std::string& /*fanId*/) { | |
| fan_plugin_called = true; | |
| asyncResp->res.jsonValue["FanPluginIndicator"] = | |
| "fan_plugin_worked"; | |
| }); | |
| app.validate(); | |
| // Create expand request | |
| std::error_code ec; | |
| Request req( | |
| {boost::beast::http::verb::get, | |
| "/redfish/v1/Chassis/ChassisA?$expand=*($levels=3)", 11}, | |
| ec); | |
| ASSERT_FALSE(ec); | |
| // Capture final response | |
| nlohmann::json final_response_json; | |
| boost::beast::http::status final_status = | |
| boost::beast::http::status::unknown; | |
| bool completed = false; | |
| { | |
| auto asyncResp = | |
| std::make_shared<bmcweb::AsyncResp>(); | |
| asyncResp->res.setCompleteRequestHandler( | |
| [&final_response_json, &final_status, &completed](Response& res) { | |
| final_response_json = res.jsonValue; | |
| final_status = res.result(); | |
| completed = true; | |
| }); | |
| app.handle(req, asyncResp); | |
| } // asyncResp count drops to 0, destructor is invoked, executing all | |
| // query param and injector logic. | |
| ASSERT_TRUE(completed); | |
| EXPECT_TRUE(chassis_plugin_called); | |
| EXPECT_TRUE(fan_plugin_called); | |
| EXPECT_EQ(final_status, boost::beast::http::status::ok); | |
| nlohmann::json expected = nlohmann::json::parse(R"json( | |
| { | |
| "@odata.id": "/redfish/v1/Chassis/ChassisA", | |
| "ChassisPluginIndicator": "chassis_plugin_worked", | |
| "Id": "ChassisA", | |
| "Location": { | |
| "Oem": { | |
| "Google": { | |
| "Devpath": "/phys/ChassisA" | |
| } | |
| }, | |
| "PartLocation": { | |
| "ServiceLabel": "ChassisA" | |
| } | |
| }, | |
| "Name": "ChassisA", | |
| "ThermalSubsystem": { | |
| "@odata.id": "/redfish/v1/Chassis/ChassisA/ThermalSubsystem", | |
| "Fans": { | |
| "@odata.id": "/redfish/v1/Chassis/ChassisA/ThermalSubsystem/Fans", | |
| "Members": [ | |
| { | |
| "@odata.id": "/redfish/v1/Chassis/ChassisA/ThermalSubsystem/Fans/fan0", | |
| "FanPluginIndicator": "fan_plugin_worked", | |
| "Id": "fan0", | |
| "Location": { | |
| "Oem": { | |
| "Google": { | |
| "Devpath": "/phys/ChassisA/fan0" | |
| } | |
| }, | |
| "PartLocation": { | |
| "ServiceLabel": "fan0" | |
| }, | |
| "PartLocationContext": "ChassisA" | |
| }, | |
| "Name": "fan0" | |
| } | |
| ], | |
| "Members@odata.count": 1, | |
| "Name": "Fan Collection" | |
| }, | |
| "Id": "ThermalSubsystem", | |
| "Name": "Thermal Subsystem" | |
| } | |
| } | |
| )json"); | |
| EXPECT_EQ(final_response_json, expected); | |
| } | |
| } // namespace crow | |