Skip to content

Commit 446f6c8

Browse files
authored
impl: add gRPC PQC test (#16282)
1 parent fdcd3a9 commit 446f6c8

4 files changed

Lines changed: 324 additions & 9 deletions

File tree

ci/cloudbuild/builds/showcase.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,5 @@ io::log_h2 "Running showcase tests"
9696
bazel test --test_env=SHOWCASE_PORT="${SHOWCASE_PORT}" \
9797
--test_env=SHOWCASE_CA_CERT="${SHOWCASE_CA_CERT}" \
9898
--test_output=errors \
99-
//ci/showcase:rest_pqc_test
99+
//ci/showcase:rest_pqc_test \
100+
//ci/showcase:grpc_pqc_test

ci/showcase/BUILD.bazel.in

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,23 @@ cc_library(
9494
],
9595
)
9696

97+
cc_test(
98+
name = "grpc_pqc_test",
99+
srcs = ["grpc_pqc_test.cc"],
100+
copts = [
101+
"-Ici/showcase",
102+
"-I$(BINDIR)/ci/showcase",
103+
],
104+
tags = ["integration-test"],
105+
deps = [
106+
":showcase_echo_client",
107+
"//:common",
108+
"//:grpc_utils",
109+
"//google/cloud/testing_util:google_cloud_cpp_testing_private",
110+
"@googletest//:gtest_main",
111+
],
112+
)
113+
97114
cc_test(
98115
name = "rest_pqc_test",
99116
srcs = ["rest_pqc_test.cc"],

ci/showcase/grpc_pqc_test.cc

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "google/cloud/common_options.h"
16+
#include "google/cloud/credentials.h"
17+
#include "google/cloud/grpc_options.h"
18+
#include "google/cloud/internal/background_threads_impl.h"
19+
#include "google/cloud/internal/unified_grpc_credentials.h"
20+
#include "google/cloud/testing_util/status_matchers.h"
21+
#include "absl/strings/match.h"
22+
#include "absl/strings/str_cat.h"
23+
#include "google/showcase/v1beta1/echo_client.h"
24+
#include "google/showcase/v1beta1/internal/echo_connection_impl.h"
25+
#include "google/showcase/v1beta1/internal/echo_metadata_decorator.h"
26+
#include "google/showcase/v1beta1/internal/echo_option_defaults.h"
27+
#include "google/showcase/v1beta1/internal/echo_stub.h"
28+
#include "google/showcase/v1beta1/internal/echo_stub_factory.h"
29+
#include <gmock/gmock.h>
30+
#include <gtest/gtest.h>
31+
#include <chrono>
32+
#include <cstdlib>
33+
#include <fstream>
34+
#include <functional>
35+
#include <map>
36+
#include <memory>
37+
#include <string>
38+
#include <vector>
39+
40+
namespace google {
41+
namespace cloud {
42+
namespace v1beta1 {
43+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
44+
namespace {
45+
46+
using ::google::cloud::testing_util::IsOkAndHolds;
47+
using ::testing::Eq;
48+
using ::testing::HasSubstr;
49+
using ::testing::IsEmpty;
50+
using ::testing::Not;
51+
using ::testing::NotNull;
52+
53+
class HeaderInterceptingEchoStub : public v1beta1_internal::EchoStub {
54+
public:
55+
HeaderInterceptingEchoStub(
56+
std::shared_ptr<v1beta1_internal::EchoStub> delegate,
57+
std::function<void(std::multimap<std::string, std::string> const&)>
58+
metadata_callback)
59+
: delegate_(std::move(delegate)),
60+
metadata_callback_(std::move(metadata_callback)) {}
61+
62+
~HeaderInterceptingEchoStub() override = default;
63+
64+
StatusOr<google::showcase::v1beta1::EchoResponse> Echo(
65+
grpc::ClientContext& context, Options const& options,
66+
google::showcase::v1beta1::EchoRequest const& request) override {
67+
auto response = delegate_->Echo(context, options, request);
68+
ExtractMetadata(context);
69+
return response;
70+
}
71+
72+
StatusOr<google::showcase::v1beta1::EchoErrorDetailsResponse>
73+
EchoErrorDetails(grpc::ClientContext& context, Options const& options,
74+
google::showcase::v1beta1::EchoErrorDetailsRequest const&
75+
request) override {
76+
return delegate_->EchoErrorDetails(context, options, request);
77+
}
78+
79+
StatusOr<google::showcase::v1beta1::FailEchoWithDetailsResponse>
80+
FailEchoWithDetails(
81+
grpc::ClientContext& context, Options const& options,
82+
google::showcase::v1beta1::FailEchoWithDetailsRequest const& request)
83+
override {
84+
return delegate_->FailEchoWithDetails(context, options, request);
85+
}
86+
87+
future<StatusOr<google::longrunning::Operation>> AsyncWait(
88+
google::cloud::CompletionQueue& cq,
89+
std::shared_ptr<grpc::ClientContext> context,
90+
google::cloud::internal::ImmutableOptions options,
91+
google::showcase::v1beta1::WaitRequest const& request) override {
92+
return delegate_->AsyncWait(cq, std::move(context), std::move(options),
93+
request);
94+
}
95+
96+
StatusOr<google::longrunning::Operation> Wait(
97+
grpc::ClientContext& context, Options options,
98+
google::showcase::v1beta1::WaitRequest const& request) override {
99+
return delegate_->Wait(context, std::move(options), request);
100+
}
101+
102+
StatusOr<google::showcase::v1beta1::BlockResponse> Block(
103+
grpc::ClientContext& context, Options const& options,
104+
google::showcase::v1beta1::BlockRequest const& request) override {
105+
return delegate_->Block(context, options, request);
106+
}
107+
108+
StatusOr<google::cloud::location::ListLocationsResponse> ListLocations(
109+
grpc::ClientContext& context, Options const& options,
110+
google::cloud::location::ListLocationsRequest const& request) override {
111+
return delegate_->ListLocations(context, options, request);
112+
}
113+
114+
StatusOr<google::cloud::location::Location> GetLocation(
115+
grpc::ClientContext& context, Options const& options,
116+
google::cloud::location::GetLocationRequest const& request) override {
117+
return delegate_->GetLocation(context, options, request);
118+
}
119+
120+
StatusOr<google::iam::v1::Policy> SetIamPolicy(
121+
grpc::ClientContext& context, Options const& options,
122+
google::iam::v1::SetIamPolicyRequest const& request) override {
123+
return delegate_->SetIamPolicy(context, options, request);
124+
}
125+
126+
StatusOr<google::iam::v1::Policy> GetIamPolicy(
127+
grpc::ClientContext& context, Options const& options,
128+
google::iam::v1::GetIamPolicyRequest const& request) override {
129+
return delegate_->GetIamPolicy(context, options, request);
130+
}
131+
132+
StatusOr<google::iam::v1::TestIamPermissionsResponse> TestIamPermissions(
133+
grpc::ClientContext& context, Options const& options,
134+
google::iam::v1::TestIamPermissionsRequest const& request) override {
135+
return delegate_->TestIamPermissions(context, options, request);
136+
}
137+
138+
StatusOr<google::longrunning::ListOperationsResponse> ListOperations(
139+
grpc::ClientContext& context, Options const& options,
140+
google::longrunning::ListOperationsRequest const& request) override {
141+
return delegate_->ListOperations(context, options, request);
142+
}
143+
144+
StatusOr<google::longrunning::Operation> GetOperation(
145+
grpc::ClientContext& context, Options const& options,
146+
google::longrunning::GetOperationRequest const& request) override {
147+
return delegate_->GetOperation(context, options, request);
148+
}
149+
150+
Status DeleteOperation(
151+
grpc::ClientContext& context, Options const& options,
152+
google::longrunning::DeleteOperationRequest const& request) override {
153+
return delegate_->DeleteOperation(context, options, request);
154+
}
155+
156+
Status CancelOperation(
157+
grpc::ClientContext& context, Options const& options,
158+
google::longrunning::CancelOperationRequest const& request) override {
159+
return delegate_->CancelOperation(context, options, request);
160+
}
161+
162+
future<StatusOr<google::longrunning::Operation>> AsyncGetOperation(
163+
google::cloud::CompletionQueue& cq,
164+
std::shared_ptr<grpc::ClientContext> context,
165+
google::cloud::internal::ImmutableOptions options,
166+
google::longrunning::GetOperationRequest const& request) override {
167+
return delegate_->AsyncGetOperation(cq, std::move(context),
168+
std::move(options), request);
169+
}
170+
171+
future<Status> AsyncCancelOperation(
172+
google::cloud::CompletionQueue& cq,
173+
std::shared_ptr<grpc::ClientContext> context,
174+
google::cloud::internal::ImmutableOptions options,
175+
google::longrunning::CancelOperationRequest const& request) override {
176+
return delegate_->AsyncCancelOperation(cq, std::move(context),
177+
std::move(options), request);
178+
}
179+
180+
private:
181+
void ExtractMetadata(grpc::ClientContext& context) {
182+
auto to_string = [](grpc::string_ref ref) {
183+
return ref.empty() ? std::string{} : std::string{ref.data(), ref.size()};
184+
};
185+
std::multimap<std::string, std::string> metadata;
186+
for (auto const& pair : context.GetServerInitialMetadata()) {
187+
metadata.emplace(to_string(pair.first), to_string(pair.second));
188+
}
189+
for (auto const& pair : context.GetServerTrailingMetadata()) {
190+
metadata.emplace(to_string(pair.first), to_string(pair.second));
191+
}
192+
metadata_callback_(metadata);
193+
}
194+
195+
std::shared_ptr<v1beta1_internal::EchoStub> delegate_;
196+
std::function<void(std::multimap<std::string, std::string> const&)>
197+
metadata_callback_;
198+
};
199+
200+
TEST(EchoGrpcIntegrationTest, EchoSuccessGrpcWithPqcVerification) {
201+
std::string ca_path;
202+
if (auto* ca_env = std::getenv("SHOWCASE_CA_CERT")) {
203+
ca_path = ca_env;
204+
} else {
205+
auto* test_srcdir = std::getenv("TEST_SRCDIR");
206+
ASSERT_THAT(test_srcdir, NotNull());
207+
ca_path = std::string(test_srcdir) + "/_main/ci/showcase/showcase.pem";
208+
}
209+
210+
std::ifstream ca_file(ca_path);
211+
ASSERT_TRUE(ca_file.good()) << "Failed to open CA file at " << ca_path;
212+
213+
std::string port = "7469";
214+
if (auto* port_env = std::getenv("SHOWCASE_PORT")) {
215+
port = port_env;
216+
}
217+
std::string endpoint = absl::StrCat("localhost:", port);
218+
219+
auto credentials = MakeAccessTokenCredentials(
220+
"dummy-token", std::chrono::system_clock::now() + std::chrono::hours(1));
221+
222+
auto options = Options{}
223+
.set<EndpointOption>(endpoint)
224+
.set<CARootsFilePathOption>(ca_path)
225+
.set<UnifiedCredentialsOption>(credentials);
226+
options = v1beta1_internal::EchoDefaultOptions(std::move(options));
227+
228+
auto background = internal::MakeBackgroundThreadsFactory(options)();
229+
auto auth = internal::CreateAuthenticationStrategy(background->cq(), options);
230+
auto real_stub = v1beta1_internal::CreateDefaultEchoStub(auth, options);
231+
232+
std::multimap<std::string, std::string> intercepted_metadata;
233+
auto metadata_callback =
234+
[&intercepted_metadata](
235+
std::multimap<std::string, std::string> const& metadata) {
236+
intercepted_metadata = metadata;
237+
};
238+
239+
std::shared_ptr<v1beta1_internal::EchoStub> stub =
240+
std::make_shared<HeaderInterceptingEchoStub>(
241+
std::move(real_stub), std::move(metadata_callback));
242+
243+
stub = std::make_shared<v1beta1_internal::EchoMetadata>(
244+
std::move(stub), std::multimap<std::string, std::string>{});
245+
246+
auto connection = std::make_shared<v1beta1_internal::EchoConnectionImpl>(
247+
std::move(background), std::move(stub), options);
248+
249+
auto client = EchoClient(connection);
250+
251+
::google::showcase::v1beta1::EchoRequest request;
252+
request.set_content("Hello from C++ GAPIC gRPC!");
253+
254+
auto response = client.Echo(request);
255+
ASSERT_STATUS_OK(response);
256+
EXPECT_THAT(response->content(), Eq("Hello from C++ GAPIC gRPC!"));
257+
258+
auto get_metadata_value =
259+
[](std::multimap<std::string, std::string> const& metadata,
260+
std::string const& key) -> std::string {
261+
for (auto const& pair : metadata) {
262+
if (absl::EqualsIgnoreCase(pair.first, key)) {
263+
return pair.second;
264+
}
265+
}
266+
return "";
267+
};
268+
269+
std::string tls_group =
270+
get_metadata_value(intercepted_metadata, "x-showcase-tls-group");
271+
std::string supported_groups = get_metadata_value(
272+
intercepted_metadata, "x-showcase-tls-client-supported-groups");
273+
274+
EXPECT_THAT(tls_group, Not(IsEmpty()))
275+
<< "x-showcase-tls-group metadata not found";
276+
EXPECT_THAT(supported_groups, Not(IsEmpty()))
277+
<< "x-showcase-tls-client-supported-groups metadata not found";
278+
279+
// Assert PQC was used.
280+
EXPECT_THAT(tls_group, Eq("X25519MLKEM768"));
281+
EXPECT_THAT(supported_groups, HasSubstr("X25519MLKEM768"));
282+
}
283+
284+
} // namespace
285+
} // namespace v1beta1
286+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
287+
} // namespace cloud
288+
} // namespace google

ci/showcase/rest_pqc_test.cc

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
#include "google/cloud/internal/curl_options.h" // for HttpVersionOption
1919
#include "google/cloud/internal/rest_background_threads_impl.h"
2020
#include "google/cloud/internal/rest_client.h"
21+
#include "google/cloud/testing_util/status_matchers.h"
2122
#include "absl/strings/match.h"
2223
#include "absl/strings/str_cat.h"
2324
#include "google/showcase/v1beta1/echo_client.h"
2425
#include "google/showcase/v1beta1/internal/echo_option_defaults.h"
2526
#include "google/showcase/v1beta1/internal/echo_rest_connection_impl.h"
2627
#include "google/showcase/v1beta1/internal/echo_rest_metadata_decorator.h"
2728
#include "google/showcase/v1beta1/internal/echo_rest_stub.h"
29+
#include <gmock/gmock.h>
2830
#include <gtest/gtest.h>
2931
#include <chrono>
3032
#include <cstdlib>
@@ -44,6 +46,12 @@ using ::google::cloud::rest_internal::RestClient;
4446
using ::google::cloud::rest_internal::RestContext;
4547
using ::google::cloud::rest_internal::RestRequest;
4648
using ::google::cloud::rest_internal::RestResponse;
49+
using ::google::cloud::testing_util::IsOkAndHolds;
50+
using ::testing::Eq;
51+
using ::testing::HasSubstr;
52+
using ::testing::IsEmpty;
53+
using ::testing::Not;
54+
using ::testing::NotNull;
4755

4856
class HeaderInterceptingRestClient : public RestClient {
4957
public:
@@ -113,7 +121,7 @@ TEST(EchoRestIntegrationTest, EchoSuccessRestWithPqcVerification) {
113121
ca_path = ca_env;
114122
} else {
115123
auto* test_srcdir = std::getenv("TEST_SRCDIR");
116-
ASSERT_NE(test_srcdir, nullptr);
124+
ASSERT_THAT(test_srcdir, NotNull());
117125
ca_path = std::string(test_srcdir) + "/_main/ci/showcase/showcase.pem";
118126
}
119127

@@ -148,7 +156,7 @@ TEST(EchoRestIntegrationTest, EchoSuccessRestWithPqcVerification) {
148156

149157
// Create the real REST client
150158
auto real_client = rest_internal::MakePooledRestClient(endpoint, options);
151-
ASSERT_NE(real_client, nullptr) << "Failed to create real REST client";
159+
ASSERT_THAT(real_client, NotNull()) << "Failed to create real REST client";
152160

153161
// Wrap it with our interceptor
154162
auto intercepting_client = std::make_shared<HeaderInterceptingRestClient>(
@@ -179,8 +187,8 @@ TEST(EchoRestIntegrationTest, EchoSuccessRestWithPqcVerification) {
179187
request.set_content("Hello from C++ GAPIC REST!");
180188

181189
auto response = client.Echo(request);
182-
ASSERT_TRUE(response.ok()) << response.status().message();
183-
EXPECT_EQ(response->content(), "Hello from C++ GAPIC REST!");
190+
ASSERT_STATUS_OK(response);
191+
EXPECT_THAT(response->content(), Eq("Hello from C++ GAPIC REST!"));
184192

185193
// Verify headers
186194
auto get_header_value =
@@ -199,13 +207,14 @@ TEST(EchoRestIntegrationTest, EchoSuccessRestWithPqcVerification) {
199207
std::string supported_groups = get_header_value(
200208
intercepted_headers, "x-showcase-tls-client-supported-groups");
201209

202-
EXPECT_FALSE(tls_group.empty()) << "x-showcase-tls-group header not found";
203-
EXPECT_FALSE(supported_groups.empty())
210+
EXPECT_THAT(tls_group, Not(IsEmpty()))
211+
<< "x-showcase-tls-group header not found";
212+
EXPECT_THAT(supported_groups, Not(IsEmpty()))
204213
<< "x-showcase-tls-client-supported-groups header not found";
205214

206215
// Assert PQC was used.
207-
EXPECT_EQ(tls_group, "X25519MLKEM768");
208-
EXPECT_TRUE(absl::StrContains(supported_groups, "X25519MLKEM768"));
216+
EXPECT_THAT(tls_group, Eq("X25519MLKEM768"));
217+
EXPECT_THAT(supported_groups, HasSubstr("X25519MLKEM768"));
209218
}
210219

211220
} // namespace

0 commit comments

Comments
 (0)