blob: d461f783b4101f9e53e631d99315674ce85ac128 [file] [log] [blame] [edit]
#include <memory>
#include <string>
#include <utility>
#include <variant>
#include "net/proto2/contrib/parse_proto/parse_text_proto.h"
#include "gmock.h"
#include "gunit.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/notification.h"
#include "absl/time/time.h"
#include "absl/types/span.h"
#include "redfish_query_engine/http/codes.h"
#include "redfish_query_engine/redfish/test_mockup.h"
#include "redfish_query_engine/redfish/transport/grpc.h"
#include "redfish_query_engine/redfish/transport/grpc_tls_options.h"
#include "redfish_query_engine/redfish/transport/interface.h"
#include "grpcpp/channel.h"
#include "grpcpp/client_context.h"
#include "grpcpp/create_channel.h"
#include "grpcpp/security/credentials.h"
#include "grpcpp/support/client_callback.h"
#include "grpcpp/support/status.h"
#include "nlohmann/json.hpp"
#include "nlohmann/json_fwd.hpp"
#include "proxy.h"
#include "proxy_config.pb.h"
#include "redfish_passthrough_plugin.h"
#include "redfish_plugin.h"
#include "request_response.h"
#include "voyager/deferrable_priority_queue.hpp"
#include "voyager/voyager_telemetry.grpc.pb.h"
#include "voyager/voyager_telemetry.pb.h"
#include "thread/thread.h"
#include "thread/thread_options.h"
namespace milotic {
namespace {
constexpr std::string_view kUdsFilename = "/tmp/test/grpc_proxy.socket";
using ::testing::Contains;
using ::testing::ElementsAre;
using ::testing::EqualsProto;
using ::testing::InvokeWithoutArgs;
using ::testing::Pair;
using ::testing::StartsWith;
using ::testing::Test;
using ::testing::proto::Partially;
using ::testing::status::StatusIs;
using ::google::protobuf::contrib::parse_proto::ParseTextProtoOrDie;
class MockedProxyServerTest : public Test {
protected:
MockedProxyServerTest() = default;
void SetUp() override {
ecclesia::StaticBufferBasedTlsOptions options;
int port;
options.SetToInsecure();
mockup_server_ = std::make_unique<ecclesia::TestingMockupServer>(
"barebones_session_auth/mockup.shar");
port = std::get<ecclesia::TestingMockupServer::ConfigNetwork>(
mockup_server_->GetConfig())
.port;
milotic_grpc_proxy::Plugin::RedfishPassthrough plugin_config;
plugin_config.set_request_timeout_msec(30000);
plugin_config.set_connect_timeout_msec(5000);
plugin_config.set_dns_timeout_sec(60);
plugin_config.set_max_recv_speed(-1);
plugin_config.set_sse_low_speed_limit_bytes_per_sec(100);
plugin_config.set_sse_low_speed_time_sec(5);
auto plugin = std::make_unique<RedfishPassthroughPlugin>(plugin_config);
std::unique_ptr<milotic::RedfishPlugin> plugins[] = {std::move(plugin)};
proxy_ = std::make_unique<Proxy>(
milotic::Host::CreateTestHosts(absl::StrCat("localhost:", port)),
absl::MakeSpan(plugins), &queue_, Proxy::Resources{},
ParseTextProtoOrDie(R"pb(
mappings: {
name: "match_all"
allow: { key: "all" }
resource_path: ""
with_subtree: true
}
)pb"));
ASSERT_OK(proxy_->AddService(
milotic_grpc_proxy::RedfishV1Options::default_instance()));
ASSERT_OK(proxy_->AddService(
milotic_grpc_proxy::VoyagerTelemetryOptions::default_instance()));
milotic_grpc_proxy::GrpcConfiguration config;
config.mutable_uds_endpoint()->set_path(kUdsFilename);
config.mutable_net_endpoint()->set_host("localhost");
config.mutable_net_endpoint()->set_port(8889);
ASSERT_OK(proxy_->ConfigGrpcAndStart(
config, [](const std::string& root) { return root == "/tmp"; }));
auto transport = ecclesia::CreateGrpcRedfishTransport(
absl::StrCat("localhost:", 8889), {}, options.GetChannelCredentials());
ASSERT_TRUE(transport.ok());
client_ = std::move(*transport);
transport = ecclesia::CreateGrpcRedfishTransport(
absl::StrCat("unix://", kUdsFilename), {},
options.GetChannelCredentials());
ASSERT_TRUE(transport.ok());
uds_client_ = std::move(*transport);
thread_.Start();
}
void TearDown() override {
queue_.Shutdown(true);
thread_.Join();
}
void RunQueue() {
while (queue_.ProcessQueue(1).ok()) {
}
}
std::unique_ptr<ecclesia::RedfishTransport> client_;
std::unique_ptr<ecclesia::RedfishTransport> uds_client_;
std::unique_ptr<ecclesia::TestingMockupServer> mockup_server_;
std::unique_ptr<Proxy> proxy_;
voyager::DeferrablePriorityQueue queue_{1};
MemberThread<MockedProxyServerTest> thread_{
thread::Options().set_joinable(true), "backgroundQueue", this,
&MockedProxyServerTest::RunQueue};
};
TEST_F(MockedProxyServerTest, TestPostPatchGet) {
absl::string_view data_post = R"json({
"ChassisType": "RackMount",
"Name": "MyChassis"
})json";
absl::StatusOr<ecclesia::RedfishTransport::Result> result_post =
client_->Post("/redfish/v1/Chassis", data_post);
ASSERT_TRUE(result_post.status().ok()) << result_post.status().message();
EXPECT_EQ(result_post->code,
ecclesia::HttpResponseCode::HTTP_CODE_NO_CONTENT);
// Test Patch request
absl::string_view data_patch = R"json({
"Name": "MyNewName"
})json";
absl::StatusOr<ecclesia::RedfishTransport::Result> result_patch =
client_->Patch("/redfish/v1/Chassis/Member1", data_patch);
ASSERT_TRUE(result_patch.status().ok()) << result_patch.status().message();
EXPECT_EQ(result_patch->code,
ecclesia::HttpResponseCode::HTTP_CODE_NO_CONTENT);
absl::StatusOr<ecclesia::RedfishTransport::Result> result_get =
client_->Get("/redfish/v1/Chassis/Member1");
LOG(WARNING) << result_get.status().message();
ASSERT_TRUE(result_get.status().ok()) << result_get.status().message();
ASSERT_TRUE(std::holds_alternative<nlohmann::json>(result_get->body));
std::string name = std::get<nlohmann::json>(result_get->body)["Name"];
EXPECT_EQ(result_get->code, ecclesia::HttpResponseCode::HTTP_CODE_REQUEST_OK);
EXPECT_EQ(name, "MyNewName");
EXPECT_EQ(result_get->code, ecclesia::HttpResponseCode::HTTP_CODE_REQUEST_OK);
}
TEST_F(MockedProxyServerTest, TestUdsPostPatchGet) {
absl::string_view data_post = R"json({
"ChassisType": "RackMount",
"Name": "MyChassis"
})json";
absl::StatusOr<ecclesia::RedfishTransport::Result> result_post =
uds_client_->Post("/redfish/v1/Chassis", data_post);
ASSERT_TRUE(result_post.status().ok()) << result_post.status().message();
EXPECT_EQ(result_post->code,
ecclesia::HttpResponseCode::HTTP_CODE_NO_CONTENT);
// Test Patch request
absl::string_view data_patch = R"json({
"Name": "MyNewName"
})json";
absl::StatusOr<ecclesia::RedfishTransport::Result> result_patch =
uds_client_->Patch("/redfish/v1/Chassis/Member1", data_patch);
ASSERT_TRUE(result_patch.status().ok()) << result_patch.status().message();
EXPECT_EQ(result_patch->code,
ecclesia::HttpResponseCode::HTTP_CODE_NO_CONTENT);
absl::StatusOr<ecclesia::RedfishTransport::Result> result_get =
uds_client_->Get("/redfish/v1/Chassis/Member1");
LOG(WARNING) << result_get.status().message();
ASSERT_TRUE(result_get.status().ok()) << result_get.status().message();
ASSERT_TRUE(std::holds_alternative<nlohmann::json>(result_get->body));
std::string name = std::get<nlohmann::json>(result_get->body)["Name"];
EXPECT_EQ(result_get->code, ecclesia::HttpResponseCode::HTTP_CODE_REQUEST_OK);
EXPECT_EQ(name, "MyNewName");
EXPECT_EQ(result_get->code, ecclesia::HttpResponseCode::HTTP_CODE_REQUEST_OK);
}
TEST_F(MockedProxyServerTest, TestVoyagerPostPatchGet) {
constexpr absl::string_view data_post = R"json({
"ChassisType": "RackMount",
"Name": "MyChassis"
})json";
std::shared_ptr<grpc::Channel> channel =
grpc::CreateChannel(absl::StrCat("unix://", kUdsFilename),
grpc::InsecureChannelCredentials());
std::unique_ptr<third_party_voyager::MachineTelemetry::Stub> stub(
third_party_voyager::MachineTelemetry::NewStub(channel));
third_party_voyager::SetRequest set_req;
set_req.set_req_id("test_post");
third_party_voyager::RequestFqp* req_fqp = set_req.add_req_fqp();
req_fqp->mutable_fqp()->set_specifier("/redfish/v1/Chassis");
set_req.set_json(data_post);
grpc::ClientContext post_context;
third_party_voyager::Update result;
ASSERT_OK(stub->Post(&post_context, set_req, &result));
EXPECT_EQ(result.req_id(), "test_post");
EXPECT_EQ(result.code(), ecclesia::HttpResponseCode::HTTP_CODE_NO_CONTENT);
EXPECT_EQ(result.data_points().size(), 0);
constexpr std::string_view data_patch = R"json({
"Name": "MyNewName"
})json";
// Test Patch request
set_req.Clear();
set_req.set_req_id("test_patch");
req_fqp = set_req.add_req_fqp();
req_fqp->mutable_fqp()->set_specifier("/redfish/v1/Chassis/{chassis_id}");
(*req_fqp->mutable_fqp()->mutable_identifiers())["chassis_id"] = "Member1";
set_req.set_json(data_patch);
grpc::ClientContext patch_context;
result.Clear();
ASSERT_OK(stub->Patch(&patch_context, set_req, &result));
EXPECT_EQ(result.req_id(), "test_patch");
EXPECT_EQ(result.code(), ecclesia::HttpResponseCode::HTTP_CODE_NO_CONTENT);
EXPECT_EQ(result.data_points().size(), 0);
EXPECT_THAT(result.http_headers(),
Contains(Pair("server", StartsWith("RedfishMockup"))));
// Test get request
third_party_voyager::Request req;
req.set_req_id("test_get");
req_fqp = req.add_req_fqp();
req_fqp->mutable_fqp()->set_specifier("/redfish/v1/Chassis/{chassis_id}");
(*req_fqp->mutable_fqp()->mutable_identifiers())["chassis_id"] = "Member1";
grpc::ClientContext get_context;
result.Clear();
ASSERT_OK(stub->Get(&get_context, req, &result));
EXPECT_EQ(result.req_id(), "test_get");
EXPECT_EQ(result.code(), ecclesia::HttpResponseCode::HTTP_CODE_REQUEST_OK);
EXPECT_THAT(result.http_headers(),
Contains(Pair("server", StartsWith("RedfishMockup"))));
ASSERT_EQ(result.data_points().size(), 1);
third_party_voyager::DataPoint expected;
expected.mutable_res_fqp()->CopyFrom(req_fqp->fqp());
EXPECT_THAT(result.data_points(0), Partially(EqualsProto(expected)));
auto json =
nlohmann::json::parse(result.data_points(0).json(), nullptr, false);
ASSERT_FALSE(json.is_discarded());
EXPECT_EQ(json["Name"], "MyNewName");
EXPECT_EQ(json["ChassisType"], "RackMount");
}
class MockReadReactor
: public grpc::ClientReadReactor<third_party_voyager::Update> {
public:
MOCK_METHOD(void, OnReadInitialMetadataDone, (bool), (override));
MOCK_METHOD(void, OnReadDone, (bool), (override));
MOCK_METHOD(void, OnDone, (const grpc::Status&), (override));
};
TEST_F(MockedProxyServerTest, SubscribeToRedfishEventsWorks) {
std::shared_ptr<grpc::Channel> channel =
grpc::CreateChannel(absl::StrCat("unix://", kUdsFilename),
grpc::InsecureChannelCredentials());
std::unique_ptr<third_party_voyager::MachineTelemetry::Stub> stub(
third_party_voyager::MachineTelemetry::NewStub(channel));
grpc::ClientContext subscribe_context;
third_party_voyager::Request req;
req.set_req_id("test_subscribe");
req.add_req_fqp()->mutable_fqp()->set_specifier(
"/redfish/v1/EventService/Subscriptions/SSE");
testing::StrictMock<MockReadReactor> reactor;
absl::Notification initial_metadata_done;
EXPECT_CALL(reactor, OnReadInitialMetadataDone(true))
.WillOnce(InvokeWithoutArgs(
[&initial_metadata_done] { initial_metadata_done.Notify(); }));
stub->async()->Subscribe(&subscribe_context, &req, &reactor);
third_party_voyager::Update response;
reactor.StartRead(&response);
reactor.StartCall();
// Make sure the request is being processed before we send the event
ASSERT_TRUE(
initial_metadata_done.WaitForNotificationWithTimeout(absl::Seconds(10)));
absl::Notification read_done;
EXPECT_CALL(reactor, OnReadDone(true))
.WillOnce(InvokeWithoutArgs([&read_done] { read_done.Notify(); }));
// Trigger an event
nlohmann::json event = {{"MessageId", "Base.13.0.Success"},
{"EventId", "1234"}};
third_party_voyager::SetRequest insert_event_req;
insert_event_req.set_req_id("test_insert_event");
insert_event_req.add_req_fqp()->mutable_fqp()->set_specifier(
"/redfish/v1/EventService/Actions/EventService.SubmitTestEvent");
insert_event_req.set_json(event.dump());
third_party_voyager::Update insert_event_response;
grpc::ClientContext post_context;
ASSERT_OK(
stub->Post(&post_context, insert_event_req, &insert_event_response));
// Wait for read to complete
ASSERT_TRUE(read_done.WaitForNotificationWithTimeout(absl::Seconds(10)));
EXPECT_EQ(response.req_id(), "test_subscribe");
nlohmann::json received = nlohmann::json::parse(
response.data_points()[0].key_value().fields().at("data").string_val(),
nullptr, false);
ASSERT_FALSE(received.is_discarded());
EXPECT_THAT(received["Events"], ElementsAre(event));
absl::Notification subscribe_done;
EXPECT_CALL(reactor, OnDone(StatusIs(absl::StatusCode::kCancelled)))
.WillOnce(
InvokeWithoutArgs([&subscribe_done] { subscribe_done.Notify(); }));
subscribe_context.TryCancel();
EXPECT_TRUE(subscribe_done.WaitForNotificationWithTimeout(absl::Seconds(10)));
}
} // namespace
} // namespace milotic