| #include "proxy.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "net/proto2/contrib/parse_proto/parse_text_proto.h" |
| #include "gmock.h" |
| #include "gunit.h" |
| #include "testing/fuzzing/fuzztest.h" |
| #include "absl/functional/bind_front.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/time/clock.h" |
| #include "absl/time/time.h" |
| #include "absl/types/span.h" |
| #include "redfish_query_engine/http/codes.h" |
| #include "fake_auth_context.h" |
| #include "mock_redfish_plugin.h" |
| #include "redfish_plugin.h" |
| #include "request_response.h" |
| #include "resource_authz.h" |
| #include "sse/sse_parser.h" |
| #include "voyager/deferrable_priority_queue.hpp" |
| #include "voyager/voyager_telemetry.pb.h" |
| |
| namespace { |
| |
| using ::testing::_; |
| using ::testing::Contains; |
| using ::testing::ElementsAre; |
| using ::testing::Field; |
| using ::testing::FieldsAre; |
| using ::testing::HasSubstr; |
| using ::testing::Invoke; |
| using ::testing::InvokeWithoutArgs; |
| using ::testing::MatcherCast; |
| using ::testing::MockFunction; |
| using ::testing::Pair; |
| using ::testing::Pointee; |
| using ::testing::Pointer; |
| using ::testing::Return; |
| using ::testing::SaveArg; |
| using ::testing::StrictMock; |
| using ::testing::status::IsOkAndHolds; |
| using ::testing::status::StatusIs; |
| |
| using ::google::protobuf::contrib::parse_proto::ParseTextProtoOrDie; |
| |
| using ProxyRequest = milotic::ProxyRequest; |
| using ProxyResponse = milotic::ProxyResponse; |
| using RequestVerb = milotic::RedfishPlugin::RequestVerb; |
| using RequestAction = milotic::RedfishPlugin::RequestAction; |
| using MockPlugin = milotic::MockRedfishPlugin; |
| using MockEventHandler = milotic::MockEventHandler; |
| |
| TEST(TestPlugins, no_plugins_ok) { |
| milotic::Proxy proxy("test_endpoint"); |
| auto request = std::make_unique<ProxyRequest>(); |
| request->uri = "test/uri"; |
| |
| absl::StatusOr<ProxyResponse> result = proxy.DispatchRequest( |
| milotic::RedfishPlugin::RequestVerb::kGet, std::move(request)); |
| EXPECT_THAT(result, StatusIs(absl::StatusCode::kUnimplemented)); |
| } |
| |
| TEST(TestPlugins, plugin_not_activated) { |
| auto default_plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*default_plugin, Initialize).WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(*default_plugin, |
| PreprocessRequest(RequestVerb::kGet, |
| Field(&ProxyRequest::uri, "test/uri"))) |
| .WillOnce(Return(RequestAction::kHandle)); |
| EXPECT_CALL(*default_plugin, |
| HandleRequest(RequestVerb::kGet, |
| Pointee(Field(&ProxyRequest::uri, "test/uri")))) |
| .WillOnce(Invoke([](RequestVerb, std::unique_ptr<ProxyRequest> req) { |
| req.reset(); // take ownership and delete |
| return ProxyResponse{{ |
| .code = ecclesia::HTTP_CODE_BAD_REQUEST, |
| .body = "test body.", |
| .headers = {{"test-header", "test-header-value"}}, |
| }}; |
| })); |
| |
| auto plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*plugin, Initialize).WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(*plugin, PreprocessRequest(RequestVerb::kGet, |
| Field(&ProxyRequest::uri, "test/uri"))) |
| .WillOnce(Return(RequestAction::kNext)); |
| |
| std::unique_ptr<milotic::RedfishPlugin> plugins[] = { |
| std::move(plugin), std::move(default_plugin)}; |
| milotic::Proxy proxy("test_endpoint", absl::MakeSpan(plugins)); |
| auto request = std::make_unique<ProxyRequest>(); |
| request->uri = "test/uri"; |
| |
| absl::StatusOr<ProxyResponse> result = |
| proxy.DispatchRequest(RequestVerb::kGet, std::move(request)); |
| EXPECT_THAT(result, |
| IsOkAndHolds(FieldsAre( |
| ecclesia::HTTP_CODE_BAD_REQUEST, "test body.", |
| ElementsAre(Pair("test-header", "test-header-value"))))); |
| } |
| |
| TEST(TestPlugins, PluginHandlesRequest) { |
| auto plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*plugin, Initialize).WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(*plugin, PreprocessRequest(RequestVerb::kGet, |
| Field(&ProxyRequest::uri, "test/uri"))) |
| .WillOnce(Return(RequestAction::kHandle)); |
| EXPECT_CALL(*plugin, |
| HandleRequest(RequestVerb::kGet, |
| Pointee(Field(&ProxyRequest::uri, "test/uri")))) |
| .WillOnce(Invoke([](RequestVerb, std::unique_ptr<ProxyRequest> req) { |
| return ProxyResponse{{ |
| .code = ecclesia::HTTP_CODE_REQUEST_OK, |
| .body = "plugin body.", |
| }}; |
| })); |
| |
| std::unique_ptr<milotic::RedfishPlugin> plugins[] = {std::move(plugin)}; |
| milotic::Proxy proxy("test_endpoint", absl::MakeSpan(plugins)); |
| |
| auto request = std::make_unique<ProxyRequest>(); |
| request->uri = "test/uri"; |
| |
| absl::StatusOr<ProxyResponse> result = |
| proxy.DispatchRequest(RequestVerb::kGet, std::move(request)); |
| EXPECT_THAT(result, IsOkAndHolds(FieldsAre(ecclesia::HTTP_CODE_REQUEST_OK, |
| "plugin body.", _))); |
| } |
| |
| TEST(TestPlugins, PluginFails) { |
| auto plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*plugin, Initialize).WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(*plugin, PreprocessRequest(RequestVerb::kGet, |
| Field(&ProxyRequest::uri, "test/uri"))) |
| .WillOnce(Return(RequestAction::kHandle)); |
| EXPECT_CALL(*plugin, |
| HandleRequest(RequestVerb::kGet, |
| Pointee(Field(&ProxyRequest::uri, "test/uri")))) |
| .WillOnce(Invoke([](RequestVerb, std::unique_ptr<ProxyRequest> req) { |
| return absl::InternalError("plugin error"); |
| })); |
| |
| std::unique_ptr<milotic::RedfishPlugin> plugins[] = {std::move(plugin)}; |
| milotic::Proxy proxy("test_endpoint", absl::MakeSpan(plugins)); |
| |
| auto request = std::make_unique<ProxyRequest>(); |
| request->uri = "test/uri"; |
| |
| absl::StatusOr<ProxyResponse> result = |
| proxy.DispatchRequest(RequestVerb::kGet, std::move(request)); |
| EXPECT_THAT(result, StatusIs(absl::StatusCode::kInternal)); |
| } |
| |
| TEST(PluginsDeathTest, HardFailIfPluginInitFails) { |
| auto default_plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*default_plugin, Initialize).Times(0); |
| auto plugin = std::make_unique<MockPlugin>(); |
| ON_CALL(*plugin, Initialize) |
| .WillByDefault(Return(absl::InternalError("test plugin init error"))); |
| |
| std::unique_ptr<milotic::RedfishPlugin> plugins[] = { |
| std::move(plugin), std::move(default_plugin)}; |
| EXPECT_DEATH( |
| { milotic::Proxy proxy("test_endpoint", absl::MakeSpan(plugins)); }, |
| HasSubstr("test plugin init error")); |
| } |
| |
| TEST(TestPlugins, PluginModifiesRequest) { |
| auto default_plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*default_plugin, Initialize).WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(*default_plugin, |
| PreprocessRequest(RequestVerb::kGet, |
| Field(&ProxyRequest::uri, "test/uri/modified"))) |
| .WillOnce(Return(RequestAction::kHandle)); |
| EXPECT_CALL( |
| *default_plugin, |
| HandleRequest(RequestVerb::kGet, |
| Pointee(Field(&ProxyRequest::uri, "test/uri/modified")))) |
| .WillOnce(Invoke([](RequestVerb, std::unique_ptr<ProxyRequest> req) { |
| req.reset(); // take ownership and delete |
| return ProxyResponse{{ |
| .code = ecclesia::HTTP_CODE_BAD_REQUEST, |
| .body = "test body.", |
| .headers = {{"test-header", "test-header-value"}}, |
| }}; |
| })); |
| |
| auto plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*plugin, Initialize).WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(*plugin, PreprocessRequest(RequestVerb::kGet, |
| Field(&ProxyRequest::uri, "test/uri"))) |
| .WillOnce(Invoke([](RequestVerb, ProxyRequest& request) { |
| request.uri = "test/uri/modified"; |
| return RequestAction::kNext; |
| })); |
| |
| std::unique_ptr<milotic::RedfishPlugin> plugins[] = { |
| std::move(plugin), std::move(default_plugin)}; |
| milotic::Proxy proxy("test_endpoint", absl::MakeSpan(plugins)); |
| |
| auto request = std::make_unique<ProxyRequest>(); |
| request->uri = "test/uri"; |
| |
| absl::StatusOr<ProxyResponse> result = |
| proxy.DispatchRequest(RequestVerb::kGet, std::move(request)); |
| EXPECT_THAT(result, |
| IsOkAndHolds(FieldsAre( |
| ecclesia::HTTP_CODE_BAD_REQUEST, "test body.", |
| ElementsAre(Pair("test-header", "test-header-value"))))); |
| } |
| |
| TEST(TestPlugins, PluginDropsRequest) { |
| auto plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*plugin, Initialize).WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(*plugin, PreprocessRequest(RequestVerb::kGet, |
| Field(&ProxyRequest::uri, "test/uri"))) |
| .WillOnce(Return(RequestAction::kDrop)); |
| |
| std::unique_ptr<milotic::RedfishPlugin> plugins[] = {std::move(plugin)}; |
| milotic::Proxy proxy("test_endpoint", absl::MakeSpan(plugins)); |
| |
| auto request = std::make_unique<ProxyRequest>(); |
| request->uri = "test/uri"; |
| |
| absl::StatusOr<ProxyResponse> result = |
| proxy.DispatchRequest(RequestVerb::kGet, std::move(request)); |
| EXPECT_THAT(result, StatusIs(absl::StatusCode::kUnavailable)); |
| } |
| |
| constexpr int kFakeEventsCount = 2; |
| template <typename Event = milotic::ServerSentEvent> |
| absl::Status FakeEvents(const ProxyResponse& response, |
| std::unique_ptr<ProxyRequest> request, |
| milotic::RedfishPlugin::EventHandler* event_handler) { |
| absl::Status status = event_handler->OnResponse(response); |
| if (!status.ok()) return status; |
| Event event; |
| for (int i = 0; i < kFakeEventsCount; ++i) { |
| event.id = absl::StrCat(i); |
| absl::Status event_status = event_handler->OnEvent(event); |
| if (!event_status.ok()) return event_status; |
| } |
| return absl::OkStatus(); |
| } |
| |
| TEST(TestPlugins, SubscribePluginOk) { |
| using Event = milotic::ServerSentEvent; |
| auto plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*plugin, Initialize).WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(*plugin, PreprocessRequest(RequestVerb::kSubscribe, |
| Field(&ProxyRequest::uri, "test/uri"))) |
| .WillOnce(Return(RequestAction::kHandle)); |
| EXPECT_CALL(*plugin, |
| Subscribe(Pointee(Field(&ProxyRequest::uri, "test/uri")), _)) |
| .WillOnce( |
| Invoke(absl::bind_front(FakeEvents<>, ProxyResponse{{.code = 200}}))); |
| |
| std::unique_ptr<milotic::RedfishPlugin> plugins[] = {std::move(plugin)}; |
| milotic::Proxy proxy("test_endpoint", absl::MakeSpan(plugins)); |
| |
| auto request = std::make_unique<ProxyRequest>(); |
| request->uri = "test/uri"; |
| |
| MockEventHandler handler; |
| ON_CALL(handler, IsCancelled()).WillByDefault(Return(false)); |
| ::testing::InSequence event_sequence; |
| EXPECT_CALL(handler, OnResponse(Field(&ProxyResponse::code, 200))) |
| .WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(handler, |
| OnEvent(MatcherCast<const Event&>(Field(&Event::id, "0")))) |
| .WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(handler, |
| OnEvent(MatcherCast<const Event&>(Field(&Event::id, "1")))) |
| .WillOnce(Return(absl::OkStatus())); |
| EXPECT_OK(proxy.Subscribe(std::move(request), &handler)); |
| } |
| |
| TEST(TestPlugins, SubscribePluginResponseError) { |
| auto plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*plugin, Initialize).WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(*plugin, PreprocessRequest(RequestVerb::kSubscribe, |
| Field(&ProxyRequest::uri, "test/uri"))) |
| .WillOnce(Return(RequestAction::kHandle)); |
| EXPECT_CALL(*plugin, |
| Subscribe(Pointee(Field(&ProxyRequest::uri, "test/uri")), _)) |
| .WillOnce( |
| Invoke(absl::bind_front(FakeEvents<>, ProxyResponse{{.code = 200}}))); |
| |
| std::unique_ptr<milotic::RedfishPlugin> plugins[] = {std::move(plugin)}; |
| milotic::Proxy proxy("test_endpoint", absl::MakeSpan(plugins)); |
| |
| auto request = std::make_unique<ProxyRequest>(); |
| request->uri = "test/uri"; |
| |
| StrictMock<MockEventHandler> handler; |
| ON_CALL(handler, IsCancelled()).WillByDefault(Return(false)); |
| EXPECT_CALL(handler, OnResponse(Field(&ProxyResponse::code, 200))) |
| .WillOnce(Return(absl::InternalError("Test error"))); |
| |
| EXPECT_THAT(proxy.Subscribe(std::move(request), &handler), |
| StatusIs(absl::StatusCode::kInternal, "Test error")); |
| } |
| |
| TEST(TestPlugins, SubscribePluginEventError) { |
| using Event = milotic::ServerSentEvent; |
| auto plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*plugin, Initialize).WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(*plugin, PreprocessRequest(RequestVerb::kSubscribe, |
| Field(&ProxyRequest::uri, "test/uri"))) |
| .WillOnce(Return(RequestAction::kHandle)); |
| EXPECT_CALL(*plugin, |
| Subscribe(Pointee(Field(&ProxyRequest::uri, "test/uri")), _)) |
| .WillOnce( |
| Invoke(absl::bind_front(FakeEvents<>, ProxyResponse{{.code = 200}}))); |
| |
| std::unique_ptr<milotic::RedfishPlugin> plugins[] = {std::move(plugin)}; |
| milotic::Proxy proxy("test_endpoint", absl::MakeSpan(plugins)); |
| |
| auto request = std::make_unique<ProxyRequest>(); |
| request->uri = "test/uri"; |
| |
| StrictMock<MockEventHandler> handler; |
| EXPECT_CALL(handler, IsCancelled()).WillRepeatedly(Return(false)); |
| ::testing::InSequence event_sequence; |
| EXPECT_CALL(handler, OnResponse(Field(&ProxyResponse::code, 200))) |
| .WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(handler, |
| OnEvent(MatcherCast<const Event&>(Field(&Event::id, "0")))) |
| .WillOnce(Return(absl::InternalError("Test error"))); |
| EXPECT_THAT(proxy.Subscribe(std::move(request), &handler), |
| StatusIs(absl::StatusCode::kInternal, "Test error")); |
| } |
| |
| TEST(TestPlugins, BidirectionalStreamPluginOk) { |
| using third_party_voyager::Update; |
| auto plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*plugin, Initialize).WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(*plugin, PreprocessRequest(RequestVerb::kBidirectionalStream, |
| Field(&ProxyRequest::uri, "test/uri"))) |
| .WillOnce(Return(RequestAction::kHandle)); |
| auto write_handler = std::make_unique<MockEventHandler>(); |
| auto* write_handler_ptr = write_handler.get(); |
| StrictMock<MockEventHandler> read_handler; |
| EXPECT_CALL(*plugin, BidirectionalStream( |
| Pointee(Field(&ProxyRequest::uri, "test/uri")), |
| Pointer(&read_handler))) |
| .WillOnce(Return(std::move(write_handler))); |
| |
| std::unique_ptr<milotic::RedfishPlugin> plugins[] = {std::move(plugin)}; |
| milotic::Proxy proxy("test_endpoint", absl::MakeSpan(plugins)); |
| |
| auto request = std::make_unique<ProxyRequest>(); |
| request->uri = "test/uri"; |
| |
| EXPECT_THAT(proxy.BidirectionalStream(std::move(request), &read_handler), |
| IsOkAndHolds(Pointer(write_handler_ptr))); |
| } |
| |
| TEST(TestCreateRequest, EndpointNameIsCopiedToHostHeader) { |
| milotic::Proxy proxy("hostname.test:1234"); |
| std::unique_ptr<ProxyRequest> request = proxy.CreateRequest("some/resource"); |
| EXPECT_THAT(request->headers, Contains(Pair("Host", "hostname.test:1234"))); |
| } |
| |
| TEST(TestCreateRequest, EndpointProtocolIsRemoved) { |
| milotic::Proxy proxy("https://hostname.test:1234"); |
| std::unique_ptr<ProxyRequest> request = proxy.CreateRequest("some/resource"); |
| EXPECT_THAT(request->headers, Contains(Pair("Host", "hostname.test:1234"))); |
| } |
| |
| TEST(TestDispatchRequest, HandlerRunsAfterRequestIsCompleted) { |
| auto plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*plugin, Initialize).WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(*plugin, PreprocessRequest) |
| .WillOnce(Return(RequestAction::kHandle)); |
| EXPECT_CALL(*plugin, HandleRequest(RequestVerb::kGet, _)) |
| .WillOnce(Return(ProxyResponse{{.code = 200, .body = "test body"}})); |
| |
| voyager::DeferrablePriorityQueue queue(1); |
| std::unique_ptr<milotic::RedfishPlugin> plugins[] = {std::move(plugin)}; |
| milotic::Proxy proxy("test_endpoint", plugins, &queue); |
| |
| StrictMock<MockFunction<milotic::Proxy::RequestJob::Handler>> handler; |
| |
| auto request_job = proxy.CreateRequestJob( |
| RequestVerb::kGet, proxy.CreateRequest("/test_resource")); |
| absl::StatusOr<ProxyResponse> called_with; |
| request_job->Handle(handler.AsStdFunction()); |
| ASSERT_OK(proxy.DispatchRequestToQueue(std::move(request_job))); |
| |
| EXPECT_CALL(handler, Call).WillOnce(SaveArg<0>(&called_with)); |
| ASSERT_OK(queue.ProcessQueue(1)); |
| |
| EXPECT_THAT(called_with, |
| IsOkAndHolds(AllOf(Field(&ProxyResponse::code, 200), |
| Field(&ProxyResponse::body, "test body")))); |
| } |
| |
| TEST(TestDispatchRequest, HandlerRunsIfRequestIsAlreadyCompleted) { |
| auto plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*plugin, Initialize).WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(*plugin, PreprocessRequest) |
| .WillOnce(Return(RequestAction::kHandle)); |
| EXPECT_CALL(*plugin, HandleRequest(RequestVerb::kGet, _)) |
| .WillOnce(Return(ProxyResponse{{.code = 200, .body = "test body"}})); |
| |
| voyager::DeferrablePriorityQueue queue(1); |
| std::unique_ptr<milotic::RedfishPlugin> plugins[] = {std::move(plugin)}; |
| milotic::Proxy proxy("test_endpoint", plugins, &queue); |
| |
| StrictMock<MockFunction<milotic::Proxy::RequestJob::Handler>> handler; |
| |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<milotic::Proxy::RequestJob> job, |
| proxy.DispatchRequestToQueue(RequestVerb::kGet, |
| proxy.CreateRequest("/test_resource"))); |
| ASSERT_OK(queue.ProcessQueue(1)); |
| EXPECT_CALL( |
| handler, |
| Call(IsOkAndHolds(AllOf(Field(&ProxyResponse::code, 200), |
| Field(&ProxyResponse::body, "test body"))))); |
| job->Handle(handler.AsStdFunction()); |
| } |
| |
| TEST(TestDispatchRequest, HandlerRunsWhenScheduled) { |
| auto plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*plugin, Initialize).WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(*plugin, PreprocessRequest) |
| .WillRepeatedly(Return(RequestAction::kHandle)); |
| EXPECT_CALL(*plugin, HandleRequest(RequestVerb::kGet, _)) |
| .WillOnce(Return(ProxyResponse{{.code = 200, .body = "test body"}})); |
| |
| voyager::DeferrablePriorityQueue queue(1); |
| std::unique_ptr<milotic::RedfishPlugin> plugins[] = {std::move(plugin)}; |
| milotic::Proxy proxy("test_endpoint", plugins, &queue); |
| |
| StrictMock<MockFunction<milotic::Proxy::RequestJob::Handler>> handler; |
| |
| auto request_job = proxy.CreateRequestJob( |
| RequestVerb::kGet, proxy.CreateRequest("/test_resource")); |
| absl::StatusOr<ProxyResponse> called_with; |
| request_job->Handle(handler.AsStdFunction()); |
| absl::Time run_after = absl::Now() + absl::Seconds(1); |
| ASSERT_OK(proxy.DispatchRequestToQueue(std::move(request_job), |
| milotic::Proxy::RequestPriority::kLow, |
| absl::Now() + absl::Seconds(1))); |
| |
| absl::Time ran_at; |
| EXPECT_CALL( |
| handler, |
| Call(IsOkAndHolds(AllOf(Field(&ProxyResponse::code, 200), |
| Field(&ProxyResponse::body, "test body"))))) |
| .WillOnce(InvokeWithoutArgs([&ran_at] { ran_at = absl::Now(); })); |
| ASSERT_OK(queue.ProcessQueue(1)); |
| |
| EXPECT_GE(ran_at, run_after); |
| } |
| |
| TEST(TestDispatchRequest, HandlerRunsAfterRequestFails) { |
| auto plugin = std::make_unique<StrictMock<MockPlugin>>(); |
| EXPECT_CALL(*plugin, Initialize).WillOnce(Return(absl::OkStatus())); |
| EXPECT_CALL(*plugin, PreprocessRequest) |
| .WillOnce(Return(RequestAction::kDrop)); |
| |
| voyager::DeferrablePriorityQueue queue(1); |
| std::unique_ptr<milotic::RedfishPlugin> plugins[] = {std::move(plugin)}; |
| milotic::Proxy proxy("test_endpoint", plugins, &queue); |
| |
| StrictMock<MockFunction<milotic::Proxy::RequestJob::Handler>> handler; |
| |
| auto request_job = proxy.CreateRequestJob( |
| RequestVerb::kGet, proxy.CreateRequest("/test_resource")); |
| absl::StatusOr<ProxyResponse> called_with; |
| request_job->Handle(handler.AsStdFunction()); |
| ASSERT_OK(proxy.DispatchRequestToQueue(std::move(request_job))); |
| |
| EXPECT_CALL(handler, Call).WillOnce(SaveArg<0>(&called_with)); |
| ASSERT_OK(queue.ProcessQueue(1)); |
| |
| EXPECT_THAT(called_with, StatusIs(absl::StatusCode::kUnavailable)); |
| } |
| |
| TEST(TestProxyConfig, DispatchRequestToQueueFailsIfNotConfigured) { |
| milotic::Proxy proxy("test_endpoint"); |
| EXPECT_THAT(proxy.DispatchRequestToQueue(RequestVerb::kGet, |
| std::make_unique<ProxyRequest>()), |
| StatusIs(absl::StatusCode::kUnavailable)); |
| } |
| |
| class MockPermissionChecker : public milotic::PermissionChecker { |
| public: |
| MOCK_METHOD(bool, Check, |
| (const milotic::AuthorizationContext& context, |
| absl::string_view permission_id), |
| (override)); |
| }; |
| |
| TEST(TestGetRequestAction, NoMatchReturnsNext) { |
| milotic::Proxy proxy("test_endpoint"); |
| EXPECT_EQ( |
| proxy.GetRequestAction(RequestVerb::kGet, ProxyRequest("/test/resource"), |
| {{RequestVerb::kGet, "/other/resource"}}), |
| RequestAction::kNext); |
| } |
| |
| TEST(TestGetRequestAction, MatchReturnsHandle) { |
| milotic::Proxy proxy("test_endpoint"); |
| EXPECT_EQ(proxy.GetRequestAction(RequestVerb::kGet, |
| ProxyRequest("test_endpoint/test/resource"), |
| {{RequestVerb::kGet, "/other/resource"}, |
| {RequestVerb::kGet, "/test/resource"}}), |
| RequestAction::kHandle); |
| } |
| |
| TEST(TestGetRequestAction, ResourceWithMultipleVerbsReturnsHandle) { |
| milotic::Proxy proxy("test_endpoint"); |
| EXPECT_EQ(proxy.GetRequestAction(RequestVerb::kGet, |
| ProxyRequest("test_endpoint/test/resource"), |
| {{RequestVerb::kPost, "/test/resource"}, |
| {RequestVerb::kGet, "/test/resource"}, |
| {RequestVerb::kPatch, "/test/resource"}}), |
| RequestAction::kHandle); |
| } |
| |
| TEST(TestGetRequestAction, NonMatchingVerbReturnsDrop) { |
| milotic::Proxy proxy("test_endpoint"); |
| EXPECT_EQ(proxy.GetRequestAction(RequestVerb::kGet, |
| ProxyRequest("test_endpoint/test/resource"), |
| {{RequestVerb::kPost, "/test/resource"}}), |
| RequestAction::kDrop); |
| } |
| |
| TEST(TestProxyAuthz, AuthorizationPolicyIsApplied) { |
| auto permission_checker = std::make_unique<MockPermissionChecker>(); |
| EXPECT_CALL(*permission_checker, Check(_, "test-permission")) |
| .WillOnce(Return(true)) |
| .WillOnce(Return(false)); |
| milotic::Proxy proxy("test_endpoint", {}, nullptr, {}, |
| ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping1" |
| request_verb: REQUEST_VERB_GET |
| resource_path: "/some/resource" |
| allow: { |
| key: "permission", |
| value: { permission_id: "test-permission" } |
| } |
| } |
| )pb"), |
| std::move(permission_checker)); |
| milotic::FakeAuthContext auth_context(false); |
| |
| EXPECT_OK(proxy.CreateAuthorizedRequest(RequestVerb::kGet, "/some/resource", |
| auth_context)); |
| EXPECT_THAT(proxy.CreateAuthorizedRequest(RequestVerb::kGet, "/some/resource", |
| auth_context), |
| StatusIs(absl::StatusCode::kPermissionDenied)); |
| EXPECT_THAT(proxy.CreateAuthorizedRequest(RequestVerb::kPost, |
| "/some/resource", auth_context), |
| StatusIs(absl::StatusCode::kPermissionDenied)); |
| EXPECT_THAT(proxy.CreateAuthorizedRequest( |
| RequestVerb::kGet, "/some/other/resource", auth_context), |
| StatusIs(absl::StatusCode::kPermissionDenied)); |
| } |
| |
| TEST(TestProxyAuthz, QueryParametersAreRemoved) { |
| auto permission_checker = std::make_unique<MockPermissionChecker>(); |
| EXPECT_CALL(*permission_checker, Check(_, "test-permission")) |
| .Times(2) |
| .WillRepeatedly(Return(true)); |
| milotic::Proxy proxy("test_endpoint", {}, nullptr, {}, |
| ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping1" |
| request_verb: REQUEST_VERB_GET |
| resource_path: "/some" |
| allow: { |
| key: "permission", |
| value: { permission_id: "test-permission" } |
| } |
| with_subtree: true |
| } |
| )pb"), |
| std::move(permission_checker)); |
| milotic::FakeAuthContext auth_context(false); |
| |
| ASSERT_OK_AND_ASSIGN( |
| auto request, |
| proxy.CreateAuthorizedRequest( |
| RequestVerb::kGet, "/some/resource?query_params?123", auth_context)); |
| EXPECT_EQ(request->uri, "test_endpoint/some/resource"); |
| |
| ASSERT_OK_AND_ASSIGN( |
| request, proxy.CreateAuthorizedRequest(RequestVerb::kGet, |
| "/some/?$expand=*", auth_context)); |
| EXPECT_EQ(request->uri, "test_endpoint/some/"); |
| } |
| |
| TEST(TestProxyAuthz, QueryParametersArePreservedIfConfigIsSet) { |
| auto permission_checker = std::make_unique<MockPermissionChecker>(); |
| EXPECT_CALL(*permission_checker, Check(_, "test-permission")) |
| .Times(2) |
| .WillRepeatedly(Return(true)); |
| milotic::Proxy proxy("test_endpoint", {}, nullptr, {}, |
| ParseTextProtoOrDie(R"pb( |
| keep_query_params: true |
| mappings: { |
| name: "test_mapping1" |
| request_verb: REQUEST_VERB_GET |
| resource_path: "/some" |
| allow: { |
| key: "permission", |
| value: { permission_id: "test-permission" } |
| } |
| with_subtree: true |
| } |
| )pb"), |
| std::move(permission_checker)); |
| milotic::FakeAuthContext auth_context(false); |
| |
| ASSERT_OK_AND_ASSIGN( |
| auto request, |
| proxy.CreateAuthorizedRequest( |
| RequestVerb::kGet, "/some/resource?query_params?123", auth_context)); |
| EXPECT_EQ(request->uri, "test_endpoint/some/resource?query_params?123"); |
| |
| ASSERT_OK_AND_ASSIGN( |
| request, proxy.CreateAuthorizedRequest(RequestVerb::kGet, |
| "/some/?$expand=*", auth_context)); |
| EXPECT_EQ(request->uri, "test_endpoint/some/?$expand=*"); |
| } |
| |
| void ExpectQueryArbitraryParametersAreRemoved(absl::string_view parameters) { |
| auto permission_checker = std::make_unique<MockPermissionChecker>(); |
| EXPECT_CALL(*permission_checker, Check(_, "test-permission")) |
| .WillOnce(Return(true)); |
| milotic::Proxy proxy("test_endpoint", {}, nullptr, {}, |
| ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "test_mapping1" |
| request_verb: REQUEST_VERB_GET |
| resource_path: "/some/resource" |
| allow: { |
| key: "permission", |
| value: { permission_id: "test-permission" } |
| } |
| } |
| )pb"), |
| std::move(permission_checker)); |
| milotic::FakeAuthContext auth_context(false); |
| |
| ASSERT_OK_AND_ASSIGN( |
| auto request, |
| proxy.CreateAuthorizedRequest(RequestVerb::kGet, |
| absl::StrCat("/some/resource?", parameters), |
| auth_context)); |
| EXPECT_EQ(request->uri, "test_endpoint/some/resource"); |
| } |
| |
| FUZZ_TEST(ProxyAuthzFuzzTest, ExpectQueryArbitraryParametersAreRemoved); |
| |
| } // namespace |