Adds Protobuf support to Client<>. (#34505)

This commit is contained in:
Szilard Szaloki
2026-03-06 15:28:26 -07:00
committed by GitHub
parent afa1f8b8f3
commit 0a2b5cea96
40 changed files with 1455 additions and 886 deletions
@@ -160,10 +160,10 @@ void BraveAccountService::RegisterInitialize(
}
auto request = MakeRequest<PasswordInit::Request>();
request.blinded_message = blinded_message;
request.initiating_service_name = "accounts";
request.new_account_email = email;
request.serialize_response = true;
request.body.blinded_message = blinded_message;
request.body.initiating_service_name = "accounts";
request.body.new_account_email = email;
request.body.serialize_response = true;
Client<PasswordInit>::Send(
url_loader_factory_, std::move(request),
base::BindOnce(&BraveAccountService::OnRegisterInitialize,
@@ -188,7 +188,7 @@ void BraveAccountService::RegisterFinalize(
auto request = MakeRequest<WithHeaders<PasswordFinalize::Request>>();
SetBearerToken(request, verification_token);
request.serialized_record = serialized_record;
request.body.serialized_record = serialized_record;
Client<PasswordFinalize>::Send(
url_loader_factory_, std::move(request),
base::BindOnce(&BraveAccountService::OnRegisterFinalize,
@@ -219,7 +219,7 @@ void BraveAccountService::ResendConfirmationEmail(
SetBearerToken(request, verification_token);
// Server side will determine locale based on the Accept-Language request
// header (which is included automatically by upstream).
request.locale = "";
request.body.locale = "";
request.timeout_duration = kVerifyResendTimeout;
Client<VerifyResend>::Send(
url_loader_factory_, std::move(request),
@@ -254,9 +254,9 @@ void BraveAccountService::LoginInitialize(const std::string& email,
}
auto request = MakeRequest<LoginInit::Request>();
request.email = email;
request.initiating_service_name = "accounts";
request.serialized_ke1 = serialized_ke1;
request.body.email = email;
request.body.initiating_service_name = "accounts";
request.body.serialized_ke1 = serialized_ke1;
Client<LoginInit>::Send(
url_loader_factory_, std::move(request),
base::BindOnce(&BraveAccountService::OnLoginInitialize,
@@ -279,7 +279,7 @@ void BraveAccountService::LoginFinalize(
auto request = MakeRequest<WithHeaders<LoginFinalize::Request>>();
SetBearerToken(request, login_token);
request.client_mac = client_mac;
request.body.client_mac = client_mac;
Client<endpoints::LoginFinalize>::Send(
url_loader_factory_, std::move(request),
base::BindOnce(&BraveAccountService::OnLoginFinalize,
@@ -343,7 +343,7 @@ void BraveAccountService::GetServiceToken(mojom::Service service,
auto request = MakeRequest<WithHeaders<ServiceToken::Request>>();
SetBearerToken(request, authentication_token);
request.service = service_name;
request.body.service = service_name;
Client<ServiceToken>::Send(
url_loader_factory_, std::move(request),
base::BindOnce(&BraveAccountService::OnGetServiceToken,
@@ -497,7 +497,7 @@ void BraveAccountService::VerifyResult(
auto request = MakeRequest<WithHeaders<VerifyResult::Request>>();
SetBearerToken(request, verification_token);
request.wait = false;
request.body.wait = false;
current_verify_result_request =
Client<endpoints::VerifyResult>::Send<RequestCancelability::kCancelable>(
url_loader_factory_, std::move(request),
@@ -3,6 +3,9 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.
import("//third_party/protobuf/proto_library.gni")
import("//tools/json_schema_compiler/json_schema_api.gni")
source_set("endpoint_client") {
sources = [
"brave_endpoint.h",
@@ -27,8 +30,26 @@ source_set("endpoint_client") {
"//brave/brave_domains",
"//net",
"//services/network/public/cpp",
"//third_party/protobuf:protobuf_lite",
"//url",
]
public_deps = [
"//brave/components/brave_account/endpoint_client:json_empty_body",
"//brave/components/brave_account/endpoint_client:protobuf_empty_body",
]
}
generated_types("json_empty_body") {
sources = [ "json_empty_body.idl" ]
deps = [ "//base" ]
root_namespace = "brave_account::%(namespace)s"
}
proto_library("protobuf_empty_body") {
sources = [ "protobuf_empty_body.proto" ]
}
source_set("test_support") {
@@ -50,7 +71,6 @@ source_set("unit_tests") {
sources = [
"client_unittest.cc",
"concept_test.h",
"is_endpoint_unittest.cc",
"is_request_body_unittest.cc",
"is_request_unittest.cc",
@@ -63,11 +83,30 @@ source_set("unit_tests") {
"//base",
"//base/test:test_support",
"//brave/components/brave_account/endpoint_client",
"//brave/components/brave_account/endpoint_client:json_test_endpoint_bodies",
"//brave/components/brave_account/endpoint_client:protobuf_test_endpoint_bodies",
"//net",
"//services/network:test_support",
"//services/network/public/cpp",
"//services/network/public/mojom",
"//testing/gtest",
"//third_party/protobuf:protobuf_lite",
"//url",
]
}
generated_types("json_test_endpoint_bodies") {
# testonly = true
sources = [ "json_test_endpoint_bodies.idl" ]
deps = [ "//base" ]
root_namespace = "brave_account::%(namespace)s"
}
proto_library("protobuf_test_endpoint_bodies") {
testonly = true
sources = [ "protobuf_test_endpoint_bodies.proto" ]
}
@@ -0,0 +1,6 @@
include_rules = [
"+google/protobuf",
"+services/network/public/cpp",
"+services/network/public/mojom",
"+services/network/test",
]
+86 -143
View File
@@ -18,30 +18,21 @@
#include "base/check_is_test.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/memory/scoped_refptr.h"
#include "base/task/sequenced_task_runner.h"
#include "base/types/expected.h"
#include "base/types/is_instantiation.h"
#include "base/values.h"
#include "base/types/to_address.h"
#include "brave/components/brave_account/endpoint_client/is_endpoint.h"
#include "brave/components/brave_account/endpoint_client/maybe_strip_with_headers.h"
#include "brave/components/brave_account/endpoint_client/request_handle.h"
#include "brave/components/brave_account/endpoint_client/response.h"
#include "brave/components/brave_account/endpoint_client/with_headers.h"
#include "net/base/load_flags.h"
#include "net/base/net_errors.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/header_util.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"
// See //brave/components/brave_account/endpoint_client/README.md
// for design, motivation, usage, and examples.
@@ -50,8 +41,92 @@ namespace brave_account::endpoint_client {
enum class RequestCancelability { kNonCancelable, kCancelable };
template <IsEndpoint T>
// Client<> performs a full network roundtrip for a given endpoint using
// network::SimpleURLLoader. It serializes the endpoint's Request type,
// performs the network call, and invokes a callback with the response
// deserialized as the endpoint's Response type. Requests and responses may
// optionally be wrapped in WithHeaders<> to include HTTP headers. Requests
// can also be made cancelable, in which case Send<>() returns a RequestHandle
// that can cancel the request.
template <IsEndpoint Endpoint>
class Client {
public:
// [[nodiscard]] enforces that callers retain the returned handle in
// the cancelable case. It has no effect in the non-cancelable case,
// where the return type is void.
template <RequestCancelability C = RequestCancelability::kNonCancelable,
typename Request,
typename Response>
requires(std::same_as<detail::MaybeStripWithHeaders<Request>,
typename Endpoint::Request> &&
std::same_as<detail::MaybeStripWithHeaders<Response>,
typename Endpoint::Response>)
[[nodiscard]] static auto Send(
const scoped_refptr<network::SharedURLLoaderFactory>& url_loader_factory,
Request request,
base::OnceCallback<void(Response)> callback) {
CHECK(url_loader_factory);
if (!request.network_traffic_annotation_tag.is_valid()) {
CHECK_IS_TEST()
<< "Client<> requires a valid network traffic annotation and "
"only permits a missing annotation in tests.";
request.network_traffic_annotation_tag =
net::MutableNetworkTrafficAnnotationTag(MISSING_TRAFFIC_ANNOTATION);
}
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = Endpoint::URL();
resource_request->method = Request::Method();
resource_request->load_flags = net::LOAD_BYPASS_CACHE |
net::LOAD_DISABLE_CACHE |
net::LOAD_DO_NOT_SAVE_COOKIES;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
if constexpr (base::is_instantiation<Request, WithHeaders>) {
resource_request->headers = std::move(request.headers);
}
auto simple_url_loader = network::SimpleURLLoader::Create(
std::move(resource_request),
static_cast<net::NetworkTrafficAnnotationTag>(
request.network_traffic_annotation_tag));
simple_url_loader->SetAllowHttpErrorResults(true);
if (auto upload_data = request.Serialize()) {
CHECK(!upload_data->empty()) << "Failed to serialize request!";
simple_url_loader->AttachStringForUpload(std::move(*upload_data),
Request::ContentType());
}
simple_url_loader->SetTimeoutDuration(request.timeout_duration);
simple_url_loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
url_loader_factory.get(),
base::BindOnce(OnResponse<Response>, std::move(callback),
MaybeMoveLoader<C>(simple_url_loader)));
return MaybeMakeHandle<C>(simple_url_loader);
}
private:
template <typename Response>
static void OnResponse(
base::OnceCallback<void(Response)> callback,
std::variant<std::unique_ptr<network::SimpleURLLoader>,
network::SimpleURLLoader*> simple_url_loader_ptr,
std::optional<std::string> response_body) {
const network::SimpleURLLoader& simple_url_loader = CHECK_DEREF(
std::visit([](const auto& ptr) { return base::to_address(ptr); },
simple_url_loader_ptr));
auto* response_info = simple_url_loader.ResponseInfo();
auto headers = response_info ? response_info->headers : nullptr;
Response response(Response::Deserialize(simple_url_loader.NetError(),
headers, std::move(response_body)));
if constexpr (base::is_instantiation<Response, WithHeaders>) {
response.headers = std::move(headers);
}
std::move(callback).Run(std::move(response));
}
// Depending on |C|, either takes ownership of (moves out) |simple_url_loader|
// (non-cancelable case), or returns a raw pointer to the loader (cancelable
// case).
@@ -81,138 +156,6 @@ class Client {
base::SequencedTaskRunner::GetCurrentDefault()));
}
}
public:
// [[nodiscard]] enforces that callers retain the returned handle in
// the cancelable case. It has no effect in the non-cancelable case,
// where the return type is void.
template <RequestCancelability C = RequestCancelability::kNonCancelable,
typename Request,
typename Response>
requires(std::same_as<detail::MaybeStripWithHeaders<Request>,
typename T::Request> &&
std::same_as<detail::MaybeStripWithHeaders<Response>,
typename T::Response>)
[[nodiscard]] static auto Send(
const scoped_refptr<network::SharedURLLoaderFactory>& url_loader_factory,
Request request,
base::OnceCallback<void(Response)> callback) {
CHECK(url_loader_factory);
if (!request.network_traffic_annotation_tag.is_valid()) {
CHECK_IS_TEST()
<< "Client<> requires a valid network traffic annotation and "
"only permits a missing annotation in tests.";
request.network_traffic_annotation_tag =
net::MutableNetworkTrafficAnnotationTag(MISSING_TRAFFIC_ANNOTATION);
}
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = T::URL();
resource_request->method = Request::Method();
resource_request->load_flags = net::LOAD_BYPASS_CACHE |
net::LOAD_DISABLE_CACHE |
net::LOAD_DO_NOT_SAVE_COOKIES;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
if constexpr (base::is_instantiation<Request, WithHeaders>) {
resource_request->headers = std::move(request.headers);
}
auto simple_url_loader = network::SimpleURLLoader::Create(
std::move(resource_request),
static_cast<net::NetworkTrafficAnnotationTag>(
request.network_traffic_annotation_tag));
simple_url_loader->SetAllowHttpErrorResults(true);
if (const auto dict = request.ToValue(); !dict.empty()) {
auto json = base::WriteJson(dict).value_or("");
CHECK(!json.empty()) << "Failed to serialize request to JSON!";
simple_url_loader->AttachStringForUpload(std::move(json),
"application/json");
}
simple_url_loader->SetTimeoutDuration(request.timeout_duration);
simple_url_loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
url_loader_factory.get(),
base::BindOnce(OnResponse<Response>, std::move(callback),
MaybeMoveLoader<C>(simple_url_loader)));
return MaybeMakeHandle<C>(simple_url_loader);
}
private:
// Body parsing behavior:
//
// response.body remains std::nullopt (no parsing attempted) when:
// - network error occurred (response.net_error != net::OK)
// - response info is missing (response_info is nullptr)
// - response headers are missing (response_info->headers is nullptr)
//
// When parsing is attempted, two cases apply:
//
// 1. No body expected (std::is_empty_v<SuccessBody/ErrorBody>):
// Whatever the server returns is ignored. We replace it with "{}" so
// response.body is always a valid instance of the empty type, never
// std::nullopt. This also means parsing failures are ignored, which
// makes sense: if you don't expect data from the server, you shouldn't
// care whether what it sent was parseable or not.
//
// 2. Body expected (!std::is_empty_v<SuccessBody/ErrorBody>):
// response.body is std::nullopt (parsing failure) when response_body is:
// - std::nullopt (no body received)
// - empty string
// - plain text (not JSON)
// - invalid JSON
// - valid JSON with wrong structure
// In all these cases, either JSONReader::Read() or FromValue() fails.
//
// response.body is non-std::nullopt only when response_body contains
// valid JSON matching the expected SuccessBody/ErrorBody structure.
template <typename Response>
static void OnResponse(
base::OnceCallback<void(Response)> callback,
std::variant<std::unique_ptr<network::SimpleURLLoader>,
network::SimpleURLLoader*> simple_url_loader,
std::optional<std::string> response_body) {
const auto& simple_url_loader_ref = std::visit(
[](const auto& ptr) -> network::SimpleURLLoader& {
return CHECK_DEREF(ptr);
},
simple_url_loader);
Response response;
response.net_error = simple_url_loader_ref.NetError();
auto* const response_info = simple_url_loader_ref.ResponseInfo();
auto headers = response_info ? response_info->headers : nullptr;
if (response.net_error != net::OK || !headers) {
return std::move(callback).Run(std::move(response));
}
response.status_code = headers->response_code();
if constexpr (base::is_instantiation<Response, WithHeaders>) {
response.headers = std::move(headers);
}
const bool is_2xx = network::IsSuccessfulStatus(*response.status_code);
if (is_2xx ? std::is_empty_v<typename Response::SuccessBody>
: std::is_empty_v<typename Response::ErrorBody>) {
response_body = "{}";
}
const auto value =
base::JSONReader::Read(response_body.value_or(""), base::JSON_PARSE_RFC)
.value_or(base::Value());
if (is_2xx) {
response.body = Response::SuccessBody::FromValue(value);
} else {
response.body =
Response::ErrorBody::FromValue(value).transform([](auto error_body) {
return base::unexpected(std::move(error_body));
});
}
std::move(callback).Run(std::move(response));
}
};
} // namespace brave_account::endpoint_client
@@ -5,6 +5,7 @@
#include "brave/components/brave_account/endpoint_client/client.h"
#include <concepts>
#include <optional>
#include <string>
#include <string_view>
@@ -14,37 +15,36 @@
#include "base/check.h"
#include "base/check_deref.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/location.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/to_string.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/no_destructor.h"
#include "base/test/run_until.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/threading/thread.h"
#include "base/types/expected.h"
#include "base/types/is_instantiation.h"
#include "base/values.h"
#include "brave/components/brave_account/endpoint_client/is_endpoint.h"
#include "brave/components/brave_account/endpoint_client/is_request.h"
#include "brave/components/brave_account/endpoint_client/is_response.h"
#include "brave/components/brave_account/endpoint_client/is_response_body.h"
#include "brave/components/brave_account/endpoint_client/json_empty_body.h"
#include "brave/components/brave_account/endpoint_client/json_test_endpoint_bodies.h"
#include "brave/components/brave_account/endpoint_client/maybe_strip_with_headers.h"
#include "brave/components/brave_account/endpoint_client/protobuf_empty_body.pb.h"
#include "brave/components/brave_account/endpoint_client/protobuf_test_endpoint_bodies.pb.h"
#include "brave/components/brave_account/endpoint_client/request_handle.h"
#include "brave/components/brave_account/endpoint_client/request_types.h"
#include "brave/components/brave_account/endpoint_client/response.h"
#include "brave/components/brave_account/endpoint_client/static_string.h"
#include "brave/components/brave_account/endpoint_client/with_headers.h"
#include "net/base/net_errors.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/http/http_version.h"
#include "services/network/public/cpp/data_element.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/resource_request_body.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/url_request.mojom-shared.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "services/network/test/test_utils.h"
@@ -75,310 +75,526 @@ bool operator==(const WithHeaders<Response<T, E>>& lhs,
return lhs.headers->StrictlyEquals(*rhs.headers);
}
bool operator==(const JSONRequestBody& lhs, const JSONRequestBody& rhs) {
return lhs.request == rhs.request;
}
bool operator==(const JSONSuccessBody& lhs, const JSONSuccessBody& rhs) {
return lhs.success == rhs.success;
}
bool operator==(const JSONErrorBody& lhs, const JSONErrorBody& rhs) {
return lhs.error == rhs.error;
}
bool operator==(const JSONEmptyBody&, const JSONEmptyBody&) {
return true;
}
bool operator==(const ProtobufRequestBody& lhs,
const ProtobufRequestBody& rhs) {
return lhs.request() == rhs.request();
}
bool operator==(const ProtobufSuccessBody& lhs,
const ProtobufSuccessBody& rhs) {
return lhs.success() == rhs.success();
}
bool operator==(const ProtobufErrorBody& lhs, const ProtobufErrorBody& rhs) {
return lhs.error() == rhs.error();
}
bool operator==(const ProtobufEmptyBody&, const ProtobufEmptyBody&) {
return true;
}
namespace {
template <detail::StaticString Key>
struct Body {
base::DictValue ToValue() const {
return base::DictValue().Set(Key.value, text);
}
static std::optional<Body> FromValue(const base::Value& value) {
const auto* dict = value.GetIfDict();
if (!dict) {
return std::nullopt;
}
const auto* found = dict->FindString(Key.value);
if (!found) {
return std::nullopt;
}
return Body(*found);
}
bool operator==(const Body& other) const { return text == other.text; }
std::string text;
};
template <>
struct Body<""> {
base::DictValue ToValue() const { return base::DictValue(); }
static std::optional<Body> FromValue(const base::Value& value) {
return value.is_dict() ? Body() : std::optional<Body>();
}
bool operator==(const Body&) const { return true; }
};
inline constexpr char kRequestKey[] = "request";
inline constexpr char kSuccessKey[] = "success";
inline constexpr char kErrorKey[] = "error";
inline constexpr char kEmptyKey[] = "";
using TestRequestBody = Body<kRequestKey>;
using TestSuccessBody = Body<kSuccessKey>;
using TestErrorBody = Body<kErrorKey>;
using TestEmptyBody = Body<kEmptyKey>;
template <typename ResponseT>
struct Endpoint {
using Request = POST<TestRequestBody>;
template <detail::IsRequest RequestT, detail::IsResponse ResponseT>
struct TestEndpoint {
using Request = RequestT;
using Response = ResponseT;
static GURL URL() { return GURL("https://example.com/api/query"); }
};
// Request: POST https://example.com/api/query { "request": "..." }
// Success: { "success": "..." }
// Error: { "error": "..." }
using TestEndpoint = Endpoint<Response<TestSuccessBody, TestErrorBody>>;
// Request: POST https://example.com/api/query { "request": "abc" }
// Success: { "success": "ok" }
// Error: { "error": "nope" }
using JSONEndpointSuccessError =
TestEndpoint<POST<JSONRequestBody>,
Response<JSONSuccessBody, JSONErrorBody>>;
// Request: POST https://example.com/api/query { "request": "..." }
// Success: ignored
// Error: { "error": "..." }
using TestEndpointWithEmptySuccessBody =
Endpoint<Response<TestEmptyBody, TestErrorBody>>;
// Request: POST https://example.com/api/query { "request": "abc" }
// Success: ignored (response body not parsed)
// Error: { "error": "nope" }
using JSONEndpointEmptyError =
TestEndpoint<POST<JSONRequestBody>, Response<JSONEmptyBody, JSONErrorBody>>;
// Request: POST https://example.com/api/query { "request": "..." }
// Success: { "success": "..." }
// Error: ignored
using TestEndpointWithEmptyErrorBody =
Endpoint<Response<TestSuccessBody, TestEmptyBody>>;
// Request: POST https://example.com/api/query { "request": "abc" }
// Success: { "success": "ok" }
// Error: ignored (response body not parsed)
using JSONEndpointSuccessEmpty =
TestEndpoint<POST<JSONRequestBody>,
Response<JSONSuccessBody, JSONEmptyBody>>;
template <typename Request, typename Response>
// Request: POST https://example.com/api/query \x0A\x03abc
// Success: \x0A\x02ok
// Error: \x0A\x04nope
using ProtobufEndpointSuccessError =
TestEndpoint<POST<ProtobufRequestBody>,
Response<ProtobufSuccessBody, ProtobufErrorBody>>;
// Request: POST https://example.com/api/query \x0A\x03abc
// Success: ignored (response body not parsed)
// Error: \x0A\x04nope
using ProtobufEndpointEmptyError =
TestEndpoint<POST<ProtobufRequestBody>,
Response<ProtobufEmptyBody, ProtobufErrorBody>>;
// Request: POST https://example.com/api/query \x0A\x03abc
// Success: \x0A\x02ok
// Error: ignored (response body not parsed)
using ProtobufEndpointSuccessEmpty =
TestEndpoint<POST<ProtobufRequestBody>,
Response<ProtobufSuccessBody, ProtobufEmptyBody>>;
template <typename Response>
requires detail::IsResponse<detail::MaybeStripWithHeaders<Response>>
struct TestCase {
std::string test_name;
Request request;
int net_error;
std::optional<net::HttpStatusCode> status_code;
std::string body;
Response expected_response;
Response response;
};
template <typename Endpoint,
typename Request = typename Endpoint::Request,
typename Response = typename Endpoint::Response>
class ClientTest : public testing::TestWithParam<TestCase<Request, Response>> {
template <IsEndpoint Endpoint, typename Request, typename Response>
requires(std::same_as<detail::MaybeStripWithHeaders<Request>,
typename Endpoint::Request> &&
std::same_as<detail::MaybeStripWithHeaders<Response>,
typename Endpoint::Response>)
class ClientTest : public testing::TestWithParam<const TestCase<Response>*> {
public:
using TestCase = typename ClientTest::ParamType;
static constexpr auto kNameGenerator = [](const auto& info) {
return info.param.test_name;
return CHECK_DEREF(info.param).test_name;
};
static std::vector<TestCase> GenerateTestCases() {
auto make_request = [] {
Request request;
request.text = "request";
if constexpr (base::is_instantiation<Request, WithHeaders>) {
SetBearerToken(request, "12345");
}
return request;
};
auto make_response = [](int net_error,
std::optional<net::HttpStatusCode> status_code,
auto body) {
Response response;
response.net_error = net_error;
response.status_code = status_code;
response.body = std::move(body);
if constexpr (base::is_instantiation<Response, WithHeaders>) {
if (status_code) {
response.headers =
net::HttpResponseHeaders::Builder(
{1, 1},
absl::StrFormat("%d %s", *status_code,
net::GetHttpReasonPhrase(*status_code)))
.AddHeader("X-Test-Header", "test-value")
.Build();
}
}
return response;
};
auto make_success_body = [](bool non_empty_nullopt) {
using SuccessBody = typename Endpoint::Response::SuccessBody;
if constexpr (std::is_empty_v<SuccessBody>) {
return SuccessBody();
} else {
return non_empty_nullopt ? std::optional<SuccessBody>()
: SuccessBody("success");
}
};
auto make_error_body = [](bool non_empty_nullopt) {
using ErrorBody = typename Endpoint::Response::ErrorBody;
if constexpr (std::is_empty_v<ErrorBody>) {
return base::unexpected(ErrorBody());
} else {
return non_empty_nullopt ? std::optional<base::unexpected<ErrorBody>>()
: base::unexpected(ErrorBody("error"));
}
};
return {
TestCase{.test_name = "net_error",
.request = make_request(),
.net_error = net::ERR_FAILED,
.status_code = std::nullopt,
.body = "",
.expected_response = make_response(
net::ERR_FAILED, std::nullopt, std::nullopt)},
TestCase{.test_name = "no_response_headers",
.request = make_request(),
.net_error = net::OK,
.status_code = std::nullopt,
.body = "",
.expected_response =
make_response(net::OK, std::nullopt, std::nullopt)},
TestCase{.test_name = "success_empty_response",
.request = make_request(),
.net_error = net::OK,
.status_code = net::HTTP_OK,
.body = "",
.expected_response = make_response(net::OK, net::HTTP_OK,
make_success_body(true))},
TestCase{.test_name = "success_plain_text_response",
.request = make_request(),
.net_error = net::OK,
.status_code = net::HTTP_OK,
.body = "plain text",
.expected_response = make_response(net::OK, net::HTTP_OK,
make_success_body(true))},
TestCase{.test_name = "success_invalid_json_response",
.request = make_request(),
.net_error = net::OK,
.status_code = net::HTTP_OK,
.body = R"({"invalid": json})",
.expected_response = make_response(net::OK, net::HTTP_OK,
make_success_body(true))},
TestCase{.test_name = "success_valid_json_lacks_success_field",
.request = make_request(),
.net_error = net::OK,
.status_code = net::HTTP_OK,
.body = R"({"valid": "json"})",
.expected_response = make_response(net::OK, net::HTTP_OK,
make_success_body(true))},
TestCase{.test_name = "success_valid_json_has_success_field",
.request = make_request(),
.net_error = net::OK,
.status_code = net::HTTP_OK,
.body = R"({"success": "success"})",
.expected_response = make_response(net::OK, net::HTTP_OK,
make_success_body(false))},
TestCase{.test_name = "error_empty_response",
.request = make_request(),
.net_error = net::OK,
.status_code = net::HTTP_BAD_REQUEST,
.body = "",
.expected_response = make_response(
net::OK, net::HTTP_BAD_REQUEST, make_error_body(true))},
TestCase{.test_name = "error_plain_text_response",
.request = make_request(),
.net_error = net::OK,
.status_code = net::HTTP_BAD_REQUEST,
.body = "plain text",
.expected_response = make_response(
net::OK, net::HTTP_BAD_REQUEST, make_error_body(true))},
TestCase{.test_name = "error_invalid_json_response",
.request = make_request(),
.net_error = net::OK,
.status_code = net::HTTP_BAD_REQUEST,
.body = R"({"invalid": json})",
.expected_response = make_response(
net::OK, net::HTTP_BAD_REQUEST, make_error_body(true))},
TestCase{.test_name = "error_valid_json_lacks_error_field",
.request = make_request(),
.net_error = net::OK,
.status_code = net::HTTP_BAD_REQUEST,
.body = R"({"valid": "json"})",
.expected_response = make_response(
net::OK, net::HTTP_BAD_REQUEST, make_error_body(true))},
TestCase{.test_name = "error_valid_json_has_error_field",
.request = make_request(),
.net_error = net::OK,
.status_code = net::HTTP_BAD_REQUEST,
.body = R"({"error": "error"})",
.expected_response = make_response(
net::OK, net::HTTP_BAD_REQUEST, make_error_body(false))}};
static auto GenerateTestCases() {
return testing::Values(NetError(), NoResponseHeaders(),
SuccessEmptyResponse(), SuccessMalformedResponse(),
SuccessUnexpectedResponse(), SuccessValidResponse(),
ErrorEmptyResponse(), ErrorMalformedResponse(),
ErrorUnexpectedResponse(), ErrorValidResponse());
}
protected:
void Interceptor(const network::ResourceRequest& resource_request) {
const auto& test_case = this->GetParam();
EXPECT_EQ(resource_request.method, Request::Method());
EXPECT_EQ(resource_request.url, Endpoint::URL());
const auto& request_body = CHECK_DEREF(resource_request.request_body);
const auto& elements = CHECK_DEREF(request_body.elements());
CHECK_EQ(elements.size(), 1u);
const auto& element = elements.front();
EXPECT_EQ(element.type(), network::DataElement::Tag::kBytes);
const auto body = base::JSONReader::Read(
element.As<network::DataElementBytes>().AsStringPiece(),
base::JSON_PARSE_RFC);
CHECK(body && body->is_dict());
const auto* request = body->GetDict().FindString(kRequestKey);
EXPECT_NE(request, nullptr);
EXPECT_EQ(*request, test_case.request.text);
EXPECT_EQ(resource_request.headers.GetHeader(
net::HttpRequestHeaders::kContentType),
"application/json");
if constexpr (base::is_instantiation<Request, WithHeaders>) {
EXPECT_EQ(resource_request.headers.GetHeader(
net::HttpRequestHeaders::kAuthorization),
"Bearer 12345");
}
auto head = test_case.status_code
? network::CreateURLResponseHead(*test_case.status_code)
: network::mojom::URLResponseHead::New();
if constexpr (base::is_instantiation<Response, WithHeaders>) {
head->headers = test_case.expected_response.headers;
}
test_url_loader_factory_.AddResponse(
resource_request.url, std::move(head), test_case.body,
network::URLLoaderCompletionStatus(test_case.net_error));
}
void RunTestCase() {
const auto& test_case = this->GetParam();
const auto& test_case = CHECK_DEREF(this->GetParam());
test_url_loader_factory_.SetInterceptor(
base::BindRepeating(&ClientTest::Interceptor, base::Unretained(this)));
base::test::TestFuture<Response> future;
Client<Endpoint>::Send(test_url_loader_factory_.GetSafeWeakWrapper(),
test_case.request, future.GetCallback());
EXPECT_EQ(future.Take(), test_case.expected_response);
MakeRequest(), future.GetCallback());
EXPECT_EQ(future.Take(), test_case.response);
}
private:
using TestCase = TestCase<Response>;
enum class BodyCase { kMalformed, kUnexpected, kValid };
static const TestCase* NetError() {
static const base::NoDestructor<TestCase> kNetError({
.test_name = "net_error",
.net_error = net::ERR_FAILED,
.status_code = std::nullopt,
.body = "",
.response = MakeResponse(net::ERR_FAILED, std::nullopt, std::nullopt),
});
return kNetError.get();
}
static const TestCase* NoResponseHeaders() {
static const base::NoDestructor<TestCase> kNoResponseHeaders({
.test_name = "no_response_headers",
.net_error = net::OK,
.status_code = std::nullopt,
.body = "",
.response = MakeResponse(net::OK, std::nullopt, std::nullopt),
});
return kNoResponseHeaders.get();
}
static const TestCase* SuccessEmptyResponse() {
static const base::NoDestructor<TestCase> kSuccessEmptyResponse({
.test_name = "success_empty_response",
.net_error = net::OK,
.status_code = net::HTTP_OK,
.body = "",
.response = MakeResponse(net::OK, net::HTTP_OK, std::nullopt),
});
return kSuccessEmptyResponse.get();
}
static const TestCase* SuccessMalformedResponse() {
static const base::NoDestructor<TestCase> kSuccessMalformedResponse({
.test_name = "success_malformed_response",
.net_error = net::OK,
.status_code = net::HTTP_OK,
.body = MakeBody<typename Endpoint::Response::SuccessBody>(
BodyCase::kMalformed),
.response = MakeResponse(net::OK, net::HTTP_OK, BodyCase::kMalformed),
});
return kSuccessMalformedResponse.get();
}
static const TestCase* SuccessUnexpectedResponse() {
static const base::NoDestructor<TestCase> kSuccessUnexpectedResponse({
.test_name = "success_unexpected_response",
.net_error = net::OK,
.status_code = net::HTTP_OK,
.body = MakeBody<typename Endpoint::Response::SuccessBody>(
BodyCase::kUnexpected),
.response = MakeResponse(net::OK, net::HTTP_OK, BodyCase::kUnexpected),
});
return kSuccessUnexpectedResponse.get();
}
static const TestCase* SuccessValidResponse() {
static const base::NoDestructor<TestCase> kSuccessValidResponse({
.test_name = "success_valid_response",
.net_error = net::OK,
.status_code = net::HTTP_OK,
.body = MakeBody<typename Endpoint::Response::SuccessBody>(
BodyCase::kValid),
.response = MakeResponse(net::OK, net::HTTP_OK, BodyCase::kValid),
});
return kSuccessValidResponse.get();
}
static const TestCase* ErrorEmptyResponse() {
static const base::NoDestructor<TestCase> kErrorEmptyResponse({
.test_name = "error_empty_response",
.net_error = net::OK,
.status_code = net::HTTP_BAD_REQUEST,
.body = "",
.response = MakeResponse(net::OK, net::HTTP_BAD_REQUEST, std::nullopt),
});
return kErrorEmptyResponse.get();
}
static const TestCase* ErrorMalformedResponse() {
static const base::NoDestructor<TestCase> kErrorMalformedResponse({
.test_name = "error_malformed_response",
.net_error = net::OK,
.status_code = net::HTTP_BAD_REQUEST,
.body = MakeBody<typename Endpoint::Response::ErrorBody>(
BodyCase::kMalformed),
.response =
MakeResponse(net::OK, net::HTTP_BAD_REQUEST, BodyCase::kMalformed),
});
return kErrorMalformedResponse.get();
}
static const TestCase* ErrorUnexpectedResponse() {
static const base::NoDestructor<TestCase> kErrorUnexpectedResponse({
.test_name = "error_unexpected_response",
.net_error = net::OK,
.status_code = net::HTTP_BAD_REQUEST,
.body = MakeBody<typename Endpoint::Response::ErrorBody>(
BodyCase::kUnexpected),
.response =
MakeResponse(net::OK, net::HTTP_BAD_REQUEST, BodyCase::kUnexpected),
});
return kErrorUnexpectedResponse.get();
}
static const TestCase* ErrorValidResponse() {
static const base::NoDestructor<TestCase> kErrorValidResponse({
.test_name = "error_valid_response",
.net_error = net::OK,
.status_code = net::HTTP_BAD_REQUEST,
.body =
MakeBody<typename Endpoint::Response::ErrorBody>(BodyCase::kValid),
.response =
MakeResponse(net::OK, net::HTTP_BAD_REQUEST, BodyCase::kValid),
});
return kErrorValidResponse.get();
}
template <std::same_as<JSONRequestBody> RequestBody>
static RequestBody MakeRequestBody() {
RequestBody request_body;
request_body.request = "request";
return request_body;
}
template <std::same_as<ProtobufRequestBody> RequestBody>
static RequestBody MakeRequestBody() {
RequestBody request_body;
request_body.set_request("request");
return request_body;
}
static Request MakeRequest() {
Request request;
request.body = MakeRequestBody<typename Endpoint::Request::Body>();
if constexpr (base::is_instantiation<Request, WithHeaders>) {
SetBearerToken(request, "12345");
}
return request;
}
template <detail::IsJSONResponseBody ResponseBody>
static auto MakeResponseBody(std::optional<BodyCase> body_case) {
std::optional<ResponseBody> response_body;
if constexpr (std::same_as<ResponseBody, JSONEmptyBody>) {
response_body.emplace();
} else if (body_case) {
switch (*body_case) {
case BodyCase::kMalformed:
break;
case BodyCase::kUnexpected:
break;
case BodyCase::kValid:
if constexpr (std::same_as<ResponseBody, JSONSuccessBody>) {
response_body.emplace().success = "success";
} else {
static_assert(std::same_as<ResponseBody, JSONErrorBody>);
response_body.emplace().error = "error";
}
}
}
return response_body;
}
template <detail::IsProtobufResponseBody ResponseBody>
static auto MakeResponseBody(std::optional<BodyCase> body_case) {
std::optional<ResponseBody> response_body;
if constexpr (std::same_as<ResponseBody, ProtobufEmptyBody>) {
response_body.emplace();
} else if (body_case) {
switch (*body_case) {
case BodyCase::kMalformed:
break;
case BodyCase::kUnexpected:
// Unexpected payloads are well-formed protobuf wire formats that do
// not match the expected ResponseBody schema (e.g. contain only
// unknown fields). Parsing succeeds, but all fields are ignored,
// yielding a default-constructed ResponseBody. See
// Response<>::Deserialize<>() for details on protobuf parsing
// semantics.
response_body.emplace();
break;
case BodyCase::kValid:
if constexpr (std::same_as<ResponseBody, ProtobufSuccessBody>) {
response_body.emplace().set_success("success");
} else {
static_assert(std::same_as<ResponseBody, ProtobufErrorBody>);
response_body.emplace().set_error("error");
}
}
}
return response_body;
}
static Response MakeResponse(int net_error,
std::optional<net::HttpStatusCode> status_code,
std::optional<BodyCase> body_case) {
Response response;
response.net_error = net_error;
response.status_code = status_code;
if (net_error == net::OK && status_code) {
if (network::IsSuccessfulStatus(*status_code)) { // 2xx
response.body =
MakeResponseBody<typename Endpoint::Response::SuccessBody>(
body_case);
} else { // non-2xx
response.body =
MakeResponseBody<typename Endpoint::Response::ErrorBody>(body_case)
.transform([](auto error_body) {
return base::unexpected(std::move(error_body));
});
}
if constexpr (base::is_instantiation<Response, WithHeaders>) {
response.headers =
net::HttpResponseHeaders::Builder(
{1, 1}, absl::StrFormat("%d %s", *status_code,
net::GetHttpReasonPhrase(*status_code)))
.AddHeader("X-Test-Header", "test-value")
.Build();
}
}
return response;
}
template <detail::IsJSONResponseBody ResponseBody>
static std::string MakeBody(BodyCase body_case) {
switch (body_case) {
case BodyCase::kMalformed:
return R"({"invalid": json})";
case BodyCase::kUnexpected:
return R"({"valid": "json"})";
case BodyCase::kValid: {
ResponseBody response_body;
if constexpr (!std::same_as<ResponseBody, JSONEmptyBody>) {
if constexpr (std::same_as<ResponseBody, JSONSuccessBody>) {
response_body.success = "success";
} else {
static_assert(std::same_as<ResponseBody, JSONErrorBody>);
response_body.error = "error";
}
}
return CHECK_DEREF(base::WriteJson(response_body.ToValue()));
}
}
}
template <detail::IsProtobufResponseBody ResponseBody>
static std::string MakeBody(BodyCase body_case) {
switch (body_case) {
case BodyCase::kMalformed:
// Field 1, length=10, but only 8 bytes follow ("tooshort").
return "\x0A\x0Atooshort";
case BodyCase::kUnexpected:
// Field 2, length=10, value="unexpected".
// Well-formed protobuf payload containing only unknown fields.
// ParseFromString() succeeds, yielding a default-constructed
// ResponseBody. See Response<>::Deserialize<>() for details on protobuf
// parsing semantics.
return "\x12\x0Aunexpected";
case BodyCase::kValid: {
ResponseBody response_body;
if constexpr (!std::same_as<ResponseBody, ProtobufEmptyBody>) {
if constexpr (std::same_as<ResponseBody, ProtobufSuccessBody>) {
response_body.set_success("success");
} else {
static_assert(std::same_as<ResponseBody, ProtobufErrorBody>);
response_body.set_error("error");
}
}
return response_body.SerializeAsString();
}
}
}
template <std::same_as<JSONRequestBody> RequestBody>
RequestBody RequestBodyFrom(std::string_view bytes) {
return CHECK_DEREF(RequestBody::FromValue(
CHECK_DEREF(base::JSONReader::Read(bytes, base::JSON_PARSE_RFC))));
}
template <std::same_as<ProtobufRequestBody> RequestBody>
RequestBody RequestBodyFrom(std::string_view bytes) {
RequestBody body;
CHECK(body.ParseFromString(bytes));
return body;
}
void Interceptor(const network::ResourceRequest& resource_request) {
EXPECT_EQ(resource_request.url, Endpoint::URL());
EXPECT_EQ(resource_request.method, Request::Method());
EXPECT_EQ(resource_request.headers.GetHeader(
net::HttpRequestHeaders::kContentType),
Request::ContentType());
const auto& request_body = CHECK_DEREF(resource_request.request_body);
const auto& elements = CHECK_DEREF(request_body.elements());
CHECK_EQ(elements.size(), 1u);
const auto& element = elements.front();
EXPECT_EQ(element.type(), network::DataElement::Tag::kBytes);
const auto bytes = element.As<network::DataElementBytes>().AsStringPiece();
EXPECT_EQ(RequestBodyFrom<typename Endpoint::Request::Body>(bytes),
MakeRequest().body);
if constexpr (base::is_instantiation<Request, WithHeaders>) {
EXPECT_EQ(resource_request.headers.GetHeader(
net::HttpRequestHeaders::kAuthorization),
"Bearer 12345");
}
const auto& test_case = CHECK_DEREF(this->GetParam());
auto head = test_case.status_code
? network::CreateURLResponseHead(*test_case.status_code)
: network::mojom::URLResponseHead::New();
if constexpr (base::is_instantiation<Response, WithHeaders>) {
head->headers = test_case.response.headers;
}
test_url_loader_factory_.AddResponse(
resource_request.url, std::move(head), test_case.body,
network::URLLoaderCompletionStatus(test_case.net_error));
}
base::test::TaskEnvironment task_environment_;
network::TestURLLoaderFactory test_url_loader_factory_;
};
using ClientTestPlainRequest = ClientTest<TestEndpoint>;
using ClientTestJSONPlainRequest =
ClientTest<JSONEndpointSuccessError,
JSONEndpointSuccessError::Request,
JSONEndpointSuccessError::Response>;
using ClientTestRequestWithHeaders =
ClientTest<TestEndpoint, WithHeaders<TestEndpoint::Request>>;
using ClientTestJSONRequestWithHeaders =
ClientTest<JSONEndpointSuccessError,
WithHeaders<JSONEndpointSuccessError::Request>,
JSONEndpointSuccessError::Response>;
using ClientTestResponseWithHeaders =
ClientTest<TestEndpoint,
TestEndpoint::Request,
WithHeaders<TestEndpoint::Response>>;
using ClientTestJSONResponseWithHeaders =
ClientTest<JSONEndpointSuccessError,
JSONEndpointSuccessError::Request,
WithHeaders<JSONEndpointSuccessError::Response>>;
using ClientTestEmptySuccessBody = ClientTest<TestEndpointWithEmptySuccessBody>;
using ClientTestJSONEmptySuccessBody =
ClientTest<JSONEndpointEmptyError,
JSONEndpointEmptyError::Request,
JSONEndpointEmptyError::Response>;
using ClientTestEmptyErrorBody = ClientTest<TestEndpointWithEmptyErrorBody>;
using ClientTestJSONEmptyErrorBody =
ClientTest<JSONEndpointSuccessEmpty,
JSONEndpointSuccessEmpty::Request,
JSONEndpointSuccessEmpty::Response>;
using ClientTestProtobufPlainRequest =
ClientTest<ProtobufEndpointSuccessError,
ProtobufEndpointSuccessError::Request,
ProtobufEndpointSuccessError::Response>;
using ClientTestProtobufRequestWithHeaders =
ClientTest<ProtobufEndpointSuccessError,
WithHeaders<ProtobufEndpointSuccessError::Request>,
ProtobufEndpointSuccessError::Response>;
using ClientTestProtobufResponseWithHeaders =
ClientTest<ProtobufEndpointSuccessError,
ProtobufEndpointSuccessError::Request,
WithHeaders<ProtobufEndpointSuccessError::Response>>;
using ClientTestProtobufEmptySuccessBody =
ClientTest<ProtobufEndpointEmptyError,
ProtobufEndpointEmptyError::Request,
ProtobufEndpointEmptyError::Response>;
using ClientTestProtobufEmptyErrorBody =
ClientTest<ProtobufEndpointSuccessEmpty,
ProtobufEndpointSuccessEmpty::Request,
ProtobufEndpointSuccessEmpty::Response>;
enum class CancelRequestOn { kSameSequence, kDifferentSequence };
@@ -391,62 +607,105 @@ class ClientTestCancelableRequest
} // namespace
TEST_P(ClientTestPlainRequest, Send) {
TEST_P(ClientTestJSONPlainRequest, Send) {
RunTestCase();
}
INSTANTIATE_TEST_SUITE_P(JSONPlainRequest,
ClientTestJSONPlainRequest,
ClientTestJSONPlainRequest::GenerateTestCases(),
ClientTestJSONPlainRequest::kNameGenerator);
TEST_P(ClientTestJSONRequestWithHeaders, Send) {
RunTestCase();
}
INSTANTIATE_TEST_SUITE_P(JSONRequestWithHeaders,
ClientTestJSONRequestWithHeaders,
ClientTestJSONRequestWithHeaders::GenerateTestCases(),
ClientTestJSONRequestWithHeaders::kNameGenerator);
TEST_P(ClientTestJSONResponseWithHeaders, Send) {
RunTestCase();
}
INSTANTIATE_TEST_SUITE_P(JSONResponseWithHeaders,
ClientTestJSONResponseWithHeaders,
ClientTestJSONResponseWithHeaders::GenerateTestCases(),
ClientTestJSONResponseWithHeaders::kNameGenerator);
TEST_P(ClientTestJSONEmptySuccessBody, Send) {
RunTestCase();
}
INSTANTIATE_TEST_SUITE_P(JSONEmptySuccessBody,
ClientTestJSONEmptySuccessBody,
ClientTestJSONEmptySuccessBody::GenerateTestCases(),
ClientTestJSONEmptySuccessBody::kNameGenerator);
TEST_P(ClientTestJSONEmptyErrorBody, Send) {
RunTestCase();
}
INSTANTIATE_TEST_SUITE_P(JSONEmptyErrorBody,
ClientTestJSONEmptyErrorBody,
ClientTestJSONEmptyErrorBody::GenerateTestCases(),
ClientTestJSONEmptyErrorBody::kNameGenerator);
TEST_P(ClientTestProtobufPlainRequest, Send) {
RunTestCase();
}
INSTANTIATE_TEST_SUITE_P(ProtobufPlainRequest,
ClientTestProtobufPlainRequest,
ClientTestProtobufPlainRequest::GenerateTestCases(),
ClientTestProtobufPlainRequest::kNameGenerator);
TEST_P(ClientTestProtobufRequestWithHeaders, Send) {
RunTestCase();
}
INSTANTIATE_TEST_SUITE_P(
PlainRequest,
ClientTestPlainRequest,
testing::ValuesIn(ClientTestPlainRequest::GenerateTestCases()),
ClientTestPlainRequest::kNameGenerator);
ProtobufRequestWithHeaders,
ClientTestProtobufRequestWithHeaders,
ClientTestProtobufRequestWithHeaders::GenerateTestCases(),
ClientTestProtobufRequestWithHeaders::kNameGenerator);
TEST_P(ClientTestRequestWithHeaders, Send) {
TEST_P(ClientTestProtobufResponseWithHeaders, Send) {
RunTestCase();
}
INSTANTIATE_TEST_SUITE_P(
RequestWithHeaders,
ClientTestRequestWithHeaders,
testing::ValuesIn(ClientTestRequestWithHeaders::GenerateTestCases()),
ClientTestRequestWithHeaders::kNameGenerator);
ProtobufResponseWithHeaders,
ClientTestProtobufResponseWithHeaders,
ClientTestProtobufResponseWithHeaders::GenerateTestCases(),
ClientTestProtobufResponseWithHeaders::kNameGenerator);
TEST_P(ClientTestResponseWithHeaders, Send) {
TEST_P(ClientTestProtobufEmptySuccessBody, Send) {
RunTestCase();
}
INSTANTIATE_TEST_SUITE_P(
ResponseWithHeaders,
ClientTestResponseWithHeaders,
testing::ValuesIn(ClientTestResponseWithHeaders::GenerateTestCases()),
ClientTestResponseWithHeaders::kNameGenerator);
ProtobufEmptySuccessBody,
ClientTestProtobufEmptySuccessBody,
ClientTestProtobufEmptySuccessBody::GenerateTestCases(),
ClientTestProtobufEmptySuccessBody::kNameGenerator);
TEST_P(ClientTestEmptySuccessBody, Send) {
TEST_P(ClientTestProtobufEmptyErrorBody, Send) {
RunTestCase();
}
INSTANTIATE_TEST_SUITE_P(
EmptySuccessBody,
ClientTestEmptySuccessBody,
testing::ValuesIn(ClientTestEmptySuccessBody::GenerateTestCases()),
ClientTestEmptySuccessBody::kNameGenerator);
TEST_P(ClientTestEmptyErrorBody, Send) {
RunTestCase();
}
INSTANTIATE_TEST_SUITE_P(
EmptyErrorBody,
ClientTestEmptyErrorBody,
testing::ValuesIn(ClientTestEmptyErrorBody::GenerateTestCases()),
ClientTestEmptyErrorBody::kNameGenerator);
INSTANTIATE_TEST_SUITE_P(ProtobufEmptyErrorBody,
ClientTestProtobufEmptyErrorBody,
ClientTestProtobufEmptyErrorBody::GenerateTestCases(),
ClientTestProtobufEmptyErrorBody::kNameGenerator);
TEST_P(ClientTestCancelableRequest, Cancel) {
base::test::TestFuture<TestEndpoint::Response> future;
base::test::TestFuture<JSONEndpointSuccessError::Response> future;
RequestHandle request_handle =
Client<TestEndpoint>::Send<RequestCancelability::kCancelable>(
Client<JSONEndpointSuccessError>::Send<RequestCancelability::kCancelable>(
test_url_loader_factory_.GetSafeWeakWrapper(),
TestEndpoint::Request{{"cancel me"}}, future.GetCallback());
JSONEndpointSuccessError::Request(), future.GetCallback());
auto weak_simple_url_loader =
CHECK_DEREF(static_cast<network::SimpleURLLoader*>(request_handle.get()))
@@ -1,36 +0,0 @@
/* Copyright (c) 2025 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#ifndef BRAVE_COMPONENTS_BRAVE_ACCOUNT_ENDPOINT_CLIENT_CONCEPT_TEST_H_
#define BRAVE_COMPONENTS_BRAVE_ACCOUNT_ENDPOINT_CLIENT_CONCEPT_TEST_H_
#include <string>
#include <tuple>
#include "testing/gtest/include/gtest/gtest.h"
namespace brave_account::endpoint_client {
struct ConceptTest {
template <typename T>
struct Fixture : testing::Test {
using TestType = std::tuple_element_t<0, T>;
using ExpectedResult = std::tuple_element_t<1, T>;
};
struct NameGenerator {
template <typename T>
static std::string GetName(int) {
using TestType = std::tuple_element_t<0, T>;
using ExpectedResult = std::tuple_element_t<1, T>;
return std::string(TestType::kName) + "_does" +
(ExpectedResult::value ? "" : "_not");
}
};
};
} // namespace brave_account::endpoint_client
#endif // BRAVE_COMPONENTS_BRAVE_ACCOUNT_ENDPOINT_CLIENT_CONCEPT_TEST_H_
@@ -16,27 +16,32 @@ namespace brave_account::endpoint_client {
namespace detail {
// Concept that checks whether `T` defines a static, accessible member
// function `URL()` such that:
// - `T::URL()` is a valid expression,
// and that call yields `GURL`
//
// In short: models any type with a proper static `URL()` function
// whose result is a `GURL`.
template <typename T>
concept URL = requires {
{ T::URL() } -> std::same_as<GURL>;
// An endpoint defines Request and Response type aliases and a static URL().
template <typename Endpoint>
concept IsEndpointLike = requires {
typename Endpoint::Request;
typename Endpoint::Response;
{ Endpoint::URL() } -> std::same_as<GURL>;
};
// A JSON endpoint has a JSON request and response type.
template <typename Endpoint>
concept IsJSONEndpoint =
IsEndpointLike<Endpoint> && IsJSONRequest<typename Endpoint::Request> &&
IsJSONResponse<typename Endpoint::Response>;
// A Protobuf endpoint has a Protobuf request and response type.
template <typename Endpoint>
concept IsProtobufEndpoint =
IsEndpointLike<Endpoint> && IsProtobufRequest<typename Endpoint::Request> &&
IsProtobufResponse<typename Endpoint::Response>;
} // namespace detail
template <typename T>
// An endpoint is either a JSON endpoint or a Protobuf endpoint.
template <typename Endpoint>
concept IsEndpoint =
requires {
typename T::Request;
typename T::Response;
} && detail::IsRequest<typename T::Request> &&
detail::IsResponse<typename T::Response> && detail::URL<T>;
detail::IsJSONEndpoint<Endpoint> || detail::IsProtobufEndpoint<Endpoint>;
} // namespace brave_account::endpoint_client
@@ -5,13 +5,11 @@
#include "brave/components/brave_account/endpoint_client/is_endpoint.h"
#include <optional>
#include <string>
#include <tuple>
#include <type_traits>
#include "base/values.h"
#include "brave/components/brave_account/endpoint_client/concept_test.h"
#include "base/strings/strcat.h"
#include "brave/components/brave_account/endpoint_client/json_test_endpoint_bodies.h"
#include "brave/components/brave_account/endpoint_client/protobuf_test_endpoint_bodies.pb.h"
#include "brave/components/brave_account/endpoint_client/request_types.h"
#include "brave/components/brave_account/endpoint_client/response.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -22,145 +20,228 @@ namespace brave_account::endpoint_client {
namespace {
struct InvalidRequest {};
struct ValidRequestBody {
base::DictValue ToValue() const;
};
using ValidRequest = POST<ValidRequestBody>;
struct InvalidResponse {};
struct ValidResponseBody {
static std::optional<ValidResponseBody> FromValue(const base::Value&);
using ValidJSONRequest = POST<JSONRequestBody>;
using ValidJSONResponse = Response<JSONSuccessBody, JSONErrorBody>;
using ValidProtobufRequest = POST<ProtobufRequestBody>;
using ValidProtobufResponse = Response<ProtobufSuccessBody, ProtobufErrorBody>;
enum class TypeAliasState {
kAbsent,
kPresentInvalidJSON,
kPresentValidJSON,
kPresentInvalidProtobuf,
kPresentValidProtobuf
};
using ValidResponse = Response<ValidResponseBody, ValidResponseBody>;
enum class FunctionState { kAbsent, kPresentInvalid, kPresentValid };
struct EndpointInvalidRequest {
static constexpr char kName[] = "EndpointInvalidRequest";
using Request = InvalidRequest;
using Response = ValidResponse;
static GURL URL();
};
struct EndpointInvalidResponse {
static constexpr char kName[] = "EndpointInvalidResponse";
using Request = ValidRequest;
using Response = InvalidResponse;
static GURL URL();
};
struct EndpointInvalidURL {
static constexpr char kName[] = "EndpointInvalidURL";
using Request = ValidRequest;
using Response = ValidResponse;
static std::string URL();
};
template <typename T>
using IsEndpointConceptTest = ConceptTest::Fixture<T>;
// Hand-crafted invalid endpoints.
using EndpointTestTypes =
testing::Types<std::tuple<EndpointInvalidRequest, std::false_type>,
std::tuple<EndpointInvalidResponse, std::false_type>,
std::tuple<EndpointInvalidURL, std::false_type>>;
} // namespace
TYPED_TEST_SUITE(IsEndpointConceptTest,
EndpointTestTypes,
ConceptTest::NameGenerator);
TYPED_TEST(IsEndpointConceptTest, SatisfyConcept) {
using TestType = typename TestFixture::TestType;
using ExpectedResult = typename TestFixture::ExpectedResult;
EXPECT_EQ(IsEndpoint<TestType>, ExpectedResult::value);
}
namespace {
template <bool>
template <TypeAliasState>
struct MaybeRequest {};
template <>
struct MaybeRequest<true> {
using Request = ValidRequest;
struct MaybeRequest<TypeAliasState::kPresentInvalidJSON> {
using Request = InvalidRequest;
};
template <bool>
template <>
struct MaybeRequest<TypeAliasState::kPresentValidJSON> {
using Request = ValidJSONRequest;
};
template <>
struct MaybeRequest<TypeAliasState::kPresentInvalidProtobuf> {
using Request = InvalidRequest;
};
template <>
struct MaybeRequest<TypeAliasState::kPresentValidProtobuf> {
using Request = ValidProtobufRequest;
};
template <TypeAliasState>
struct MaybeResponse {};
template <>
struct MaybeResponse<true> {
using Response = ValidResponse;
struct MaybeResponse<TypeAliasState::kPresentInvalidJSON> {
using Response = InvalidResponse;
};
template <bool>
template <>
struct MaybeResponse<TypeAliasState::kPresentValidJSON> {
using Response = ValidJSONResponse;
};
template <>
struct MaybeResponse<TypeAliasState::kPresentInvalidProtobuf> {
using Response = InvalidResponse;
};
template <>
struct MaybeResponse<TypeAliasState::kPresentValidProtobuf> {
using Response = ValidProtobufResponse;
};
template <FunctionState>
struct MaybeURL {};
template <>
struct MaybeURL<true> {
struct MaybeURL<FunctionState::kPresentInvalid> {
static std::string URL();
};
template <>
struct MaybeURL<FunctionState::kPresentValid> {
static GURL URL();
};
template <bool HasRequest, bool HasResponse, bool HasURL>
struct EndpointCase : MaybeRequest<HasRequest>,
MaybeResponse<HasResponse>,
MaybeURL<HasURL> {
static constexpr bool kHasRequest = HasRequest;
static constexpr bool kHasResponse = HasResponse;
static constexpr bool kHasURL = HasURL;
template <TypeAliasState RequestState,
TypeAliasState ResponseState,
FunctionState URLState>
struct IsEndpointTestCase : MaybeRequest<RequestState>,
MaybeResponse<ResponseState>,
MaybeURL<URLState> {
static constexpr TypeAliasState kRequestState = RequestState;
static constexpr TypeAliasState kResponseState = ResponseState;
static constexpr FunctionState kURLState = URLState;
static constexpr bool kSatisfiesConcept =
kHasRequest && kHasResponse && kHasURL;
((RequestState == TypeAliasState::kPresentValidJSON &&
ResponseState == TypeAliasState::kPresentValidJSON) ||
(RequestState == TypeAliasState::kPresentValidProtobuf &&
ResponseState == TypeAliasState::kPresentValidProtobuf)) &&
URLState == FunctionState::kPresentValid;
};
template <typename>
struct IsEndpointConceptMatrixTest : testing::Test {};
// clang-format off
// Test cases: 5 request states x 5 response states x 3 URL states = 75 tests
using IsEndpointTestCases = testing::Types<
IsEndpointTestCase<TypeAliasState::kAbsent, TypeAliasState::kAbsent, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kAbsent, TypeAliasState::kAbsent, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kAbsent, TypeAliasState::kAbsent, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kAbsent, TypeAliasState::kPresentInvalidJSON, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kAbsent, TypeAliasState::kPresentInvalidJSON, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kAbsent, TypeAliasState::kPresentInvalidJSON, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kAbsent, TypeAliasState::kPresentValidJSON, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kAbsent, TypeAliasState::kPresentValidJSON, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kAbsent, TypeAliasState::kPresentValidJSON, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kAbsent, TypeAliasState::kPresentInvalidProtobuf, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kAbsent, TypeAliasState::kPresentInvalidProtobuf, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kAbsent, TypeAliasState::kPresentInvalidProtobuf, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kAbsent, TypeAliasState::kPresentValidProtobuf, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kAbsent, TypeAliasState::kPresentValidProtobuf, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kAbsent, TypeAliasState::kPresentValidProtobuf, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidJSON, TypeAliasState::kAbsent, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidJSON, TypeAliasState::kAbsent, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidJSON, TypeAliasState::kAbsent, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidJSON, TypeAliasState::kPresentInvalidJSON, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidJSON, TypeAliasState::kPresentInvalidJSON, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidJSON, TypeAliasState::kPresentInvalidJSON, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidJSON, TypeAliasState::kPresentValidJSON, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidJSON, TypeAliasState::kPresentValidJSON, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidJSON, TypeAliasState::kPresentValidJSON, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidJSON, TypeAliasState::kPresentInvalidProtobuf, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidJSON, TypeAliasState::kPresentInvalidProtobuf, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidJSON, TypeAliasState::kPresentInvalidProtobuf, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidJSON, TypeAliasState::kPresentValidProtobuf, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidJSON, TypeAliasState::kPresentValidProtobuf, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidJSON, TypeAliasState::kPresentValidProtobuf, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentValidJSON, TypeAliasState::kAbsent, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentValidJSON, TypeAliasState::kAbsent, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentValidJSON, TypeAliasState::kAbsent, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentValidJSON, TypeAliasState::kPresentInvalidJSON, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentValidJSON, TypeAliasState::kPresentInvalidJSON, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentValidJSON, TypeAliasState::kPresentInvalidJSON, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentValidJSON, TypeAliasState::kPresentValidJSON, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentValidJSON, TypeAliasState::kPresentValidJSON, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentValidJSON, TypeAliasState::kPresentValidJSON, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentValidJSON, TypeAliasState::kPresentInvalidProtobuf, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentValidJSON, TypeAliasState::kPresentInvalidProtobuf, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentValidJSON, TypeAliasState::kPresentInvalidProtobuf, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentValidJSON, TypeAliasState::kPresentValidProtobuf, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentValidJSON, TypeAliasState::kPresentValidProtobuf, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentValidJSON, TypeAliasState::kPresentValidProtobuf, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidProtobuf, TypeAliasState::kAbsent, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidProtobuf, TypeAliasState::kAbsent, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidProtobuf, TypeAliasState::kAbsent, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidProtobuf, TypeAliasState::kPresentInvalidJSON, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidProtobuf, TypeAliasState::kPresentInvalidJSON, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidProtobuf, TypeAliasState::kPresentInvalidJSON, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidProtobuf, TypeAliasState::kPresentValidJSON, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidProtobuf, TypeAliasState::kPresentValidJSON, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidProtobuf, TypeAliasState::kPresentValidJSON, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidProtobuf, TypeAliasState::kPresentInvalidProtobuf, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidProtobuf, TypeAliasState::kPresentInvalidProtobuf, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidProtobuf, TypeAliasState::kPresentInvalidProtobuf, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidProtobuf, TypeAliasState::kPresentValidProtobuf, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidProtobuf, TypeAliasState::kPresentValidProtobuf, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentInvalidProtobuf, TypeAliasState::kPresentValidProtobuf, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentValidProtobuf, TypeAliasState::kAbsent, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentValidProtobuf, TypeAliasState::kAbsent, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentValidProtobuf, TypeAliasState::kAbsent, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentValidProtobuf, TypeAliasState::kPresentInvalidJSON, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentValidProtobuf, TypeAliasState::kPresentInvalidJSON, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentValidProtobuf, TypeAliasState::kPresentInvalidJSON, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentValidProtobuf, TypeAliasState::kPresentValidJSON, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentValidProtobuf, TypeAliasState::kPresentValidJSON, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentValidProtobuf, TypeAliasState::kPresentValidJSON, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentValidProtobuf, TypeAliasState::kPresentInvalidProtobuf, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentValidProtobuf, TypeAliasState::kPresentInvalidProtobuf, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentValidProtobuf, TypeAliasState::kPresentInvalidProtobuf, FunctionState::kPresentValid>,
IsEndpointTestCase<TypeAliasState::kPresentValidProtobuf, TypeAliasState::kPresentValidProtobuf, FunctionState::kAbsent>,
IsEndpointTestCase<TypeAliasState::kPresentValidProtobuf, TypeAliasState::kPresentValidProtobuf, FunctionState::kPresentInvalid>,
IsEndpointTestCase<TypeAliasState::kPresentValidProtobuf, TypeAliasState::kPresentValidProtobuf, FunctionState::kPresentValid>
>;
// clang-format on
// Exhaustive presence/absence matrix.
using EndpointMatrix = testing::Types<EndpointCase<false, false, false>,
EndpointCase<false, false, true>,
EndpointCase<false, true, false>,
EndpointCase<false, true, true>,
EndpointCase<true, false, false>,
EndpointCase<true, false, true>,
EndpointCase<true, true, false>,
EndpointCase<true, true, true>>;
struct EndpointName {
template <typename T>
struct IsEndpointTestCaseName {
template <typename IsEndpointTestCase>
static std::string GetName(int) {
auto name =
[](auto... bools) {
std::string bits;
((bits += (bools ? "1" : "0")), ...);
return bits;
}(T::kHasRequest, T::kHasResponse, T::kHasURL) +
"_Endpoint";
auto type_alias_state_to_string = [](TypeAliasState state) {
switch (state) {
case TypeAliasState::kAbsent:
return "Absent";
case TypeAliasState::kPresentInvalidJSON:
return "InvalidJSON";
case TypeAliasState::kPresentValidJSON:
return "ValidJSON";
case TypeAliasState::kPresentInvalidProtobuf:
return "InvalidProtobuf";
case TypeAliasState::kPresentValidProtobuf:
return "ValidProtobuf";
}
};
if (!T::kHasRequest) {
name += "NoRequest";
}
auto function_state_to_string = [](FunctionState state) {
switch (state) {
case FunctionState::kAbsent:
return "Absent";
case FunctionState::kPresentInvalid:
return "Invalid";
case FunctionState::kPresentValid:
return "Valid";
}
};
if (!T::kHasResponse) {
name += "NoResponse";
}
if (!T::kHasURL) {
name += "NoURL";
}
return name + "_does" + (T::kSatisfiesConcept ? "" : "_not");
return base::StrCat(
{"Request_",
type_alias_state_to_string(IsEndpointTestCase::kRequestState),
"_Response_",
type_alias_state_to_string(IsEndpointTestCase::kResponseState),
"_URL_", function_state_to_string(IsEndpointTestCase::kURLState),
"_does", (IsEndpointTestCase::kSatisfiesConcept ? "" : "_not")});
}
};
template <typename>
struct IsEndpointTest : testing::Test {};
} // namespace
TYPED_TEST_SUITE(IsEndpointConceptMatrixTest, EndpointMatrix, EndpointName);
TYPED_TEST_SUITE(IsEndpointTest, IsEndpointTestCases, IsEndpointTestCaseName);
TYPED_TEST(IsEndpointConceptMatrixTest, SatisfyConcept) {
TYPED_TEST(IsEndpointTest, SatisfyConcept) {
EXPECT_EQ(IsEndpoint<TypeParam>, TypeParam::kSatisfiesConcept);
}
@@ -11,20 +11,29 @@
namespace brave_account::endpoint_client::detail {
// Primary template: a type does not satisfy IsRequest unless
// matched by the partial specialization below.
template <typename>
inline constexpr bool kIsRequest = false;
inline constexpr bool kIsJSONRequest = false;
// Partial specialization: Request<T, M> satisfies IsRequest if
// T satisfies IsRequestBody.
template <IsRequestBody T, Method M>
inline constexpr bool kIsRequest<Request<T, M>> = true;
template <IsJSONRequestBody RequestBody, Method M>
inline constexpr bool kIsJSONRequest<Request<RequestBody, M>> = true;
// Concept: a type satisfies IsRequest if
// its kIsRequest specialization evaluates to true.
template <typename T>
concept IsRequest = kIsRequest<T>;
// A JSON request is a Request<> whose body is a JSON request body.
template <typename Request>
concept IsJSONRequest = kIsJSONRequest<Request>;
template <typename>
inline constexpr bool kIsProtobufRequest = false;
template <IsProtobufRequestBody RequestBody, Method M>
inline constexpr bool kIsProtobufRequest<Request<RequestBody, M>> = true;
// A Protobuf request is a Request<> whose body is a Protobuf request body.
template <typename Request>
concept IsProtobufRequest = kIsProtobufRequest<Request>;
// A request is either a JSON request or a Protobuf request.
template <typename Request>
concept IsRequest = IsJSONRequest<Request> || IsProtobufRequest<Request>;
} // namespace brave_account::endpoint_client::detail
@@ -10,22 +10,27 @@
#include <type_traits>
#include "base/values.h"
#include "google/protobuf/message_lite.h"
namespace brave_account::endpoint_client::detail {
// Concept that checks whether `T` defines a non-static, accessible member
// function `ToValue()` such that:
// - `t.ToValue()` is a valid expression,
// and that call yields a `base::DictValue`
// - `&T::ToValue` is a valid member function pointer (ensures it's a
// non-static member function)
//
// In short: models any type with a proper non-static `ToValue()` function
// whose result is a `base::DictValue`.
template <typename T>
concept IsRequestBody = requires(T t) {
{ t.ToValue() } -> std::same_as<base::DictValue>;
} && std::is_member_function_pointer_v<decltype(&T::ToValue)>;
// A JSON request body provides a non-static ToValue()
// callable on a const object and returning base::DictValue.
template <typename RequestBody>
concept IsJSONRequestBody = requires(const RequestBody& request_body) {
{ request_body.ToValue() } -> std::same_as<base::DictValue>;
} && std::is_member_function_pointer_v<decltype(&RequestBody::ToValue)>;
// A Protobuf request body is a type publicly derived from
// google::protobuf::MessageLite.
template <typename RequestBody>
concept IsProtobufRequestBody =
std::derived_from<RequestBody, google::protobuf::MessageLite>;
// A request body is either a JSON request body or a Protobuf request body.
template <typename RequestBody>
concept IsRequestBody =
IsJSONRequestBody<RequestBody> || IsProtobufRequestBody<RequestBody>;
} // namespace brave_account::endpoint_client::detail
@@ -5,61 +5,96 @@
#include "brave/components/brave_account/endpoint_client/is_request_body.h"
#include <tuple>
#include <type_traits>
#include <string>
#include "base/values.h"
#include "brave/components/brave_account/endpoint_client/concept_test.h"
#include "base/strings/strcat.h"
#include "google/protobuf/message_lite.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace base {
class DictValue;
} // namespace base
namespace brave_account::endpoint_client::detail {
namespace {
struct RequestBodyNoToValue {
static constexpr char kName[] = "RequestBodyNoToValue";
struct JSONRequestBodyNoToValue {
static constexpr char kName[] = "JSONRequestBodyNoToValue";
};
struct RequestBodyStaticToValue {
static constexpr char kName[] = "RequestBodyStaticToValue";
struct JSONRequestBodyStaticToValue {
static constexpr char kName[] = "JSONRequestBodyStaticToValue";
static base::DictValue ToValue();
};
struct RequestBodyToValueWithWrongReturnType {
static constexpr char kName[] = "RequestBodyToValueWithWrongReturnType";
void ToValue() const;
struct JSONRequestBodyNonConstToValue {
static constexpr char kName[] = "JSONRequestBodyNonConstToValue";
base::DictValue ToValue();
};
struct RequestBodyToValueWithWrongParameterType {
static constexpr char kName[] = "RequestBodyToValueWithWrongParameterType";
struct JSONRequestBodyToValueWithWrongReturnType {
static constexpr char kName[] = "JSONRequestBodyToValueWithWrongReturnType";
std::string ToValue() const;
};
struct JSONRequestBodyToValueWithWrongParameterType {
static constexpr char kName[] =
"JSONRequestBodyToValueWithWrongParameterType";
base::DictValue ToValue(int) const;
};
struct ValidRequestBody {
static constexpr char kName[] = "ValidRequestBody";
struct JSONRequestBodyValid {
static constexpr char kName[] = "JSONRequestBodyValid";
base::DictValue ToValue() const;
};
template <typename T>
using IsRequestBodyConceptTest = ConceptTest::Fixture<T>;
class ProtobufRequestBodyPrivateInheritance : google::protobuf::MessageLite {
public:
static constexpr char kName[] = "ProtobufRequestBodyPrivateInheritance";
};
using RequestBodyTestTypes = testing::Types<
std::tuple<RequestBodyNoToValue, std::false_type>,
std::tuple<RequestBodyStaticToValue, std::false_type>,
std::tuple<RequestBodyToValueWithWrongReturnType, std::false_type>,
std::tuple<RequestBodyToValueWithWrongParameterType, std::false_type>,
std::tuple<ValidRequestBody, std::true_type>>;
struct ProtobufRequestBodyValid : google::protobuf::MessageLite {
static constexpr char kName[] = "ProtobufRequestBodyValid";
};
template <typename T, bool SatisfiesConcept>
struct IsRequestBodyTestCase {
using Type = T;
static constexpr bool kSatisfiesConcept = SatisfiesConcept;
};
using IsRequestBodyTestCases = testing::Types<
IsRequestBodyTestCase<JSONRequestBodyNoToValue, false>,
IsRequestBodyTestCase<JSONRequestBodyStaticToValue, false>,
IsRequestBodyTestCase<JSONRequestBodyNonConstToValue, false>,
IsRequestBodyTestCase<JSONRequestBodyToValueWithWrongReturnType, false>,
IsRequestBodyTestCase<JSONRequestBodyToValueWithWrongParameterType, false>,
IsRequestBodyTestCase<JSONRequestBodyValid, true>,
IsRequestBodyTestCase<ProtobufRequestBodyPrivateInheritance, false>,
IsRequestBodyTestCase<ProtobufRequestBodyValid, true>>;
struct IsRequestBodyTestCaseName {
template <typename IsRequestBodyTestCase>
static std::string GetName(int) {
return base::StrCat(
{IsRequestBodyTestCase::Type::kName, "_does",
(IsRequestBodyTestCase::kSatisfiesConcept ? "" : "_not")});
}
};
template <typename>
struct IsRequestBodyTest : testing::Test {};
} // namespace
TYPED_TEST_SUITE(IsRequestBodyConceptTest,
RequestBodyTestTypes,
ConceptTest::NameGenerator);
TYPED_TEST_SUITE(IsRequestBodyTest,
IsRequestBodyTestCases,
IsRequestBodyTestCaseName);
TYPED_TEST(IsRequestBodyConceptTest, SatisfyConcept) {
using TestType = typename TestFixture::TestType;
using ExpectedResult = typename TestFixture::ExpectedResult;
EXPECT_EQ(IsRequestBody<TestType>, ExpectedResult::value);
TYPED_TEST(IsRequestBodyTest, SatisfyConcept) {
EXPECT_EQ(IsRequestBody<typename TypeParam::Type>,
TypeParam::kSatisfiesConcept);
}
} // namespace brave_account::endpoint_client::detail
@@ -5,11 +5,8 @@
#include "brave/components/brave_account/endpoint_client/is_request.h"
#include <tuple>
#include <type_traits>
#include "base/values.h"
#include "brave/components/brave_account/endpoint_client/concept_test.h"
#include "brave/components/brave_account/endpoint_client/json_test_endpoint_bodies.h"
#include "brave/components/brave_account/endpoint_client/protobuf_test_endpoint_bodies.pb.h"
#include "brave/components/brave_account/endpoint_client/request_types.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -17,36 +14,47 @@ namespace brave_account::endpoint_client::detail {
namespace {
struct ValidRequestBody {
base::DictValue ToValue() const;
template <typename T, bool SatisfiesConcept>
struct IsRequestTestCase {
using Type = T;
static constexpr bool kSatisfiesConcept = SatisfiesConcept;
};
template <typename T>
using IsRequestConceptTest = ConceptTest::Fixture<T>;
using IsRequestTestCases =
testing::Types<IsRequestTestCase<void*, false>,
IsRequestTestCase<volatile int, false>,
IsRequestTestCase<JSONRequestBody, false>,
IsRequestTestCase<CONNECT<JSONRequestBody>, true>,
IsRequestTestCase<DELETE<JSONRequestBody>, true>,
IsRequestTestCase<GET<JSONRequestBody>, true>,
IsRequestTestCase<HEAD<JSONRequestBody>, true>,
IsRequestTestCase<OPTIONS<JSONRequestBody>, true>,
IsRequestTestCase<PATCH<JSONRequestBody>, true>,
IsRequestTestCase<POST<JSONRequestBody>, true>,
IsRequestTestCase<PUT<JSONRequestBody>, true>,
IsRequestTestCase<TRACE<JSONRequestBody>, true>,
IsRequestTestCase<TRACK<JSONRequestBody>, true>,
IsRequestTestCase<ProtobufRequestBody, false>,
IsRequestTestCase<CONNECT<ProtobufRequestBody>, true>,
IsRequestTestCase<DELETE<ProtobufRequestBody>, true>,
IsRequestTestCase<GET<ProtobufRequestBody>, true>,
IsRequestTestCase<HEAD<ProtobufRequestBody>, true>,
IsRequestTestCase<OPTIONS<ProtobufRequestBody>, true>,
IsRequestTestCase<PATCH<ProtobufRequestBody>, true>,
IsRequestTestCase<POST<ProtobufRequestBody>, true>,
IsRequestTestCase<PUT<ProtobufRequestBody>, true>,
IsRequestTestCase<TRACE<ProtobufRequestBody>, true>,
IsRequestTestCase<TRACK<ProtobufRequestBody>, true>>;
using RequestTestTypes =
testing::Types<std::tuple<void*, std::false_type>,
std::tuple<volatile int, std::false_type>,
std::tuple<ValidRequestBody, std::false_type>,
std::tuple<CONNECT<ValidRequestBody>, std::true_type>,
std::tuple<DELETE<ValidRequestBody>, std::true_type>,
std::tuple<GET<ValidRequestBody>, std::true_type>,
std::tuple<HEAD<ValidRequestBody>, std::true_type>,
std::tuple<OPTIONS<ValidRequestBody>, std::true_type>,
std::tuple<PATCH<ValidRequestBody>, std::true_type>,
std::tuple<POST<ValidRequestBody>, std::true_type>,
std::tuple<PUT<ValidRequestBody>, std::true_type>,
std::tuple<TRACE<ValidRequestBody>, std::true_type>,
std::tuple<TRACK<ValidRequestBody>, std::true_type>>;
template <typename>
struct IsRequestTest : testing::Test {};
} // namespace
TYPED_TEST_SUITE(IsRequestConceptTest, RequestTestTypes);
TYPED_TEST_SUITE(IsRequestTest, IsRequestTestCases);
TYPED_TEST(IsRequestConceptTest, SatisfyConcept) {
using TestType = typename TestFixture::TestType;
using ExpectedResult = typename TestFixture::ExpectedResult;
EXPECT_EQ(IsRequest<TestType>, ExpectedResult::value);
TYPED_TEST(IsRequestTest, SatisfyConcept) {
EXPECT_EQ(IsRequest<typename TypeParam::Type>, TypeParam::kSatisfiesConcept);
}
} // namespace brave_account::endpoint_client::detail
@@ -11,20 +11,31 @@
namespace brave_account::endpoint_client::detail {
// Primary template: a type does not satisfy IsResponse unless
// matched by the partial specialization below.
template <typename>
inline constexpr bool kIsResponse = false;
inline constexpr bool kIsJSONResponse = false;
// Partial specialization: Response<T, E> satisfies IsResponse if
// T and E satisfy IsResponseBody.
template <IsResponseBody T, IsResponseBody E>
inline constexpr bool kIsResponse<Response<T, E>> = true;
template <IsJSONResponseBody SuccessBody, IsJSONResponseBody ErrorBody>
inline constexpr bool kIsJSONResponse<Response<SuccessBody, ErrorBody>> = true;
// Concept: a type satisfies IsResponse if
// its kIsResponse specialization evaluates to true.
template <typename T>
concept IsResponse = kIsResponse<T>;
// A JSON response is a Response<> whose bodies are JSON response bodies.
template <typename Response>
concept IsJSONResponse = kIsJSONResponse<Response>;
template <typename>
inline constexpr bool kIsProtobufResponse = false;
template <IsProtobufResponseBody SuccessBody, IsProtobufResponseBody ErrorBody>
inline constexpr bool kIsProtobufResponse<Response<SuccessBody, ErrorBody>> =
true;
// A Protobuf response is a Response<> whose bodies are Protobuf response
// bodies.
template <typename Response>
concept IsProtobufResponse = kIsProtobufResponse<Response>;
// A response is either a JSON response or a Protobuf response.
template <typename Response>
concept IsResponse = IsJSONResponse<Response> || IsProtobufResponse<Response>;
} // namespace brave_account::endpoint_client::detail
@@ -9,24 +9,31 @@
#include <concepts>
#include <optional>
namespace base {
class Value;
} // namespace base
#include "base/values.h"
#include "google/protobuf/message_lite.h"
namespace brave_account::endpoint_client::detail {
// Concept that checks whether `T` defines a static, accessible member
// function `FromValue()` such that:
// - `T::FromValue(value)` is a valid expression,
// and that call yields `std::optional<T>`
//
// In short: models any type with a proper static `FromValue()` function
// whose result is a `std::optional<T>`.
template <typename T>
concept IsResponseBody = requires(const base::Value& value) {
{ T::FromValue(value) } -> std::same_as<std::optional<T>>;
// A JSON response body provides a static FromValue() taking
// const base::Value& and returning std::optional<ResponseBody>.
template <typename ResponseBody>
concept IsJSONResponseBody = requires(const base::Value& value) {
{
ResponseBody::FromValue(value)
} -> std::same_as<std::optional<ResponseBody>>;
};
// A Protobuf response body is a type publicly derived from
// google::protobuf::MessageLite.
template <typename ResponseBody>
concept IsProtobufResponseBody =
std::derived_from<ResponseBody, google::protobuf::MessageLite>;
// A response body is either a JSON response body or a Protobuf response body.
template <typename ResponseBody>
concept IsResponseBody =
IsJSONResponseBody<ResponseBody> || IsProtobufResponseBody<ResponseBody>;
} // namespace brave_account::endpoint_client::detail
#endif // BRAVE_COMPONENTS_BRAVE_ACCOUNT_ENDPOINT_CLIENT_IS_RESPONSE_BODY_H_
@@ -6,62 +6,95 @@
#include "brave/components/brave_account/endpoint_client/is_response_body.h"
#include <optional>
#include <tuple>
#include <type_traits>
#include <string>
#include "base/values.h"
#include "brave/components/brave_account/endpoint_client/concept_test.h"
#include "base/strings/strcat.h"
#include "google/protobuf/message_lite.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace base {
class Value;
} // namespace base
namespace brave_account::endpoint_client::detail {
namespace {
struct ResponseBodyNoFromValue {
static constexpr char kName[] = "ResponseBodyNoFromValue";
struct JSONResponseBodyNoFromValue {
static constexpr char kName[] = "JSONResponseBodyNoFromValue";
};
struct ResponseBodyNonStaticFromValue {
static constexpr char kName[] = "ResponseBodyNonStaticFromValue";
std::optional<ResponseBodyNonStaticFromValue> FromValue(const base::Value&);
struct JSONResponseBodyNonStaticFromValue {
static constexpr char kName[] = "JSONResponseBodyNonStaticFromValue";
std::optional<JSONResponseBodyNonStaticFromValue> FromValue(
const base::Value&);
};
struct ResponseBodyFromValueWithWrongReturnType {
static constexpr char kName[] = "ResponseBodyFromValueWithWrongReturnType";
static ResponseBodyFromValueWithWrongReturnType FromValue(const base::Value&);
struct JSONResponseBodyFromValueWithWrongReturnType {
static constexpr char kName[] =
"JSONResponseBodyFromValueWithWrongReturnType";
static JSONResponseBodyFromValueWithWrongReturnType FromValue(
const base::Value&);
};
struct ResponseBodyFromValueWithWrongParameterType {
static constexpr char kName[] = "ResponseBodyFromValueWithWrongParameterType";
static std::optional<ResponseBodyFromValueWithWrongParameterType> FromValue(
int);
struct JSONResponseBodyFromValueWithWrongParameterType {
static constexpr char kName[] =
"JSONResponseBodyFromValueWithWrongParameterType";
static std::optional<JSONResponseBodyFromValueWithWrongParameterType>
FromValue(const std::string&);
};
struct ValidResponseBody {
static constexpr char kName[] = "ValidResponseBody";
static std::optional<ValidResponseBody> FromValue(const base::Value&);
struct JSONResponseBodyValid {
static constexpr char kName[] = "JSONResponseBodyValid";
static std::optional<JSONResponseBodyValid> FromValue(const base::Value&);
};
template <typename T>
using IsResponseBodyConceptTest = ConceptTest::Fixture<T>;
class ProtobufResponseBodyPrivateInheritance : google::protobuf::MessageLite {
public:
static constexpr char kName[] = "ProtobufResponseBodyPrivateInheritance";
};
using ResponseBodyTestTypes = testing::Types<
std::tuple<ResponseBodyNoFromValue, std::false_type>,
std::tuple<ResponseBodyNonStaticFromValue, std::false_type>,
std::tuple<ResponseBodyFromValueWithWrongReturnType, std::false_type>,
std::tuple<ResponseBodyFromValueWithWrongParameterType, std::false_type>,
std::tuple<ValidResponseBody, std::true_type>>;
struct ProtobufResponseBodyValid : google::protobuf::MessageLite {
static constexpr char kName[] = "ProtobufResponseBodyValid";
};
template <typename T, bool SatisfiesConcept>
struct IsResponseBodyTestCase {
using Type = T;
static constexpr bool kSatisfiesConcept = SatisfiesConcept;
};
using IsResponseBodyTestCases = testing::Types<
IsResponseBodyTestCase<JSONResponseBodyNoFromValue, false>,
IsResponseBodyTestCase<JSONResponseBodyNonStaticFromValue, false>,
IsResponseBodyTestCase<JSONResponseBodyFromValueWithWrongReturnType, false>,
IsResponseBodyTestCase<JSONResponseBodyFromValueWithWrongParameterType,
false>,
IsResponseBodyTestCase<JSONResponseBodyValid, true>,
IsResponseBodyTestCase<ProtobufResponseBodyPrivateInheritance, false>,
IsResponseBodyTestCase<ProtobufResponseBodyValid, true>>;
struct IsResponseBodyTestCaseName {
template <typename IsResponseBodyTestCase>
static std::string GetName(int) {
return base::StrCat(
{IsResponseBodyTestCase::Type::kName, "_does",
(IsResponseBodyTestCase::kSatisfiesConcept ? "" : "_not")});
}
};
template <typename>
struct IsResponseBodyTest : testing::Test {};
} // namespace
TYPED_TEST_SUITE(IsResponseBodyConceptTest,
ResponseBodyTestTypes,
ConceptTest::NameGenerator);
TYPED_TEST_SUITE(IsResponseBodyTest,
IsResponseBodyTestCases,
IsResponseBodyTestCaseName);
TYPED_TEST(IsResponseBodyConceptTest, SatisfyConcept) {
using TestType = typename TestFixture::TestType;
using ExpectedResult = typename TestFixture::ExpectedResult;
EXPECT_EQ(IsResponseBody<TestType>, ExpectedResult::value);
TYPED_TEST(IsResponseBodyTest, SatisfyConcept) {
EXPECT_EQ(IsResponseBody<typename TypeParam::Type>,
TypeParam::kSatisfiesConcept);
}
} // namespace brave_account::endpoint_client::detail
@@ -5,12 +5,8 @@
#include "brave/components/brave_account/endpoint_client/is_response.h"
#include <optional>
#include <tuple>
#include <type_traits>
#include "base/values.h"
#include "brave/components/brave_account/endpoint_client/concept_test.h"
#include "brave/components/brave_account/endpoint_client/json_test_endpoint_bodies.h"
#include "brave/components/brave_account/endpoint_client/protobuf_test_endpoint_bodies.pb.h"
#include "brave/components/brave_account/endpoint_client/response.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -18,27 +14,30 @@ namespace brave_account::endpoint_client::detail {
namespace {
struct ValidResponseBody {
static std::optional<ValidResponseBody> FromValue(const base::Value&);
template <typename T, bool SatisfiesConcept>
struct IsResponseTestCase {
using Type = T;
static constexpr bool kSatisfiesConcept = SatisfiesConcept;
};
template <typename T>
using IsResponseConceptTest = ConceptTest::Fixture<T>;
using IsResponseTestCases = testing::Types<
IsResponseTestCase<void*, false>,
IsResponseTestCase<volatile int, false>,
IsResponseTestCase<JSONSuccessBody, false>,
IsResponseTestCase<Response<JSONSuccessBody, JSONErrorBody>, true>,
IsResponseTestCase<ProtobufSuccessBody, false>,
IsResponseTestCase<Response<ProtobufSuccessBody, ProtobufErrorBody>, true>,
IsResponseTestCase<Response<JSONSuccessBody, ProtobufErrorBody>, false>>;
using ResponseTestTypes = testing::Types<
std::tuple<void*, std::false_type>,
std::tuple<volatile int, std::false_type>,
std::tuple<ValidResponseBody, std::false_type>,
std::tuple<Response<ValidResponseBody, ValidResponseBody>, std::true_type>>;
template <typename>
struct IsResponseTest : testing::Test {};
} // namespace
TYPED_TEST_SUITE(IsResponseConceptTest, ResponseTestTypes);
TYPED_TEST_SUITE(IsResponseTest, IsResponseTestCases);
TYPED_TEST(IsResponseConceptTest, SatisfyConcept) {
using TestType = typename TestFixture::TestType;
using ExpectedResult = typename TestFixture::ExpectedResult;
EXPECT_EQ(IsResponse<TestType>, ExpectedResult::value);
TYPED_TEST(IsResponseTest, SatisfyConcept) {
EXPECT_EQ(IsResponse<typename TypeParam::Type>, TypeParam::kSatisfiesConcept);
}
} // namespace brave_account::endpoint_client::detail
@@ -3,11 +3,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
// Schema for the /v2/auth/logout request and response.
namespace endpoints {
dictionary AuthLogoutRequestBody {};
dictionary AuthLogoutSuccessBody {};
dictionary AuthLogoutErrorBody {};
// Schema for the empty JSON body.
// Used when no request body is sent or the response body is ignored.
namespace endpoint_client {
dictionary JSONEmptyBody {};
};
@@ -3,11 +3,17 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
// Schema for the /v2/verify request and response.
namespace endpoints {
dictionary VerifyDeleteRequestBody {};
// Schemas for JSON bodies used by test endpoints.
namespace endpoint_client {
dictionary JSONRequestBody {
DOMString request;
};
dictionary VerifyDeleteSuccessBody {};
dictionary JSONSuccessBody {
DOMString success;
};
dictionary VerifyDeleteErrorBody {};
dictionary JSONErrorBody {
DOMString error;
};
};
@@ -0,0 +1,16 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
syntax = "proto3";
option optimize_for = LITE_RUNTIME;
package brave_account.endpoint_client;
// Schema for the empty Protobuf body.
// Used when no request body is sent or the response body is ignored.
// Compatible with LITE_RUNTIME because it subclasses MessageLite
// (unlike google::protobuf::Empty).
message ProtobufEmptyBody {}
@@ -0,0 +1,24 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
syntax = "proto3";
option optimize_for = LITE_RUNTIME;
package brave_account.endpoint_client;
// Schemas for Protobuf bodies used by test endpoints.
message ProtobufRequestBody {
string request = 1;
}
message ProtobufSuccessBody {
string success = 1;
}
message ProtobufErrorBody {
string error = 1;
}
@@ -6,9 +6,13 @@
#ifndef BRAVE_COMPONENTS_BRAVE_ACCOUNT_ENDPOINT_CLIENT_REQUEST_H_
#define BRAVE_COMPONENTS_BRAVE_ACCOUNT_ENDPOINT_CLIENT_REQUEST_H_
#include <optional>
#include <string>
#include <string_view>
#include "base/json/json_writer.h"
#include "base/time/time.h"
#include "base/values.h"
#include "brave/components/brave_account/endpoint_client/is_request_body.h"
#include "net/http/http_request_headers.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
@@ -29,11 +33,12 @@ enum class Method {
kTrack
};
// Wrapper that binds an IsRequestBody to a specific HTTP method.
// Inherits from T to expose its ToValue() interface, and
// adds a static Method() accessor returning the canonical HTTP method string.
template <IsRequestBody T, Method M>
struct Request : T {
// A request binds a request body type to a specific HTTP method and
// provides access to the method, Content-Type, and serialized body.
template <IsRequestBody RequestBody, Method M>
struct Request {
using Body = RequestBody;
static constexpr std::string_view Method() {
if constexpr (M == Method::kConnect) {
return net::HttpRequestHeaders::kConnectMethod;
@@ -60,8 +65,43 @@ struct Request : T {
}
}
static constexpr std::string_view ContentType() {
return ContentType<Body>();
}
std::optional<std::string> Serialize() const { return Serialize(body); }
Body body;
net::MutableNetworkTrafficAnnotationTag network_traffic_annotation_tag;
base::TimeDelta timeout_duration;
private:
// Returns the Content-Type value for a JSON request body.
template <IsJSONRequestBody>
static constexpr std::string_view ContentType() {
return "application/json";
}
// Returns the Content-Type value for a Protobuf request body.
template <IsProtobufRequestBody>
static constexpr std::string_view ContentType() {
return "application/protobuf";
}
// Serializes a JSON request body to a JSON string.
// Returns std::nullopt if the request body produces an empty dictionary.
static auto Serialize(const IsJSONRequestBody auto& request_body) {
const base::DictValue dict = request_body.ToValue();
return !dict.empty() ? base::WriteJson(dict).value_or("")
: std::optional<std::string>();
}
// Serializes a Protobuf request body to a binary string.
// Returns std::nullopt if the request body is empty (ByteSizeLong() == 0).
static auto Serialize(const IsProtobufRequestBody auto& request_body) {
return request_body.ByteSizeLong() ? request_body.SerializeAsString()
: std::optional<std::string>();
}
};
} // namespace brave_account::endpoint_client::detail
@@ -6,22 +6,132 @@
#ifndef BRAVE_COMPONENTS_BRAVE_ACCOUNT_ENDPOINT_CLIENT_RESPONSE_H_
#define BRAVE_COMPONENTS_BRAVE_ACCOUNT_ENDPOINT_CLIENT_RESPONSE_H_
#include <concepts>
#include <optional>
#include <string>
#include <utility>
#include "base/json/json_reader.h"
#include "base/memory/scoped_refptr.h"
#include "base/types/expected.h"
#include "base/values.h"
#include "brave/components/brave_account/endpoint_client/is_response_body.h"
#include "brave/components/brave_account/endpoint_client/json_empty_body.h"
#include "brave/components/brave_account/endpoint_client/protobuf_empty_body.pb.h"
#include "net/base/net_errors.h"
#include "net/http/http_response_headers.h"
#include "services/network/public/cpp/header_util.h"
namespace brave_account::endpoint_client {
template <detail::IsResponseBody T, detail::IsResponseBody E>
// A response contains the network result, HTTP status code, and the
// deserialized success or error body.
template <detail::IsResponseBody SuccessResponseBody,
detail::IsResponseBody ErrorResponseBody>
struct Response {
using SuccessBody = T;
using ErrorBody = E;
using SuccessBody = SuccessResponseBody;
using ErrorBody = ErrorResponseBody;
// Builds a Response from the network result, HTTP response headers,
// and optional response body. Body parsing is only attempted for
// net::OK responses with headers present.
static Response Deserialize(
int net_error,
const scoped_refptr<net::HttpResponseHeaders>& headers,
std::optional<std::string> response_body) {
Response response;
response.net_error = net_error;
if (response.net_error != net::OK || !headers) {
return response;
}
response.status_code = headers->response_code();
if (network::IsSuccessfulStatus(*response.status_code)) { // 2xx
response.body = Deserialize<SuccessBody>(std::move(response_body));
} else { // non-2xx
response.body = Deserialize<ErrorBody>(std::move(response_body))
.transform([](auto error_body) {
return base::unexpected(std::move(error_body));
});
}
return response;
}
int net_error = net::ERR_IO_PENDING;
std::optional<int> status_code;
std::optional<base::expected<T, E>> body;
std::optional<base::expected<SuccessBody, ErrorBody>> body;
private:
// Deserializes a JSON response body into ResponseBody.
//
// Behavior:
//
// 1. No body expected:
// (std::same_as<ResponseBody, JSONEmptyBody>):
// Whatever the server returns is ignored. We replace it with "{}" so the
// returned value is always a valid instance of the empty type, never
// std::nullopt. This also means parsing failures are ignored: if you don't
// expect data from the server, you shouldn't care whether what it sent was
// parseable or not.
//
// 2. Body expected:
// (!std::same_as<ResponseBody, JSONEmptyBody>):
// The returned value is std::nullopt if:
// - response_body is std::nullopt,
// - JSONReader::Read() fails,
// - ResponseBody::FromValue() fails.
//
// The returned value is non-std::nullopt only when response_body contains
// valid JSON matching the expected ResponseBody structure.
template <detail::IsJSONResponseBody ResponseBody>
static auto Deserialize(std::optional<std::string> response_body) {
if (std::same_as<ResponseBody, JSONEmptyBody>) {
response_body = "{}";
}
const auto value =
base::JSONReader::Read(response_body.value_or(""), base::JSON_PARSE_RFC)
.value_or(base::Value());
return ResponseBody::FromValue(value);
}
// Deserializes a Protobuf response body into ResponseBody.
//
// Behavior:
//
// 1. No body expected
// (std::same_as<ResponseBody, ProtobufEmptyBody>):
// Whatever the server returns is ignored. We skip parsing and treat the
// body as successfully deserialized, so the returned value is always a
// valid instance of the empty type, never std::nullopt. This also means
// parsing failures are ignored: if you don't expect
// data from the server, you shouldn't care whether what it sent was
// parseable or not.
//
// 2. Body expected
// (!std::same_as<ResponseBody, ProtobufEmptyBody>):
// The returned value is std::nullopt if:
// - response_body is std::nullopt,
// - response_body is empty,
// - parsing via ParseFromString() fails (invalid wire format).
//
// The returned value is non-std::nullopt only when response_body is
// non-empty and can be successfully parsed into ResponseBody.
//
// Note: successful parsing guarantees only wire-format compatibility with
// ResponseBody. Protobuf permits unknown fields, so semantic correctness
// must be validated by the caller.
template <detail::IsProtobufResponseBody ResponseBody>
static auto Deserialize(std::optional<std::string> response_body) {
ResponseBody body;
return std::same_as<ResponseBody, ProtobufEmptyBody> ||
(response_body && !response_body->empty() &&
body.ParseFromString(*response_body))
? std::optional(std::move(body))
: std::nullopt;
}
};
} // namespace brave_account::endpoint_client
@@ -15,6 +15,7 @@
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "services/network/test/test_utils.h"
@@ -31,7 +32,7 @@ void MockResponseFor(network::TestURLLoaderFactory& test_url_loader_factory,
return network::CreateURLResponseHead(
static_cast<net::HttpStatusCode>(status_code));
})
.value_or(nullptr);
.value_or(network::mojom::URLResponseHead::New());
const auto body = base::WriteJson(response.body
.transform([](const auto& body) {
@@ -27,7 +27,6 @@ source_set("endpoints") {
generated_types("generated_api_types") {
sources = [
"auth_logout_bodies.idl",
"auth_validate_bodies.idl",
"error_body.idl",
"login_finalize_bodies.idl",
@@ -35,7 +34,6 @@ generated_types("generated_api_types") {
"password_finalize_bodies.idl",
"password_init_bodies.idl",
"service_token_bodies.idl",
"verify_delete_bodies.idl",
"verify_resend_bodies.idl",
"verify_result_bodies.idl",
]
@@ -7,17 +7,18 @@
#define BRAVE_COMPONENTS_BRAVE_ACCOUNT_ENDPOINTS_AUTH_LOGOUT_H_
#include "brave/components/brave_account/endpoint_client/brave_endpoint.h"
#include "brave/components/brave_account/endpoint_client/json_empty_body.h"
#include "brave/components/brave_account/endpoint_client/request_types.h"
#include "brave/components/brave_account/endpoint_client/response.h"
#include "brave/components/brave_account/endpoints/auth_logout_bodies.h"
namespace brave_account::endpoints {
using AuthLogout = endpoint_client::BraveEndpoint<
"accounts.bsg",
"/v2/auth/logout",
endpoint_client::POST<AuthLogoutRequestBody>,
endpoint_client::Response<AuthLogoutSuccessBody, AuthLogoutErrorBody>>;
endpoint_client::POST<endpoint_client::JSONEmptyBody>,
endpoint_client::Response<endpoint_client::JSONEmptyBody,
endpoint_client::JSONEmptyBody>>;
} // namespace brave_account::endpoints
@@ -14,16 +14,6 @@
namespace brave_account::endpoints {
bool operator==(const AuthLogout::Response::SuccessBody&,
const AuthLogout::Response::SuccessBody&) {
return true;
}
bool operator==(const AuthLogout::Response::ErrorBody&,
const AuthLogout::Response::ErrorBody&) {
return true;
}
namespace {
using AuthLogoutTestCase = EndpointTestCase<AuthLogout>;
@@ -33,9 +23,10 @@ const AuthLogoutTestCase* Success() {
{.test_name = "success",
.http_status_code = net::HTTP_NO_CONTENT,
.raw_response_body = "",
.expected_response = {.net_error = net::OK,
.status_code = net::HTTP_NO_CONTENT,
.body = AuthLogout::Response::SuccessBody()}});
.expected_response = {
.net_error = net::OK,
.status_code = net::HTTP_NO_CONTENT,
.body = base::ok(AuthLogout::Response::SuccessBody())}});
return kSuccess.get();
}
@@ -7,6 +7,7 @@
#define BRAVE_COMPONENTS_BRAVE_ACCOUNT_ENDPOINTS_AUTH_VALIDATE_H_
#include "brave/components/brave_account/endpoint_client/brave_endpoint.h"
#include "brave/components/brave_account/endpoint_client/json_empty_body.h"
#include "brave/components/brave_account/endpoint_client/request_types.h"
#include "brave/components/brave_account/endpoint_client/response.h"
#include "brave/components/brave_account/endpoints/auth_validate_bodies.h"
@@ -17,7 +18,7 @@ namespace brave_account::endpoints {
using AuthValidate = endpoint_client::BraveEndpoint<
"accounts.bsg",
"/v2/auth/validate",
endpoint_client::GET<AuthValidateRequestBody>,
endpoint_client::GET<endpoint_client::JSONEmptyBody>,
endpoint_client::Response<AuthValidateSuccessBody, ErrorBody>>;
} // namespace brave_account::endpoints
@@ -3,10 +3,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
// Schema for the /v2/auth/validate request and response.
// Schema for the /v2/auth/validate response.
namespace endpoints {
dictionary AuthValidateRequestBody {};
dictionary AuthValidateSuccessBody {
DOMString email;
};
@@ -26,19 +26,26 @@
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace brave_account::endpoint_client {
template <typename T, typename E>
inline bool operator==(const Response<T, E>& lhs, const Response<T, E>& rhs) {
return std::tie(lhs.net_error, lhs.status_code, lhs.body) ==
std::tie(rhs.net_error, rhs.status_code, rhs.body);
}
inline bool operator==(const JSONEmptyBody&, const JSONEmptyBody&) {
return true;
}
} // namespace brave_account::endpoint_client
namespace brave_account::endpoints {
inline bool operator==(const ErrorBody& lhs, const ErrorBody& rhs) {
return lhs.code == rhs.code;
}
template <typename T, typename E>
bool operator==(const endpoint_client::Response<T, E>& lhs,
const endpoint_client::Response<T, E>& rhs) {
return lhs.net_error == rhs.net_error && lhs.status_code == rhs.status_code &&
lhs.body == rhs.body;
}
template <endpoint_client::IsEndpoint T>
struct EndpointTestCase {
std::string test_name;
@@ -7,6 +7,7 @@
#define BRAVE_COMPONENTS_BRAVE_ACCOUNT_ENDPOINTS_PASSWORD_FINALIZE_H_
#include "brave/components/brave_account/endpoint_client/brave_endpoint.h"
#include "brave/components/brave_account/endpoint_client/json_empty_body.h"
#include "brave/components/brave_account/endpoint_client/request_types.h"
#include "brave/components/brave_account/endpoint_client/response.h"
#include "brave/components/brave_account/endpoints/error_body.h"
@@ -18,7 +19,7 @@ using PasswordFinalize = endpoint_client::BraveEndpoint<
"accounts.bsg",
"/v2/accounts/password/finalize",
endpoint_client::POST<PasswordFinalizeRequestBody>,
endpoint_client::Response<PasswordFinalizeSuccessBody, ErrorBody>>;
endpoint_client::Response<endpoint_client::JSONEmptyBody, ErrorBody>>;
} // namespace brave_account::endpoints
@@ -3,11 +3,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
// Schema for the /v2/accounts/password/finalize request and response.
// Schema for the /v2/accounts/password/finalize request.
namespace endpoints {
dictionary PasswordFinalizeRequestBody {
DOMString serializedRecord;
};
dictionary PasswordFinalizeSuccessBody {};
};
@@ -15,11 +15,6 @@
namespace brave_account::endpoints {
bool operator==(const PasswordFinalize::Response::SuccessBody& lhs,
const PasswordFinalize::Response::SuccessBody& rhs) {
return true;
}
using PasswordFinalizeTestCase = EndpointTestCase<PasswordFinalize>;
namespace {
@@ -7,17 +7,18 @@
#define BRAVE_COMPONENTS_BRAVE_ACCOUNT_ENDPOINTS_VERIFY_DELETE_H_
#include "brave/components/brave_account/endpoint_client/brave_endpoint.h"
#include "brave/components/brave_account/endpoint_client/json_empty_body.h"
#include "brave/components/brave_account/endpoint_client/request_types.h"
#include "brave/components/brave_account/endpoint_client/response.h"
#include "brave/components/brave_account/endpoints/verify_delete_bodies.h"
namespace brave_account::endpoints {
using VerifyDelete = endpoint_client::BraveEndpoint<
"accounts.bsg",
"/v2/verify",
endpoint_client::DELETE<VerifyDeleteRequestBody>,
endpoint_client::Response<VerifyDeleteSuccessBody, VerifyDeleteErrorBody>>;
endpoint_client::DELETE<endpoint_client::JSONEmptyBody>,
endpoint_client::Response<endpoint_client::JSONEmptyBody,
endpoint_client::JSONEmptyBody>>;
} // namespace brave_account::endpoints
@@ -14,16 +14,6 @@
namespace brave_account::endpoints {
bool operator==(const VerifyDelete::Response::SuccessBody&,
const VerifyDelete::Response::SuccessBody&) {
return true;
}
bool operator==(const VerifyDelete::Response::ErrorBody&,
const VerifyDelete::Response::ErrorBody&) {
return true;
}
namespace {
using VerifyDeleteTestCase = EndpointTestCase<VerifyDelete>;
@@ -33,9 +23,10 @@ const VerifyDeleteTestCase* Success() {
{.test_name = "success",
.http_status_code = net::HTTP_NO_CONTENT,
.raw_response_body = "",
.expected_response = {.net_error = net::OK,
.status_code = net::HTTP_NO_CONTENT,
.body = VerifyDelete::Response::SuccessBody()}});
.expected_response = {
.net_error = net::OK,
.status_code = net::HTTP_NO_CONTENT,
.body = base::ok(VerifyDelete::Response::SuccessBody())}});
return kSuccess.get();
}
@@ -7,6 +7,7 @@
#define BRAVE_COMPONENTS_BRAVE_ACCOUNT_ENDPOINTS_VERIFY_RESEND_H_
#include "brave/components/brave_account/endpoint_client/brave_endpoint.h"
#include "brave/components/brave_account/endpoint_client/json_empty_body.h"
#include "brave/components/brave_account/endpoint_client/request_types.h"
#include "brave/components/brave_account/endpoint_client/response.h"
#include "brave/components/brave_account/endpoints/error_body.h"
@@ -18,7 +19,7 @@ using VerifyResend = endpoint_client::BraveEndpoint<
"accounts.bsg",
"/v2/verify/resend",
endpoint_client::POST<VerifyResendRequestBody>,
endpoint_client::Response<VerifyResendSuccessBody, ErrorBody>>;
endpoint_client::Response<endpoint_client::JSONEmptyBody, ErrorBody>>;
} // namespace brave_account::endpoints
@@ -3,11 +3,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
// Schema for the /v2/verify/resend request and response.
// Schema for the /v2/verify/resend request.
namespace endpoints {
dictionary VerifyResendRequestBody {
DOMString locale;
};
dictionary VerifyResendSuccessBody {};
};
@@ -13,11 +13,6 @@
namespace brave_account::endpoints {
bool operator==(const VerifyResend::Response::SuccessBody&,
const VerifyResend::Response::SuccessBody&) {
return true;
}
namespace {
using VerifyResendTestCase = EndpointTestCase<VerifyResend>;
@@ -11,10 +11,6 @@ namespace email_aliases {
// Email Aliases aliases management JSON types
// Base URL: https://aliases.bravesoftware.com/manage
// Request body for POST /manage (generate alias)
dictionary GenerateAliasRequest {
};
// Response of POST /manage (generate alias)
// Expected when alias is successfully created.
dictionary GenerateAliasResponse {
@@ -58,10 +54,6 @@ dictionary AliasListEntry {
DOMString status;
};
// Request to GET /manage?status=active (alias list result)
dictionary AliasListRequest {
};
// Response to GET /manage?status=active (alias list result)
dictionary AliasListResponse {
// Array of AliasListEntry objects.
@@ -7,6 +7,7 @@
#define BRAVE_COMPONENTS_EMAIL_ALIASES_EMAIL_ALIASES_ENDPOINTS_H_
#include "brave/components/brave_account/endpoint_client/brave_endpoint.h"
#include "brave/components/brave_account/endpoint_client/json_empty_body.h"
#include "brave/components/brave_account/endpoint_client/request_types.h"
#include "brave/components/brave_account/endpoint_client/response.h"
#include "brave/components/email_aliases/email_aliases_api.h"
@@ -16,13 +17,15 @@ namespace email_aliases::endpoints {
using AliasList = brave_account::endpoint_client::BraveEndpoint<
"aliases",
"/manage?status=active",
brave_account::endpoint_client::GET<AliasListRequest>,
brave_account::endpoint_client::GET<
brave_account::endpoint_client::JSONEmptyBody>,
brave_account::endpoint_client::Response<AliasListResponse, ErrorMessage>>;
using GenerateAlias = brave_account::endpoint_client::BraveEndpoint<
"aliases",
"/manage",
brave_account::endpoint_client::POST<GenerateAliasRequest>,
brave_account::endpoint_client::POST<
brave_account::endpoint_client::JSONEmptyBody>,
brave_account::endpoint_client::Response<GenerateAliasResponse,
ErrorMessage>>;
@@ -268,8 +268,8 @@ void EmailAliasesService::UpdateAliasWithToken(
if (update_data->active.has_value()) {
auto request = MakeRequest<brave_account::endpoint_client::WithHeaders<
endpoints::UpdateAlias::Request>>(token.value()->serviceToken);
request.alias = alias_email;
request.status = *update_data->active ? "active" : "inactive";
request.body.alias = alias_email;
request.body.status = *update_data->active ? "active" : "inactive";
refresh_aliases = false; // will be updated in response.
brave_account::endpoint_client::Client<endpoints::UpdateAlias>::Send(
@@ -302,7 +302,7 @@ void EmailAliasesService::DeleteAliasWithToken(const std::string& alias_email,
if (token.has_value()) {
auto request = MakeRequest<brave_account::endpoint_client::WithHeaders<
endpoints::DeleteAlias::Request>>(token.value()->serviceToken);
request.alias = alias_email;
request.body.alias = alias_email;
brave_account::endpoint_client::Client<endpoints::DeleteAlias>::Send(
url_loader_factory_, std::move(request),
base::BindOnce(&EmailAliasesService::OnEditAliasResponse,