blob: af59bc8fce3904514d46988fc3c466a8f2ea556b [file] [log] [blame]
#include "ssh_actions_plugin.h"
#include <memory>
#include <string>
#include <utility>
#include "gmock.h"
#include "gunit.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
#include "absl/types/span.h"
#include "redfish_query_engine/protobuf/parse.h"
#include "nlohmann/json_fwd.hpp"
#include "mock_ssh_client.h"
#include "proxy.h"
#include "proxy_config.pb.h"
#include "redfish_plugin.h"
#include "request_response.h"
#include "ssh_client.h"
#include "voyager/deferrable_priority_queue.hpp"
#include "voyager/priority_queue.hpp"
#include "thread/thread.h"
#include "thread/thread_options.h"
namespace {
using ::testing::_;
using ::testing::Contains;
using ::testing::Invoke;
using ::testing::Pair;
using ::testing::StrictMock;
using ::testing::status::StatusIs;
using ::milotic::ExecutionCallbacks;
using ::milotic::Proxy;
using ::milotic::RedfishPlugin;
using ::milotic::SshActionsPlugin;
using ::voyager::DeferrablePriorityQueue;
using ProxyRequest = ::milotic::ProxyRequest;
using ProxyResponse = ::milotic::ProxyResponse;
using SshActionsConfig = milotic_grpc_proxy::Plugin::SshActions;
using ::milotic::MockExecutionContext;
using ::milotic::MockSshClient;
struct TestEnv {
explicit TestEnv(
std::unique_ptr<RedfishPlugin> plugin =
std::make_unique<SshActionsPlugin>(
ecclesia::ParseTextAsProtoOrDie<SshActionsConfig>(R"pb(
actions: {
action_path: "/google/v1/Test/Actions/Action.1"
ssh_command: "ssh command a"
timeout_sec: 60
}
actions: {
action_path: "/google/v1/Test/Actions/Action.2"
ssh_command: "ssh command b"
timeout_sec: 30
}
actions: {
action_path: "/google/v1/Test/Actions/Action.p1"
ssh_command: "ssh command $0"
timeout_sec: 30
args_count : 1
}
actions: {
action_path: "/google/v1/Test/Actions/Action.p2"
ssh_command: "ssh command 1:$1 0:$0"
timeout_sec: 30
args_count : 2
}
)pb")))
: queue(1),
proxy("http://test_endpoint:1234", absl::MakeSpan(&plugin, 1), &queue,
{.ssh_client = std::make_unique<MockSshClient>()}),
queue_thread(thread::Options().set_joinable(true), "TestEnv", this,
&TestEnv::ProcessQueue) {
queue_thread.Start();
}
~TestEnv() {
queue.Shutdown(false);
queue_thread.Join();
}
MockSshClient& SshClient() const {
return static_cast<MockSshClient&>(*proxy.GetResources().ssh_client);
}
void ProcessQueue() {
while (queue.ProcessQueue(1).ok()) {
}
}
DeferrablePriorityQueue queue;
Proxy proxy;
MemberThread<TestEnv> queue_thread;
};
TEST(SshActionsPluginTest, PluginHandlesRequests) {
TestEnv env;
EXPECT_CALL(env.SshClient(), ExecuteCommand("ssh command a", _, _))
.WillOnce(Invoke([](absl::string_view, ExecutionCallbacks* callbacks,
const milotic::SshClient::CommandOptions& options) {
auto context = std::make_unique<StrictMock<MockExecutionContext>>();
EXPECT_OK(callbacks->OnStdout(context.get(), "data a1"));
EXPECT_OK(callbacks->OnStderr(context.get(), "error a1"));
EXPECT_OK(callbacks->OnStderr(context.get(), "error a2"));
EXPECT_OK(callbacks->OnStdout(context.get(), "data a2"));
callbacks->OnDone(context.get(), 1);
return context;
}));
EXPECT_CALL(env.SshClient(), ExecuteCommand("ssh command b", _, _))
.WillOnce(Invoke([](absl::string_view, ExecutionCallbacks* callbacks,
const milotic::SshClient::CommandOptions& options) {
auto context = std::make_unique<StrictMock<MockExecutionContext>>();
EXPECT_OK(callbacks->OnStdout(context.get(), "data b1"));
EXPECT_OK(callbacks->OnStderr(context.get(), "error b1"));
EXPECT_OK(callbacks->OnStderr(context.get(), "error b2"));
EXPECT_OK(callbacks->OnStdout(context.get(), "data b2"));
callbacks->OnDone(context.get(), 2);
return context;
}));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<Proxy::RequestJob> job1,
env.proxy.DispatchRequestToQueue(
RedfishPlugin::RequestVerb::kPost,
env.proxy.CreateRequest("/google/v1/Test/Actions/Action.1")));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<Proxy::RequestJob> job2,
env.proxy.DispatchRequestToQueue(
RedfishPlugin::RequestVerb::kPost,
env.proxy.CreateRequest("/google/v1/Test/Actions/Action.2")));
ASSERT_EQ(job1->Wait(), voyager::Job::JobState::kDone);
ASSERT_OK_AND_ASSIGN(ProxyResponse response1, job1->response());
ASSERT_EQ(job2->Wait(), voyager::Job::JobState::kDone);
ASSERT_OK_AND_ASSIGN(ProxyResponse response2, job2->response());
EXPECT_EQ(response1.code, 200);
const nlohmann::json expected_a = {{"StdOut", "data a1data a2"},
{"StdErr", "error a1error a2"},
{"Result", 1}};
EXPECT_EQ(response1.GetBodyJson(), expected_a);
EXPECT_THAT(response1.headers,
Contains(Pair("Content-Type", "application/json")));
EXPECT_THAT(response1.headers, Contains(Pair("OData-Version", "4.0")));
EXPECT_EQ(response2.code, 200);
const nlohmann::json expected_b = {{"StdOut", "data b1data b2"},
{"StdErr", "error b1error b2"},
{"Result", 2}};
EXPECT_EQ(response2.GetBodyJson(), expected_b);
}
TEST(SshActionsPluginTest, PluginForwardsOtherUrls) {
TestEnv env;
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<Proxy::RequestJob> job,
env.proxy.DispatchRequestToQueue(
RedfishPlugin::RequestVerb::kPost,
env.proxy.CreateRequest("/google/v1/Test/Actions/Not.Supported")));
ASSERT_EQ(job->Wait(), voyager::Job::JobState::kDone);
EXPECT_THAT(job->response(), StatusIs(absl::StatusCode::kUnimplemented));
}
TEST(SshActionsPluginTest, PluginHandlesRequestsWithArgs) {
TestEnv env;
EXPECT_CALL(env.SshClient(), ExecuteCommand("ssh command a", _, _))
.WillOnce(Invoke([](absl::string_view, ExecutionCallbacks* callbacks,
const milotic::SshClient::CommandOptions& options) {
auto context = std::make_unique<StrictMock<MockExecutionContext>>();
EXPECT_OK(callbacks->OnStdout(context.get(), "data a1"));
EXPECT_OK(callbacks->OnStderr(context.get(), "error a1"));
EXPECT_OK(callbacks->OnStderr(context.get(), "error a2"));
EXPECT_OK(callbacks->OnStdout(context.get(), "data a2"));
callbacks->OnDone(context.get(), 1);
return context;
}));
EXPECT_CALL(env.SshClient(), ExecuteCommand("ssh command b", _, _))
.WillOnce(Invoke([](absl::string_view, ExecutionCallbacks* callbacks,
const milotic::SshClient::CommandOptions& options) {
auto context = std::make_unique<StrictMock<MockExecutionContext>>();
EXPECT_OK(callbacks->OnStdout(context.get(), "data b1"));
EXPECT_OK(callbacks->OnStderr(context.get(), "error b1"));
EXPECT_OK(callbacks->OnStderr(context.get(), "error b2"));
EXPECT_OK(callbacks->OnStdout(context.get(), "data b2"));
callbacks->OnDone(context.get(), 2);
return context;
}));
auto request_a = env.proxy.CreateRequest("/google/v1/Test/Actions/Action.p1");
request_a->body = "{ \"args\" : \"a\" }";
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<Proxy::RequestJob> job1,
env.proxy.DispatchRequestToQueue(
RedfishPlugin::RequestVerb::kPost,
std::move(request_a)));
auto request_b = env.proxy.CreateRequest("/google/v1/Test/Actions/Action.p1");
request_b->body = "{ \"args\" : [ \"b\" ] }";
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<Proxy::RequestJob> job2,
env.proxy.DispatchRequestToQueue(
RedfishPlugin::RequestVerb::kPost,
std::move(request_b)));
ASSERT_EQ(job1->Wait(), voyager::Job::JobState::kDone);
ASSERT_OK_AND_ASSIGN(ProxyResponse response1, job1->response());
ASSERT_EQ(job2->Wait(), voyager::Job::JobState::kDone);
ASSERT_OK_AND_ASSIGN(ProxyResponse response2, job2->response());
EXPECT_EQ(response1.code, 200);
const nlohmann::json expected_a = {{"StdOut", "data a1data a2"},
{"StdErr", "error a1error a2"},
{"Result", 1}};
EXPECT_EQ(response1.GetBodyJson(), expected_a);
EXPECT_THAT(response1.headers,
Contains(Pair("Content-Type", "application/json")));
EXPECT_THAT(response1.headers, Contains(Pair("OData-Version", "4.0")));
EXPECT_EQ(response2.code, 200);
const nlohmann::json expected_b = {{"StdOut", "data b1data b2"},
{"StdErr", "error b1error b2"},
{"Result", 2}};
EXPECT_EQ(response2.GetBodyJson(), expected_b);
}
TEST(SshActionsPluginTest, PluginHandlesRequestsWithTwoArgs) {
TestEnv env;
EXPECT_CALL(env.SshClient(), ExecuteCommand("ssh command 1:b 0:a", _, _))
.WillOnce(Invoke([](absl::string_view, ExecutionCallbacks* callbacks,
const milotic::SshClient::CommandOptions& options) {
auto context = std::make_unique<StrictMock<MockExecutionContext>>();
callbacks->OnDone(context.get(), 1);
return context;
}));
auto request_a = env.proxy.CreateRequest("/google/v1/Test/Actions/Action.p2");
request_a->body = "{ \"args\" : [ \"a\", \"b\" ] }";
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<Proxy::RequestJob> job1,
env.proxy.DispatchRequestToQueue(
RedfishPlugin::RequestVerb::kPost,
std::move(request_a)));
ASSERT_EQ(job1->Wait(), voyager::Job::JobState::kDone);
ASSERT_OK_AND_ASSIGN(ProxyResponse response1, job1->response());
EXPECT_EQ(response1.code, 200);
EXPECT_THAT(response1.headers,
Contains(Pair("Content-Type", "application/json")));
EXPECT_THAT(response1.headers, Contains(Pair("OData-Version", "4.0")));
}
TEST(SshActionsPluginTest, PluginHandlesRequestsWithArgsInvalid) {
TestEnv env;
auto request_a = env.proxy.CreateRequest("/google/v1/Test/Actions/Action.p1");
request_a->body = "{ \"args\" : 13 }";
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<Proxy::RequestJob> job1,
env.proxy.DispatchRequestToQueue(
RedfishPlugin::RequestVerb::kPost,
std::move(request_a)));
ASSERT_EQ(job1->Wait(), voyager::Job::JobState::kDone);
ASSERT_OK_AND_ASSIGN(ProxyResponse response1, job1->response());
EXPECT_EQ(response1.code, 500);
EXPECT_THAT(response1.headers,
Contains(Pair("Content-Type", "text/plain")));
}
TEST(SshActionsPluginTest, PluginHandlesRequestsWithArgsMissing) {
TestEnv env;
auto request_a = env.proxy.CreateRequest("/google/v1/Test/Actions/Action.p1");
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<Proxy::RequestJob> job1,
env.proxy.DispatchRequestToQueue(
RedfishPlugin::RequestVerb::kPost,
std::move(request_a)));
ASSERT_EQ(job1->Wait(), voyager::Job::JobState::kDone);
ASSERT_OK_AND_ASSIGN(ProxyResponse response1, job1->response());
EXPECT_EQ(response1.code, 500);
EXPECT_THAT(response1.headers,
Contains(Pair("Content-Type", "text/plain")));
}
} // namespace