| #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 |
| |