From 259350e343f2ce08deca2ed3a23d663def8265d2 Mon Sep 17 00:00:00 2001 From: Qin Wang Date: Tue, 27 Jun 2023 11:41:21 -0700 Subject: [PATCH] Implement FastPairClient to provides APIs to the FastPair backend OAuth/un-OAuth API PiperOrigin-RevId: 543807725 --- fastpair/proto/proto_builder.cc | 19 +- fastpair/proto/proto_builder.h | 8 +- fastpair/proto/proto_builder_test.cc | 8 +- fastpair/server_access/BUILD | 35 + fastpair/server_access/fast_pair_client.h | 51 ++ .../server_access/fast_pair_client_impl.cc | 346 +++++++++ .../server_access/fast_pair_client_impl.h | 87 +++ .../fast_pair_client_impl_test.cc | 669 ++++++++++++++++++ 8 files changed, 1204 insertions(+), 19 deletions(-) create mode 100644 fastpair/server_access/fast_pair_client.h create mode 100644 fastpair/server_access/fast_pair_client_impl.cc create mode 100644 fastpair/server_access/fast_pair_client_impl.h create mode 100644 fastpair/server_access/fast_pair_client_impl_test.cc diff --git a/fastpair/proto/proto_builder.cc b/fastpair/proto/proto_builder.cc index 43554d72..6f3d4b3c 100644 --- a/fastpair/proto/proto_builder.cc +++ b/fastpair/proto/proto_builder.cc @@ -31,17 +31,16 @@ namespace nearby { namespace fastpair { -::nearby::fastpair::proto::FastPairInfo BuildFastPairInfo( - const FastPairDevice& fast_pair_device) { +void BuildFastPairInfo(::nearby::fastpair::proto::FastPairInfo* fast_pair_info, + const FastPairDevice& fast_pair_device) { NEARBY_LOGS(VERBOSE) << __func__; const AccountKey& account_key = fast_pair_device.GetAccountKey(); auto& metadata = fast_pair_device.GetMetadata(); DCHECK(metadata); DCHECK(account_key.Ok()); - ::nearby::fastpair::proto::FastPairInfo proto; - // Sets FastPairDevice to FastPairInfo - auto* device = proto.mutable_device(); + // Sets FastPairDevice of FastPairInfo + auto* device = fast_pair_info->mutable_device(); device->set_account_key(account_key.GetAsBytes()); // Create a SHA256 hash of the |mac_address| with the |account_key| as salt. // The hash is used to identify devices via non discoverable advertisements. @@ -122,15 +121,11 @@ namespace fastpair { fast_pair_strings->set_sync_sms_description(strings.sync_sms_description()); device->set_discovery_item_bytes(discovery_item.SerializeAsString()); - - return proto; } -::nearby::fastpair::proto::FastPairInfo BuildFastPairInfoForOptIn( - proto::OptInStatus opt_in_status) { - proto::FastPairInfo proto; - proto.set_opt_in_status(opt_in_status); - return proto; +void BuildFastPairInfo(::nearby::fastpair::proto::FastPairInfo* fast_pair_info, + proto::OptInStatus opt_in_status) { + fast_pair_info->set_opt_in_status(opt_in_status); } } // namespace fastpair } // namespace nearby diff --git a/fastpair/proto/proto_builder.h b/fastpair/proto/proto_builder.h index 88f5711e..f1fd6445 100644 --- a/fastpair/proto/proto_builder.h +++ b/fastpair/proto/proto_builder.h @@ -22,12 +22,12 @@ namespace nearby { namespace fastpair { // Builds FastPairInfo proto from a FastPairDevice instance -::nearby::fastpair::proto::FastPairInfo BuildFastPairInfo( - const FastPairDevice& fast_pair_device); +void BuildFastPairInfo(::nearby::fastpair::proto::FastPairInfo* fast_pair_info, + const FastPairDevice& fast_pair_device); // Builds FastPairInfo proto from OptInStatus -::nearby::fastpair::proto::FastPairInfo BuildFastPairInfoForOptIn( - proto::OptInStatus opt_in_status); +void BuildFastPairInfo(::nearby::fastpair::proto::FastPairInfo* fast_pair_info, + proto::OptInStatus opt_in_status); } // namespace fastpair } // namespace nearby diff --git a/fastpair/proto/proto_builder_test.cc b/fastpair/proto/proto_builder_test.cc index 9e11ecf7..f00bf419 100644 --- a/fastpair/proto/proto_builder_test.cc +++ b/fastpair/proto/proto_builder_test.cc @@ -52,7 +52,8 @@ TEST(ProtoBuilderTest, BuildFastPairInfo) { device.SetMetadata(device_metadata); // Builds FastPairInfo from the created FastPairDevice - proto::FastPairInfo fast_proto_info = BuildFastPairInfo(device); + proto::FastPairInfo fast_proto_info; + BuildFastPairInfo(&fast_proto_info, device); EXPECT_EQ(fast_proto_info.device().account_key(), account_key.GetAsBytes()); EXPECT_EQ(absl::BytesToHexString( @@ -69,8 +70,9 @@ TEST(ProtoBuilderTest, BuildFastPairInfo) { } TEST(ProtoBuilderTest, BuildFastPairInfoForOptIn) { - proto::FastPairInfo fast_proto_info = - BuildFastPairInfoForOptIn(proto::OptInStatus::OPT_IN_STATUS_OPTED_IN); + proto::FastPairInfo fast_proto_info; + BuildFastPairInfo(&fast_proto_info, + proto::OptInStatus::OPT_IN_STATUS_OPTED_IN); EXPECT_EQ(fast_proto_info.opt_in_status(), proto::OptInStatus::OPT_IN_STATUS_OPTED_IN); } diff --git a/fastpair/server_access/BUILD b/fastpair/server_access/BUILD index 2ab74ccd..07fdf6d7 100644 --- a/fastpair/server_access/BUILD +++ b/fastpair/server_access/BUILD @@ -17,6 +17,7 @@ licenses(["notice"]) cc_library( name = "server_access", srcs = [ + "fast_pair_client_impl.cc", "fast_pair_http_notifier.cc", "fast_pair_metadata_downloader.cc", "fast_pair_metadata_downloader_impl.cc", @@ -24,6 +25,8 @@ cc_library( "fast_pair_repository_impl.cc", ], hdrs = [ + "fast_pair_client.h", + "fast_pair_client_impl.h", "fast_pair_http_notifier.h", "fast_pair_metadata_downloader.h", "fast_pair_metadata_downloader_impl.h", @@ -38,14 +41,20 @@ cc_library( "//fastpair/proto:fastpair_cc_proto", "//fastpair/proto:proto_to_json", "//fastpair/repository", + "//internal/account", + "//internal/auth:types", "//internal/base", "//internal/base:bluetooth_address", "//internal/crypto", "//internal/network:nearby_http_client", "//internal/network:types", "//internal/platform:logging", + "//internal/platform:types", "@com_google_absl//absl/functional:any_invocable", + "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/synchronization", "@nlohmann_json//:json", ], ) @@ -140,3 +149,29 @@ cc_test( "@com_google_googletest//:gtest_main", ], ) + +cc_test( + name = "fast_pair_client_impl_test", + srcs = [ + "fast_pair_client_impl_test.cc", + ], + copts = [ + "-Ithird_party", + ], + deps = [ + ":server_access", + "//fastpair/common", + "//fastpair/proto:fastpair_cc_proto", + "//fastpair/proto:proto_builder", + "//internal/account", + "//internal/account:test_support", + "//internal/auth:credential", + "//internal/network:types", + "//internal/platform:types", + "//internal/platform/implementation/g3", # build_cleaner: keep + "//internal/test", + "//internal/test/google3_only:test", + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/fastpair/server_access/fast_pair_client.h b/fastpair/server_access/fast_pair_client.h new file mode 100644 index 00000000..32a2157a --- /dev/null +++ b/fastpair/server_access/fast_pair_client.h @@ -0,0 +1,51 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_FASTPAIR_SERVER_ACCESS_FAST_PAIR_CLIENT_H_ +#define THIRD_PARTY_NEARBY_FASTPAIR_SERVER_ACCESS_FAST_PAIR_CLIENT_H_ + +#include "absl/status/statusor.h" +#include "fastpair/proto/fastpair_rpcs.proto.h" + +namespace nearby { +namespace fastpair { + +// FastPairClient is used to access Fast Pair backend APIs. +class FastPairClient { + public: + virtual ~FastPairClient() = default; + // Gets an observed device. + // Blocking function + virtual absl::StatusOr GetObservedDevice( + const proto::GetObservedDeviceRequest& request) = 0; + + // Reads the user's devices. + // Blocking function + virtual absl::StatusOr UserReadDevices( + const proto::UserReadDevicesRequest& request) = 0; + + // Writes a new device to a user's account. + // Blocking function + virtual absl::StatusOr UserWriteDevice( + const proto::UserWriteDeviceRequest& request) = 0; + + // Deletes an existing device from a user's account. + // Blocking function + virtual absl::StatusOr UserDeleteDevice( + const proto::UserDeleteDeviceRequest& request) = 0; +}; +} // namespace fastpair +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_FASTPAIR_SERVER_ACCESS_FAST_PAIR_CLIENT_H_ diff --git a/fastpair/server_access/fast_pair_client_impl.cc b/fastpair/server_access/fast_pair_client_impl.cc new file mode 100644 index 00000000..c3d4a628 --- /dev/null +++ b/fastpair/server_access/fast_pair_client_impl.cc @@ -0,0 +1,346 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "fastpair/server_access/fast_pair_client_impl.h" + +#include +#include +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/string_view.h" +#include "absl/synchronization/notification.h" +#include "fastpair/common/fast_pair_switches.h" +#include "internal/account/account_manager.h" +#include "internal/network/http_client.h" +#include "internal/network/url.h" +#include "internal/platform/logging.h" + +namespace nearby { +namespace fastpair { +namespace { +using ::nearby::network::HttpClient; +using ::nearby::network::HttpRequest; +using ::nearby::network::HttpRequestMethod; +using ::nearby::network::HttpResponse; +using ::nearby::network::Url; +// ----------------- HTTP Constants --------------------------------- +constexpr absl::string_view kProtobufContentType = "application/x-protobuf"; +constexpr absl::string_view kKey = "key"; +constexpr absl::string_view kClientId = + "AIzaSyBv7ZrOlX5oIJLVQrZh-WkZFKm5L6FlStQ"; +constexpr absl::string_view kMode = "mode"; +constexpr absl::string_view kQueryParameterAlternateOutputKey = "alt"; +constexpr absl::string_view kQueryParameterAlternateOutputProto = "proto"; +constexpr absl::string_view kPlatformTypeHeaderName = + "X-FastPair-Platform-Type"; +constexpr absl::string_view kDefaultNearbyDevicesHttpHost = + "https://nearbydevices-pa.googleapis.com"; + +constexpr absl::string_view kNearbyV1Path = "v1/"; +constexpr absl::string_view kDevicesPath = "device/"; +constexpr absl::string_view kUserDevicesPath = "user/devices"; +constexpr absl::string_view kUserDeleteDevicePath = "user/device"; +const char* GetObservedDeviceMode[3] = {"MODE_UNKNOWN", "MODE_RELEASE", + "MODE_DEBUG"}; + +absl::string_view GetPlatformTypeString(api::DeviceInfo::OsType os_type) { + switch (os_type) { + case api::DeviceInfo::OsType::kAndroid: + return "OSType.ANDROID"; + case api::DeviceInfo::OsType::kChromeOs: + return "OSType.CHROME_OS"; + case api::DeviceInfo::OsType::kIos: + return "OSType.IOS"; + case api::DeviceInfo::OsType::kWindows: + return "OSType.WINDOWS"; + default: + return "OSType.UNKNOWN"; + } +} + +// Creates the full Nearby V1 URL with |request_path|. +Url CreateV1RequestUrl(absl::string_view request_path) { + std::string host = std::string(kDefaultNearbyDevicesHttpHost); + auto host_switch = switches::GetNearbyFastPairHttpHost(); + if (!host_switch.empty()) { + host = host_switch; + } + std::string path = + absl::StrFormat("%s/%s%s", host, kNearbyV1Path, request_path); + NEARBY_LOGS(INFO) << __func__ << "= " << path; + return Url::Create(path).value(); +} +} // namespace + +FastPairClientImpl::FastPairClientImpl( + auth::AuthenticationManager* authentication_manager, + AccountManager* account_manager, + std::unique_ptr http_client, + FastPairHttpNotifier* notifier, DeviceInfo* device_info) + : authentication_manager_(authentication_manager), + account_manager_(account_manager), + http_client_(std::move(http_client)), + notifier_(notifier), + device_info_(device_info) {} + +// Gets an observed device. +absl::StatusOr +FastPairClientImpl::GetObservedDevice( + const proto::GetObservedDeviceRequest& request) { + NEARBY_LOGS(VERBOSE) << __func__ << ": Start API call to get observed device"; + notifier_->NotifyOfRequest(request); + + // Sets up request mode. + QueryParameters params; + params.push_back({std::string(kMode), GetObservedDeviceMode[request.mode()]}); + + HttpRequest http_request = CreateHttpRequest( + /*access token= */ std::nullopt, + /*Url=*/ + CreateV1RequestUrl(std::string(kDevicesPath) + + std::to_string(request.device_id())), + RequestType::kGet, + /*query parameters=*/params, + /*body=*/std::string()); + + absl::StatusOr http_response = + http_client_->GetResponse(http_request); + + if (!http_response.ok()) { + NEARBY_LOGS(ERROR) << __func__ << ": Failed to get response."; + return http_response.status(); + } + + proto::GetObservedDeviceResponse response; + if (!response.ParseFromString(http_response->GetBody().GetRawData())) { + NEARBY_LOGS(ERROR) << __func__ << ": Failed to parse server response."; + return absl::InvalidArgumentError("Parse proto error"); + } + + notifier_->NotifyOfResponse(response); + NEARBY_LOGS(INFO) + << __func__ << ": Complete API call to get observed device successfully"; + return response; +} + +// Reads the user's devices. +absl::StatusOr +FastPairClientImpl::UserReadDevices( + const proto::UserReadDevicesRequest& request) { + NEARBY_LOGS(VERBOSE) << __func__ << ": Start API call to read user's devices"; + notifier_->NotifyOfRequest(request); + + absl::StatusOr access_token = GetAccessToken(); + if (!access_token.ok()) { + NEARBY_LOGS(WARNING) << __func__ + << ": Skip API call to read user devices due to no " + "available access token."; + return absl::UnauthenticatedError("No user logged in."); + } + + HttpRequest http_request = CreateHttpRequest( + *access_token, + /*Url=*/CreateV1RequestUrl(kUserDevicesPath), RequestType::kGet, + /*query parameters=*/std::nullopt, + /*body=*/request.SerializeAsString()); + + absl::StatusOr http_response = + http_client_->GetResponse(http_request); + + if (!http_response.ok()) { + NEARBY_LOGS(ERROR) << __func__ << ": Failed to get response."; + return http_response.status(); + } + + proto::UserReadDevicesResponse response; + if (!response.ParseFromString(http_response->GetBody().GetRawData())) { + NEARBY_LOGS(ERROR) << __func__ << ": Failed to parse server response."; + return absl::InvalidArgumentError("Parse proto error"); + } + + notifier_->NotifyOfResponse(response); + NEARBY_LOGS(INFO) << __func__ + << ": Complete API call to read user devices successfully"; + return response; +} + +// Writes a new device to a user's account. +absl::StatusOr +FastPairClientImpl::UserWriteDevice( + const proto::UserWriteDeviceRequest& request) { + NEARBY_LOGS(VERBOSE) << __func__ << ": Start API call to write user device"; + notifier_->NotifyOfRequest(request); + + absl::StatusOr access_token = GetAccessToken(); + if (!access_token.ok()) { + NEARBY_LOGS(WARNING) << __func__ + << ": Skip API call to write user devices due to no " + "available access token."; + return absl::UnauthenticatedError("No user logged in."); + } + + HttpRequest http_request = CreateHttpRequest( + *access_token, + /*Url=*/CreateV1RequestUrl(kUserDevicesPath), RequestType::kPost, + /*query parameters=*/std::nullopt, + /*body=*/request.SerializeAsString()); + + absl::StatusOr http_response = + http_client_->GetResponse(http_request); + + if (!http_response.ok()) { + NEARBY_LOGS(ERROR) << __func__ << ": Failed to get response."; + return http_response.status(); + } + + proto::UserWriteDeviceResponse response; + if (!response.ParseFromString(http_response->GetBody().GetRawData())) { + NEARBY_LOGS(ERROR) << __func__ << ": Failed to parse server response."; + return absl::InvalidArgumentError("Parse proto error"); + } + + notifier_->NotifyOfResponse(response); + NEARBY_LOGS(INFO) << __func__ + << ": Complete API call to write user devices successfully"; + return response; +} + +// Deletes an existing device from a user's account. +absl::StatusOr +FastPairClientImpl::UserDeleteDevice( + const proto::UserDeleteDeviceRequest& request) { + NEARBY_LOGS(VERBOSE) << __func__ << ": Start API call to delete user device"; + notifier_->NotifyOfRequest(request); + + absl::StatusOr access_token = GetAccessToken(); + if (!access_token.ok()) { + NEARBY_LOGS(WARNING) << __func__ + << ": Skip API call to delete user devices due to no " + "available access token."; + return absl::UnauthenticatedError("No user logged in."); + } + + HttpRequest http_request = + CreateHttpRequest(*access_token, + /*Url=*/ + CreateV1RequestUrl(std::string(kUserDeleteDevicePath) + + "/" + request.hex_account_key()), + RequestType::kDelete, + /*query parameters=*/std::nullopt, + /*body=*/std::string()); + + absl::StatusOr http_response = + http_client_->GetResponse(http_request); + + if (!http_response.ok()) { + NEARBY_LOGS(ERROR) << __func__ << ": Failed to get response."; + return http_response.status(); + } + + proto::UserDeleteDeviceResponse response; + if (!response.ParseFromString(http_response->GetBody().GetRawData())) { + NEARBY_LOGS(ERROR) << __func__ << ": Failed to parse server response."; + return absl::InvalidArgumentError("Parse proto error"); + } + + notifier_->NotifyOfResponse(response); + NEARBY_LOGS(INFO) + << __func__ << ": Complete API call to delete user devices successfully"; + return response; +} + +// Blocking function +absl::StatusOr FastPairClientImpl::GetAccessToken() { + NEARBY_LOGS(VERBOSE) << __func__; + std::optional account = + account_manager_->GetCurrentAccount(); + if (!account.has_value()) { + NEARBY_LOGS(WARNING) + << __func__ << ": Failed to get access token due to no login user."; + return absl::UnauthenticatedError("No user logged in."); + } + absl::StatusOr result; + absl::Notification notification; + authentication_manager_->FetchAccessToken( + account->id, { + .success_cb = + [&](absl::string_view access_token) { + result = std::string(access_token); + notification.Notify(); + }, + .failure_cb = + [&](auth::AuthStatus status) { + result = absl::UnknownError( + absl::StrCat(static_cast(status))); + notification.Notify(); + }, + }); + notification.WaitForNotification(); + return result; +} + +HttpRequest FastPairClientImpl::CreateHttpRequest( + std::optional access_token, Url url, + RequestType request_type, + std::optional request_as_query_parameters, + std::optional body) { + NEARBY_LOGS(VERBOSE) << __func__; + HttpRequest request{url}; + + // Handles query strings + request.AddQueryParameter(kQueryParameterAlternateOutputKey, + kQueryParameterAlternateOutputProto); + request.AddQueryParameter(kKey, kClientId); + if (request_as_query_parameters.has_value()) { + for (const auto& key_value_pair : *request_as_query_parameters) { + request.AddQueryParameter(key_value_pair.first, key_value_pair.second); + } + } + + // Handles request method + request.SetMethod(GetRequestMethod(request_type)); + // Handles headers + if (access_token.has_value()) { + NEARBY_LOGS(INFO) << __func__ << " : Authorization with access token"; + request.AddHeader("Authorization", + absl::StrCat("Bearer ", access_token.value())); + } + request.AddHeader(kPlatformTypeHeaderName, + GetPlatformTypeString(device_info_->GetOsType())); + request.AddHeader("Content-Type", + body ? kProtobufContentType : std::string()); + + // Handles request body + request.SetBody(body.value_or(std::string())); + return request; +} + +HttpRequestMethod FastPairClientImpl::GetRequestMethod( + RequestType request_type) const { + if (request_type == RequestType::kPost) { + NEARBY_LOGS(INFO) << __func__ << " : request_type= kPost"; + return HttpRequestMethod::kPost; + } else if (request_type == RequestType::kDelete) { + NEARBY_LOGS(INFO) << __func__ << ": request_type= kDelete"; + return HttpRequestMethod::kDelete; + } + NEARBY_LOGS(INFO) << __func__ << " : request_type= kGet"; + return HttpRequestMethod::kGet; +} +} // namespace fastpair +} // namespace nearby diff --git a/fastpair/server_access/fast_pair_client_impl.h b/fastpair/server_access/fast_pair_client_impl.h new file mode 100644 index 00000000..f9aba500 --- /dev/null +++ b/fastpair/server_access/fast_pair_client_impl.h @@ -0,0 +1,87 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_FASTPAIR_SERVER_ACCESS_FAST_PAIR_CLIENT_IMPL_H_ +#define THIRD_PARTY_NEARBY_FASTPAIR_SERVER_ACCESS_FAST_PAIR_CLIENT_IMPL_H_ + +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "fastpair/server_access/fast_pair_client.h" +#include "fastpair/server_access/fast_pair_http_notifier.h" +#include "internal/account/account_manager.h" +#include "internal/auth/authentication_manager.h" +#include "internal/network/http_client.h" +#include "internal/network/url.h" +#include "internal/platform/device_info.h" + +namespace nearby { +namespace fastpair { +// An implementation of FastPairClient that fetches access tokens and makes +// HTTP request to FastPair backend. +class FastPairClientImpl : public FastPairClient { + public: + // Query strings of the request. + using QueryParameters = std::vector>; + + FastPairClientImpl(auth::AuthenticationManager* authentication_manager, + AccountManager* account_manager, + std::unique_ptr http_client, + FastPairHttpNotifier* notifier, DeviceInfo* device_info); + FastPairClientImpl(FastPairClientImpl&) = delete; + FastPairClientImpl& operator=(FastPairClientImpl&) = delete; + ~FastPairClientImpl() override = default; + + // Gets an observed device. + absl::StatusOr GetObservedDevice( + const proto::GetObservedDeviceRequest& request) override; + + // Reads the user's devices. + absl::StatusOr UserReadDevices( + const proto::UserReadDevicesRequest& request) override; + + // Writes a new device to a user's account. + absl::StatusOr UserWriteDevice( + const proto::UserWriteDeviceRequest& request) override; + + // Deletes an existing device from a user's account. + absl::StatusOr UserDeleteDevice( + const proto::UserDeleteDeviceRequest& request) override; + + private: + enum class RequestType { kGet, kPost, kDelete }; + network::HttpRequestMethod GetRequestMethod( + FastPairClientImpl::RequestType request_type) const; + // Fetches access token for a logged in user. + absl::StatusOr GetAccessToken(); + network::HttpRequest CreateHttpRequest( + std::optional access_token, network::Url url, + RequestType request_type, + std::optional request_as_query_parameters, + std::optional body); + + auth::AuthenticationManager* authentication_manager_ = nullptr; + AccountManager* account_manager_ = nullptr; + std::unique_ptr http_client_; + FastPairHttpNotifier* notifier_ = nullptr; + DeviceInfo* device_info_ = nullptr; +}; +} // namespace fastpair +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_FASTPAIR_SERVER_ACCESS_FAST_PAIR_CLIENT_IMPL_H_ diff --git a/fastpair/server_access/fast_pair_client_impl_test.cc b/fastpair/server_access/fast_pair_client_impl_test.cc new file mode 100644 index 00000000..8ca6b21c --- /dev/null +++ b/fastpair/server_access/fast_pair_client_impl_test.cc @@ -0,0 +1,669 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "fastpair/server_access/fast_pair_client_impl.h" + +#include + +#include +#include +#include +#include +#include + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" +#include "fastpair/common/fast_pair_device.h" +#include "fastpair/common/fast_pair_prefs.h" +#include "fastpair/common/fast_pair_switches.h" +#include "fastpair/proto/data.proto.h" +#include "fastpair/proto/enum.proto.h" +#include "fastpair/proto/fast_pair_string.proto.h" +#include "fastpair/proto/proto_builder.h" +#include "fastpair/server_access/fast_pair_http_notifier.h" +#include "internal/account/account_manager.h" +#include "internal/account/fake_account_manager.h" +#include "internal/auth/auth_status_util.h" +#include "internal/network/http_client.h" +#include "internal/network/http_client_factory.h" +#include "internal/network/http_request.h" +#include "internal/network/http_response.h" +#include "internal/network/http_status_code.h" +#include "internal/network/url.h" +#include "internal/platform/task_runner_impl.h" +#include "internal/test/fake_device_info.h" +#include "internal/test/google3_only/fake_authentication_manager.h" + +namespace nearby { +namespace fastpair { +namespace { + +using ::nearby::network::HttpClient; +using ::nearby::network::HttpRequest; +using ::nearby::network::HttpRequestMethod; +using ::nearby::network::HttpResponse; +using ::nearby::network::HttpStatusCode; +using ::nearby::network::Url; + +constexpr char kHexModelId[] = "718C17"; +constexpr char kAccessToken[] = "access_token"; +constexpr char kTestAccountId[] = "test_account_id"; +constexpr char kFastPairPreferencesFilePath[] = "Google/Nearby/FastPair"; +constexpr char kDevicesPath[] = "device/"; +constexpr char kUserDevicesPath[] = "user/devices"; +constexpr char kUserDeleteDevicePath[] = "user/device"; +constexpr char kKey[] = "key"; +constexpr char kClientId[] = "AIzaSyBv7ZrOlX5oIJLVQrZh-WkZFKm5L6FlStQ"; +const char kQueryParameterAlternateOutputKey[] = "alt"; +const char kQueryParameterAlternateOutputProto[] = "proto"; +const char kPlatformTypeHeaderName[] = "X-FastPair-Platform-Type"; +constexpr char kWindowsPlatformType[] = "OSType.WINDOWS"; +constexpr char kMode[] = "mode"; +constexpr char kReleaseMode[] = "MODE_RELEASE"; +constexpr char kTestGoogleApisUrl[] = + "https://nearbydevices-pa.testgoogleapis.com"; +constexpr char kBleAddress[] = "11::22::33::44::55::66"; +constexpr char kPublicAddress[] = "20:64:DE:40:F8:93"; +constexpr char kDisplayName[] = "Test Device"; +constexpr char kInitialPairingdescription[] = "InitialPairingdescription"; +constexpr char kAccountKey[] = "04b85786180add47fb81a04a8ce6b0de"; +constexpr char kExpectedSha256Hash[] = + "6353c0075a35b7d81bb30a6190ab246da4b8c55a6111d387400579133c090ed8"; + +class MockHttpClient : public HttpClient { + public: + MOCK_METHOD(void, StartRequest, + (const HttpRequest& request, + std::function&)>), + (override)); + MOCK_METHOD(void, StartCancellableRequest, + (std::unique_ptr request, + std::function&)>), + (override)); + MOCK_METHOD(absl::StatusOr, GetResponse, (const HttpRequest&), + (override)); +}; + +// Return the values associated with |key|, or fail the test if |key| isn't in +// |query_parameters| +std::vector ExpectQueryStringValues( + const std::vector>& query_parameters, + absl::string_view key) { + std::vector values; + for (const auto& pair : query_parameters) { + const auto& query_key = pair.first; + const auto& query_value = pair.second; + if (query_key == key) { + values.push_back(query_value); + } + } + EXPECT_GT(values.size(), 0); + return values; +} + +// A gMock matcher to match proto values. Use this matcher like: +// request/response proto, expected_proto; +// EXPECT_THAT(proto, MatchesProto(expected_proto)); +MATCHER_P(MatchesProto, expected_proto, + absl::StrCat(negation ? "does not match" : "matches", + testing::PrintToString(expected_proto.SerializeAsString()))) { + return arg.has_value() && + arg->SerializeAsString() == expected_proto.SerializeAsString(); +} + +class FastPairClientImplTest : public ::testing::Test, + public FastPairHttpNotifier::Observer { + protected: + FastPairClientImplTest() { + preferences_manager_ = std::make_unique( + kFastPairPreferencesFilePath); + authentication_manager_ = std::make_unique(); + AccountManager::Account account; + account.id = kTestAccountId; + account_manager_ = std::make_unique( + preferences_manager_.get(), prefs::kNearbyFastPairUsersName, + authentication_manager_.get(), task_runner_.get()); + account_manager_->SetAccount(account); + task_runner_ = std::make_unique(1); + device_info_ = std::make_unique(); + } + + void SetUp() override { + GetAuthManager()->EnableSyncMode(); + auto http_client = std::make_unique<::testing::NiceMock>(); + http_client_ = + dynamic_cast<::testing::NiceMock*>(http_client.get()); + switches::SetNearbyFastPairHttpHost(kTestGoogleApisUrl); + fast_pair_client_ = std::make_unique( + authentication_manager_.get(), account_manager_.get(), + std::move(http_client), ¬ifier_, device_info_.get()); + notifier_.AddObserver(this); + } + + void TearDown() override { notifier_.RemoveObserver(this); } + + nearby::FakeAuthenticationManager* GetAuthManager() { + return reinterpret_cast( + authentication_manager_.get()); + } + + // FastPairHttpNotifier::Observer: + // Called when HTTP RPC is made for GetObservedDeviceRequest/Response + void OnGetObservedDeviceRequest( + const proto::GetObservedDeviceRequest& request) override { + get_observer_device_request_ = request; + } + + void OnGetObservedDeviceResponse( + const proto::GetObservedDeviceResponse& response) override { + get_observer_device_response_ = response; + } + + // Called when HTTP RPC is made for UserReadDevicesRequest/Response + void OnUserReadDevicesRequest( + const proto::UserReadDevicesRequest& request) override { + read_devices_request_ = request; + } + void OnUserReadDevicesResponse( + const proto::UserReadDevicesResponse& response) override { + read_devices_response_ = response; + } + + // Called when HTTP RPC is made for UserWriteDeviceRequest/Response + void OnUserWriteDeviceRequest( + const proto::UserWriteDeviceRequest& request) override { + write_device_request_ = request; + } + void OnUserWriteDeviceResponse( + const proto::UserWriteDeviceResponse& response) override { + write_device_response_ = response; + } + + // Called when HTTP RPC is made for UserDeleteDeviceRequest/Response + void OnUserDeleteDeviceRequest( + const proto::UserDeleteDeviceRequest& request) override { + delete_device_request_ = request; + } + void OnUserDeleteDeviceResponse( + const proto::UserDeleteDeviceResponse& response) override { + delete_device_response_ = response; + } + + Url GetUrl(absl::string_view path) { + return Url::Create(absl::StrCat(kTestGoogleApisUrl, "/v1/", path)).value(); + } + + // Requests/Responses + std::optional get_observer_device_request_; + std::optional get_observer_device_response_; + std::optional read_devices_request_; + std::optional read_devices_response_; + std::optional write_device_request_; + std::optional write_device_response_; + std::optional delete_device_request_; + std::optional delete_device_response_; + + std::unique_ptr preferences_manager_; + std::unique_ptr authentication_manager_; + std::unique_ptr account_manager_; + std::unique_ptr fast_pair_client_; + std::unique_ptr http_client_factory_; + std::unique_ptr device_info_; + std::unique_ptr task_runner_; + ::testing::NiceMock* http_client_; + FastPairHttpNotifier notifier_; +}; + +TEST_F(FastPairClientImplTest, GetObservedDeviceSuccess) { + // Sets up proto::GetObservedDeviceRequest + proto::GetObservedDeviceRequest request_proto; + int64_t device_id; + CHECK(absl::SimpleHexAtoi(kHexModelId, &device_id)); + request_proto.set_device_id(device_id); + request_proto.set_mode(proto::GetObservedDeviceRequest::MODE_RELEASE); + + // Sets up proto::GetObservedDeviceResponse + proto::GetObservedDeviceResponse response_proto; + auto* device = response_proto.mutable_device(); + device->set_id(device_id); + auto* observed_device_strings = response_proto.mutable_strings(); + observed_device_strings->set_initial_pairing_description( + kInitialPairingdescription); + + // Sets up HttpResponse + HttpResponse http_response; + http_response.SetStatusCode(nearby::network::HttpStatusCode::kHttpOk); + http_response.SetBody(response_proto.SerializeAsString()); + + // Verifies HttpRequest is as expected + EXPECT_CALL(*http_client_, GetResponse) + .WillOnce([&](const HttpRequest& request) { + EXPECT_EQ(request.GetMethod(), HttpRequestMethod::kGet); + EXPECT_THAT(request.GetUrl().GetUrlPath(), + ::testing::HasSubstr(GetUrl(kDevicesPath).GetUrlPath())); + + EXPECT_EQ(request.GetAllHeaders().find(kPlatformTypeHeaderName)->second, + std::vector{kWindowsPlatformType}); + EXPECT_EQ( + ExpectQueryStringValues(request.GetAllQueryParameters(), kKey), + std::vector{kClientId}); + EXPECT_EQ( + ExpectQueryStringValues(request.GetAllQueryParameters(), + kQueryParameterAlternateOutputKey), + std::vector{kQueryParameterAlternateOutputProto}); + EXPECT_EQ( + ExpectQueryStringValues(request.GetAllQueryParameters(), kMode), + std::vector{kReleaseMode}); + proto::GetObservedDeviceRequest expected_request; + EXPECT_TRUE(expected_request.ParseFromString( + request_proto.SerializeAsString())); + EXPECT_EQ(expected_request.device_id(), device_id); + EXPECT_EQ(expected_request.mode(), + proto::GetObservedDeviceRequest::MODE_RELEASE); + return http_response; + }); + + absl::StatusOr response = + fast_pair_client_->GetObservedDevice(request_proto); + + EXPECT_OK(response); + + // Verifies proto::GetObservedDeviceRequest is as expected + EXPECT_THAT(get_observer_device_request_, MatchesProto(request_proto)); + + // Verifies proto::GetObservedDeviceResponse is as expected + EXPECT_THAT(get_observer_device_response_, MatchesProto(response_proto)); + EXPECT_EQ(response->device().id(), device_id); + EXPECT_EQ(response->strings().initial_pairing_description(), + kInitialPairingdescription); +} + +TEST_F(FastPairClientImplTest, GetObservedDeviceFailureWhenNoRespsone) { + // Sets up proto::GetObservedDeviceRequest + proto::GetObservedDeviceRequest request_proto; + int64_t device_id; + CHECK(absl::SimpleHexAtoi(kHexModelId, &device_id)); + request_proto.set_device_id(device_id); + request_proto.set_mode(proto::GetObservedDeviceRequest::MODE_RELEASE); + + // Verifies HttpRequest is as expected + EXPECT_CALL(*http_client_, GetResponse) + .WillOnce([&](const HttpRequest& request) { + EXPECT_EQ(request.GetMethod(), HttpRequestMethod::kGet); + EXPECT_THAT(request.GetUrl().GetUrlPath(), + ::testing::HasSubstr(GetUrl(kDevicesPath).GetUrlPath())); + return absl::UnavailableError(""); + }); + + absl::StatusOr response = + fast_pair_client_->GetObservedDevice(request_proto); + + EXPECT_FALSE(response.ok()); + EXPECT_TRUE(absl::IsUnavailable(response.status())); +} + +TEST_F(FastPairClientImplTest, GetObservedDeviceFailureWhenParseResponse) { + EXPECT_CALL(*http_client_, GetResponse) + .WillOnce([&](const HttpRequest& request) { + EXPECT_EQ(request.GetMethod(), HttpRequestMethod::kGet); + HttpResponse http_response; + http_response.SetStatusCode(HttpStatusCode::kHttpOk); + http_response.SetBody("Not a valid serialized response message."); + return http_response; + }); + + absl::StatusOr response = + fast_pair_client_->GetObservedDevice(proto::GetObservedDeviceRequest()); + EXPECT_TRUE(absl::IsInvalidArgument(response.status())); +} + +TEST_F(FastPairClientImplTest, UserReadDevicesSuccess) { + // Sets up proto::UserReadDevicesRequest + proto::UserReadDevicesRequest request_proto; + + // Sets up proto::UserReadDevicesResponse + proto::UserReadDevicesResponse response_proto; + auto* fast_pair_info = response_proto.add_fast_pair_info(); + fast_pair_info->set_opt_in_status(proto::OptInStatus::OPT_IN_STATUS_OPTED_IN); + + // Sets up HttpResponse + HttpResponse http_response; + http_response.SetStatusCode(nearby::network::HttpStatusCode::kHttpOk); + http_response.SetBody(response_proto.SerializeAsString()); + + GetAuthManager()->SetFetchAccessTokenResult(auth::AuthStatus::SUCCESS, + kAccessToken); + + // Verifies HttpRequest is as expected + EXPECT_CALL(*http_client_, GetResponse) + .WillOnce([&](const HttpRequest& request) { + EXPECT_EQ(request.GetMethod(), HttpRequestMethod::kGet); + EXPECT_THAT( + request.GetUrl().GetUrlPath(), + ::testing::HasSubstr(GetUrl(kUserDevicesPath).GetUrlPath())); + return http_response; + }); + + absl::StatusOr response = + fast_pair_client_->UserReadDevices(request_proto); + + EXPECT_OK(response); + + // Verifies proto::UserReadDevicesRequest is as expected + EXPECT_THAT(read_devices_request_, MatchesProto(request_proto)); + + // Verifies proto::UserReadDevicesResponse is as expected + EXPECT_THAT(read_devices_response_, MatchesProto(response_proto)); + EXPECT_EQ(response->fast_pair_info().size(), 1); + EXPECT_EQ(response->fast_pair_info().Get(0).opt_in_status(), + proto::OptInStatus::OPT_IN_STATUS_OPTED_IN); +} + +TEST_F(FastPairClientImplTest, UserReadDevicesFailureWhenNoRespsone) { + // Sets up proto::UserReadDevicesRequest + proto::UserReadDevicesRequest request_proto; + + GetAuthManager()->SetFetchAccessTokenResult(auth::AuthStatus::SUCCESS, + kAccessToken); + + // Verifies HttpRequest is as expected + EXPECT_CALL(*http_client_, GetResponse) + .WillOnce([&](const HttpRequest& request) { + EXPECT_EQ(request.GetMethod(), HttpRequestMethod::kGet); + EXPECT_THAT( + request.GetUrl().GetUrlPath(), + ::testing::HasSubstr(GetUrl(kUserDevicesPath).GetUrlPath())); + return absl::UnavailableError(""); + }); + + absl::StatusOr response = + fast_pair_client_->UserReadDevices(request_proto); + + EXPECT_FALSE(response.ok()); + EXPECT_TRUE(absl::IsUnavailable(response.status())); +} + +TEST_F(FastPairClientImplTest, UserReadDevicesFailureWhenNoLoginUser) { + account_manager_->SetAccount(std::nullopt); + + EXPECT_CALL(*http_client_, GetResponse).Times(0); + absl::StatusOr response = + fast_pair_client_->UserReadDevices(proto::UserReadDevicesRequest()); + EXPECT_FALSE(response.ok()); +} + +TEST_F(FastPairClientImplTest, UserReadDevicesFailureWhenParseResponse) { + EXPECT_CALL(*http_client_, GetResponse) + .WillOnce([&](const HttpRequest& request) { + EXPECT_EQ(request.GetMethod(), HttpRequestMethod::kGet); + HttpResponse http_response; + http_response.SetStatusCode(HttpStatusCode::kHttpOk); + http_response.SetBody("Not a valid serialized response message."); + return http_response; + }); + + GetAuthManager()->SetFetchAccessTokenResult(auth::AuthStatus::SUCCESS, + kAccessToken); + absl::StatusOr response = + fast_pair_client_->UserReadDevices(proto::UserReadDevicesRequest()); + EXPECT_TRUE(absl::IsInvalidArgument(response.status())); +} + +TEST_F(FastPairClientImplTest, UserWriteDeviceSuccess) { + // Sets up proto::UserWriteDeviceRequest + proto::UserWriteDeviceRequest request_proto; + FastPairDevice device(kHexModelId, kBleAddress, + Protocol::kFastPairInitialPairing); + AccountKey account_key(absl::HexStringToBytes(kAccountKey)); + device.SetAccountKey(account_key); + device.SetPublicAddress(kPublicAddress); + device.SetDisplayName(kDisplayName); + proto::GetObservedDeviceResponse get_observed_device_response; + auto* observed_device_strings = + get_observed_device_response.mutable_strings(); + observed_device_strings->set_initial_pairing_description( + kInitialPairingdescription); + DeviceMetadata device_metadata(get_observed_device_response); + device.SetMetadata(device_metadata); + auto* fast_pair_info = request_proto.mutable_fast_pair_info(); + BuildFastPairInfo(fast_pair_info, device); + + // Sets up proto::UserWriteDeviceResponse + proto::UserWriteDeviceResponse response_proto; + + // Sets up HttpResponse + HttpResponse http_response; + http_response.SetStatusCode(nearby::network::HttpStatusCode::kHttpOk); + http_response.SetBody(response_proto.SerializeAsString()); + + GetAuthManager()->SetFetchAccessTokenResult(auth::AuthStatus::SUCCESS, + kAccessToken); + + // Verifies HttpRequest is as expected + EXPECT_CALL(*http_client_, GetResponse) + .WillOnce([&](const HttpRequest& request) { + EXPECT_EQ(request.GetMethod(), HttpRequestMethod::kPost); + EXPECT_THAT( + request.GetUrl().GetUrlPath(), + ::testing::HasSubstr(GetUrl(kUserDevicesPath).GetUrlPath())); + proto::UserWriteDeviceRequest expected_request; + EXPECT_TRUE(expected_request.ParseFromString( + request_proto.SerializeAsString())); + EXPECT_TRUE(expected_request.has_fast_pair_info()); + auto fast_proto_info = expected_request.fast_pair_info(); + EXPECT_TRUE(fast_proto_info.has_device()); + auto device = fast_proto_info.device(); + EXPECT_EQ(device.account_key(), account_key.GetAsBytes()); + EXPECT_EQ( + absl::BytesToHexString(device.sha256_account_key_public_address()), + kExpectedSha256Hash); + proto::StoredDiscoveryItem stored_discovery_item; + EXPECT_TRUE(stored_discovery_item.ParseFromString( + device.discovery_item_bytes())); + EXPECT_EQ(stored_discovery_item.title(), kDisplayName); + proto::FastPairStrings fast_pair_strings = + stored_discovery_item.fast_pair_strings(); + EXPECT_EQ(fast_pair_strings.initial_pairing_description(), + kInitialPairingdescription); + return http_response; + }); + + absl::StatusOr response = + fast_pair_client_->UserWriteDevice(request_proto); + + EXPECT_OK(response); + + // Verifies proto::UserWriteDeviceRequest is as expected + EXPECT_THAT(write_device_request_, MatchesProto(request_proto)); + + // Verifies proto::UserWriteDeviceResponse is as expected + EXPECT_THAT(write_device_response_, MatchesProto(response_proto)); +} + +TEST_F(FastPairClientImplTest, UserWriteDeviceFailureWhenNoRespsone) { + // Sets up proto::UserWriteDeviceRequest + proto::UserWriteDeviceRequest request_proto; + + GetAuthManager()->SetFetchAccessTokenResult(auth::AuthStatus::SUCCESS, + kAccessToken); + + // Verifies HttpRequest is as expected + EXPECT_CALL(*http_client_, GetResponse) + .WillOnce([&](const HttpRequest& request) { + EXPECT_EQ(request.GetMethod(), HttpRequestMethod::kPost); + EXPECT_THAT( + request.GetUrl().GetUrlPath(), + ::testing::HasSubstr(GetUrl(kUserDevicesPath).GetUrlPath())); + return absl::UnavailableError(""); + }); + + absl::StatusOr response = + fast_pair_client_->UserWriteDevice(request_proto); + + EXPECT_FALSE(response.ok()); + EXPECT_TRUE(absl::IsUnavailable(response.status())); +} + +TEST_F(FastPairClientImplTest, UserWriteDeviceFailureWhenNoLoginUser) { + account_manager_->SetAccount(std::nullopt); + + EXPECT_CALL(*http_client_, GetResponse).Times(0); + absl::StatusOr response = + fast_pair_client_->UserWriteDevice(proto::UserWriteDeviceRequest()); + EXPECT_FALSE(response.ok()); +} + +TEST_F(FastPairClientImplTest, UserWriteDeviceFailureWhenParseResponse) { + EXPECT_CALL(*http_client_, GetResponse) + .WillOnce([&](const HttpRequest& request) { + EXPECT_EQ(request.GetMethod(), HttpRequestMethod::kPost); + HttpResponse http_response; + http_response.SetStatusCode(HttpStatusCode::kHttpOk); + http_response.SetBody("Not a valid serialized response message."); + return http_response; + }); + + GetAuthManager()->SetFetchAccessTokenResult(auth::AuthStatus::SUCCESS, + kAccessToken); + absl::StatusOr response = + fast_pair_client_->UserWriteDevice(proto::UserWriteDeviceRequest()); + EXPECT_TRUE(absl::IsInvalidArgument(response.status())); +} + +TEST_F(FastPairClientImplTest, UserDeleteDeviceSuccess) { + // Sets up proto::UserDeleteDeviceRequest + std::string hex_account_key = kAccountKey; + absl::AsciiStrToUpper(&hex_account_key); + proto::UserDeleteDeviceRequest request_proto; + request_proto.set_hex_account_key(hex_account_key); + + // Sets up proto::UserDeleteDeviceResponse + proto::UserDeleteDeviceResponse response_proto; + + // Sets up HttpResponse + HttpResponse http_response; + http_response.SetStatusCode(nearby::network::HttpStatusCode::kHttpOk); + http_response.SetBody(response_proto.SerializeAsString()); + + GetAuthManager()->SetFetchAccessTokenResult(auth::AuthStatus::SUCCESS, + kAccessToken); + + // Verifies HttpRequest is as expected + EXPECT_CALL(*http_client_, GetResponse) + .WillOnce([&](const HttpRequest& request) { + EXPECT_EQ(request.GetMethod(), HttpRequestMethod::kDelete); + EXPECT_THAT( + request.GetUrl().GetUrlPath(), + ::testing::HasSubstr(GetUrl(kUserDeleteDevicePath).GetUrlPath())); + proto::UserDeleteDeviceRequest expected_request; + EXPECT_TRUE(expected_request.ParseFromString( + request_proto.SerializeAsString())); + EXPECT_EQ(expected_request.hex_account_key(), hex_account_key); + return http_response; + }); + + absl::StatusOr response = + fast_pair_client_->UserDeleteDevice(request_proto); + + EXPECT_OK(response); + + // Verifies proto::UserDeleteDeviceRequest is as expected + EXPECT_THAT(delete_device_request_, MatchesProto(request_proto)); + + // Verifies proto::UserDeleteDeviceResponse is as expected + EXPECT_THAT(delete_device_response_, MatchesProto(response_proto)); +} + +TEST_F(FastPairClientImplTest, UserDeleteDeviceFailureWhenNoRespsone) { + // Sets up proto::UserDeleteDeviceRequest + proto::UserDeleteDeviceRequest request_proto; + + GetAuthManager()->SetFetchAccessTokenResult(auth::AuthStatus::SUCCESS, + kAccessToken); + + // Verifies HttpRequest is as expected + EXPECT_CALL(*http_client_, GetResponse) + .WillOnce([&](const HttpRequest& request) { + EXPECT_EQ(request.GetMethod(), HttpRequestMethod::kDelete); + EXPECT_THAT( + request.GetUrl().GetUrlPath(), + ::testing::HasSubstr(GetUrl(kUserDeleteDevicePath).GetUrlPath())); + return absl::UnavailableError(""); + }); + + absl::StatusOr response = + fast_pair_client_->UserDeleteDevice(request_proto); + + EXPECT_FALSE(response.ok()); + EXPECT_TRUE(absl::IsUnavailable(response.status())); +} + +TEST_F(FastPairClientImplTest, UserDeleteDeviceFailureWhenNoLoginUser) { + account_manager_->SetAccount(std::nullopt); + + EXPECT_CALL(*http_client_, GetResponse).Times(0); + absl::StatusOr response = + fast_pair_client_->UserDeleteDevice(proto::UserDeleteDeviceRequest()); + EXPECT_FALSE(response.ok()); +} + +TEST_F(FastPairClientImplTest, UserDeleteDeviceFailureWhenParseResponse) { + EXPECT_CALL(*http_client_, GetResponse) + .WillOnce([&](const HttpRequest& request) { + EXPECT_EQ(request.GetMethod(), HttpRequestMethod::kDelete); + HttpResponse http_response; + http_response.SetStatusCode(HttpStatusCode::kHttpOk); + http_response.SetBody("Not a valid serialized response message."); + return http_response; + }); + + GetAuthManager()->SetFetchAccessTokenResult(auth::AuthStatus::SUCCESS, + kAccessToken); + absl::StatusOr response = + fast_pair_client_->UserDeleteDevice(proto::UserDeleteDeviceRequest()); + EXPECT_TRUE(absl::IsInvalidArgument(response.status())); +} + +TEST_F(FastPairClientImplTest, FetchAccessTokenFailure) { + EXPECT_CALL(*http_client_, GetResponse).Times(0); + + GetAuthManager()->SetFetchAccessTokenResult( + auth::AuthStatus::PERMISSION_DENIED, std::nullopt); + absl::StatusOr response = + fast_pair_client_->UserReadDevices(proto::UserReadDevicesRequest()); + + EXPECT_TRUE(absl::IsUnauthenticated(response.status())); +} + +TEST_F(FastPairClientImplTest, ParseResponseProtoFailure) { + EXPECT_CALL(*http_client_, GetResponse) + .WillOnce([&](const HttpRequest& request) { + EXPECT_EQ(request.GetMethod(), HttpRequestMethod::kGet); + HttpResponse http_response; + http_response.SetStatusCode(HttpStatusCode::kHttpOk); + http_response.SetBody("Not a valid serialized response message."); + return http_response; + }); + + GetAuthManager()->SetFetchAccessTokenResult(auth::AuthStatus::SUCCESS, + kAccessToken); + absl::StatusOr response = + fast_pair_client_->UserReadDevices(proto::UserReadDevicesRequest()); + EXPECT_TRUE(absl::IsInvalidArgument(response.status())); +} + +} // namespace +} // namespace fastpair +} // namespace nearby