diff --git a/olp-cpp-sdk-dataservice-read/include/olp/dataservice/read/KeyDataRequest.h b/olp-cpp-sdk-dataservice-read/include/olp/dataservice/read/KeyDataRequest.h new file mode 100644 index 000000000..ab6e4577f --- /dev/null +++ b/olp-cpp-sdk-dataservice-read/include/olp/dataservice/read/KeyDataRequest.h @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2026 HERE Europe B.V. + * + * 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 + * + * http://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. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace olp { +namespace dataservice { +namespace read { + +/** + * @brief Encapsulates the fields required to request a plain data blob for + * the given catalog, layer, and key. + * + * You must specify the key of the blob to be retrieved. If the key is not + * set in the request, the request fails with the following error: + * `ErrorCode::InvalidArgument`. + */ +class DATASERVICE_READ_API KeyDataRequest final { + public: + /** + * @brief Gets the key of the requested data blob. + * + * @return The key. + */ + const porting::optional& GetKey() const { return key_; } + + /** + * @brief Sets the key of the data blob to be retrieved. + * + * If the key cannot be found in the layer, the callback returns with an + * empty response (the `null` result for data and an error). + * + * @param key The key. + * + * @return A reference to the updated `KeyDataRequest` instance. + */ + template > + KeyDataRequest& WithKey(T&& key) { + key_ = std::forward(key); + return *this; + } + + /** + * @brief Gets the billing tag to group billing records together. + * + * The billing tag is an optional free-form tag that is used for grouping + * billing records together. If supplied, it must be 4–16 characters + * long and contain only alphanumeric ASCII characters [A-Za-z0-9]. + * + * @return The `BillingTag` string or `olp::porting::none` if the billing tag + * is not set. + */ + const porting::optional& GetBillingTag() const { + return billing_tag_; + } + + /** + * @brief Sets the billing tag for the request. + * + * @see `GetBillingTag()` for information on usage and format. + * + * @param tag The `BillingTag` string or `olp::porting::none`. + * + * @return A reference to the updated `KeyDataRequest` instance. + */ + template > + KeyDataRequest& WithBillingTag(T&& tag) { + billing_tag_ = std::forward(tag); + return *this; + } + + /** + * @brief Gets the request priority. + * + * The default priority is `Priority::NORMAL`. + * + * @return The request priority. + */ + uint32_t GetPriority() const { return priority_; } + + /** + * @brief Sets the priority of the request. + * + * @param priority The priority of the request. + * + * @return A reference to the updated `KeyDataRequest` instance. + */ + KeyDataRequest& WithPriority(uint32_t priority) { + priority_ = priority; + return *this; + } + + /** + * @brief Creates a readable format for the request. + * + * @param layer_id The ID of the layer that is used for the request. + * + * @return A string representation of the request. + */ + std::string CreateKey(const std::string& layer_id) const { + std::stringstream out; + out << layer_id << "["; + if (GetKey()) { + out << *GetKey(); + } + out << "]"; + if (GetBillingTag()) { + out << "$" << *GetBillingTag(); + } + return out.str(); + } + + private: + porting::optional key_; + porting::optional billing_tag_; + uint32_t priority_{thread::NORMAL}; +}; + +} // namespace read +} // namespace dataservice +} // namespace olp diff --git a/olp-cpp-sdk-dataservice-read/include/olp/dataservice/read/VersionedLayerClient.h b/olp-cpp-sdk-dataservice-read/include/olp/dataservice/read/VersionedLayerClient.h index 398d5cf79..e595b8775 100644 --- a/olp-cpp-sdk-dataservice-read/include/olp/dataservice/read/VersionedLayerClient.h +++ b/olp-cpp-sdk-dataservice-read/include/olp/dataservice/read/VersionedLayerClient.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2024 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,9 +31,9 @@ #include #include #include +#include #include #include -#include #include #include #include @@ -187,6 +187,46 @@ class DATASERVICE_READ_API VersionedLayerClient final { */ client::CancellableFuture GetData(DataRequest data_request); + /** + * @brief Fetches a plain data blob asynchronously using a key. + * + * If the specified key cannot be found in the layer, the callback is invoked + * with the empty `DataResponse` object (the `nullptr` result and an error). + * If the key is not set in the request, the callback is invoked with the + * following error: `ErrorCode::InvalidArgument`. + * + * @note The key-based data is not cached at the moment; every request + * results in a network call. + * + * @param request The `KeyDataRequest` instance that contains a complete set + * of request parameters. + * @param callback The `DataResponseCallback` object that is invoked if + * the `DataResult` object is available or an error is encountered. + * + * @return A token that can be used to cancel this request. + */ + client::CancellationToken GetDataByKey(KeyDataRequest request, + DataResponseCallback callback); + + /** + * @brief Fetches a plain data blob asynchronously using a key. + * + * If the specified key cannot be found in the layer, the callback is invoked + * with the empty `DataResponse` object (the `nullptr` result and an error). + * If the key is not set in the request, the callback is invoked with the + * following error: `ErrorCode::InvalidArgument`. + * + * @note The key-based data is not cached at the moment; every request + * results in a network call. + * + * @param request The `KeyDataRequest` instance that contains a complete set + * of request parameters. + * + * @return `CancellableFuture` that contains the `DataResponse` instance + * or an error. You can also use `CancellableFuture` to cancel this request. + */ + client::CancellableFuture GetDataByKey(KeyDataRequest request); + /** * @brief Fetches data asynchronously using a TileKey. * diff --git a/olp-cpp-sdk-dataservice-read/src/VersionedLayerClient.cpp b/olp-cpp-sdk-dataservice-read/src/VersionedLayerClient.cpp index 3084b217d..193be5f2f 100644 --- a/olp-cpp-sdk-dataservice-read/src/VersionedLayerClient.cpp +++ b/olp-cpp-sdk-dataservice-read/src/VersionedLayerClient.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2024 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,6 +56,16 @@ client::CancellableFuture VersionedLayerClient::GetData( return impl_->GetData(std::move(data_request)); } +client::CancellationToken VersionedLayerClient::GetDataByKey( + KeyDataRequest request, DataResponseCallback callback) { + return impl_->GetDataByKey(std::move(request), std::move(callback)); +} + +client::CancellableFuture VersionedLayerClient::GetDataByKey( + KeyDataRequest request) { + return impl_->GetDataByKey(std::move(request)); +} + client::CancellationToken VersionedLayerClient::GetPartitions( PartitionsRequest partitions_request, PartitionsResponseCallback callback) { return impl_->GetPartitions(std::move(partitions_request), diff --git a/olp-cpp-sdk-dataservice-read/src/VersionedLayerClientImpl.cpp b/olp-cpp-sdk-dataservice-read/src/VersionedLayerClientImpl.cpp index 7b41ac79a..50f396bc5 100644 --- a/olp-cpp-sdk-dataservice-read/src/VersionedLayerClientImpl.cpp +++ b/olp-cpp-sdk-dataservice-read/src/VersionedLayerClientImpl.cpp @@ -251,6 +251,33 @@ client::CancellableFuture VersionedLayerClientImpl::GetData( return {cancel_token, std::move(promise)}; } +client::CancellationToken VersionedLayerClientImpl::GetDataByKey( + KeyDataRequest request, DataResponseCallback callback) { + auto data_task = + [=](const client::CancellationContext& context) mutable -> DataResponse { + if (!request.GetKey()) { + return client::ApiError::InvalidArgument("Key is missing"); + } + + repository::DataRepository repository(catalog_, settings_, lookup_client_, + mutex_storage_); + return repository.GetBlobDataByKey(layer_id_, request, context); + }; + + return task_sink_.AddTask(std::move(data_task), std::move(callback), + request.GetPriority()); +} + +client::CancellableFuture VersionedLayerClientImpl::GetDataByKey( + KeyDataRequest request) { + auto promise = std::make_shared>(); + auto cancel_token = + GetDataByKey(std::move(request), [promise](DataResponse response) { + promise->set_value(std::move(response)); + }); + return {cancel_token, std::move(promise)}; +} + client::CancellationToken VersionedLayerClientImpl::PrefetchPartitions( PrefetchPartitionsRequest request, PrefetchPartitionsResponseCallback callback, diff --git a/olp-cpp-sdk-dataservice-read/src/VersionedLayerClientImpl.h b/olp-cpp-sdk-dataservice-read/src/VersionedLayerClientImpl.h index 24ef9fe89..0442efd3a 100644 --- a/olp-cpp-sdk-dataservice-read/src/VersionedLayerClientImpl.h +++ b/olp-cpp-sdk-dataservice-read/src/VersionedLayerClientImpl.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2024 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -66,6 +67,12 @@ class VersionedLayerClientImpl { virtual client::CancellableFuture GetData( DataRequest data_request); + virtual client::CancellationToken GetDataByKey(KeyDataRequest request, + DataResponseCallback callback); + + virtual client::CancellableFuture GetDataByKey( + KeyDataRequest request); + virtual client::CancellationToken GetData(TileRequest request, DataResponseCallback callback); diff --git a/olp-cpp-sdk-dataservice-read/src/generated/api/BlobApi.cpp b/olp-cpp-sdk-dataservice-read/src/generated/api/BlobApi.cpp index cb15747f6..46460a0e4 100644 --- a/olp-cpp-sdk-dataservice-read/src/generated/api/BlobApi.cpp +++ b/olp-cpp-sdk-dataservice-read/src/generated/api/BlobApi.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2024 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,10 +22,9 @@ #include #include #include -#include -#include #include +#include namespace olp { namespace dataservice { @@ -55,7 +54,7 @@ BlobApi::DataResponse BlobApi::GetBlob( // In case we know the size in advance, we should pre-allocated a buffer. const auto expected_size = partition.GetDataSize(); - const auto kPartitionPreallocateLimit = 10 * 1024 * 1024; + constexpr auto kPartitionPreallocateLimit = 10 * 1024 * 1024; if (expected_size && *expected_size > 0 && *expected_size < kPartitionPreallocateLimit) { buffer.reserve(*expected_size); @@ -84,6 +83,51 @@ BlobApi::DataResponse BlobApi::GetBlob( return {std::make_shared>(std::move(buffer)), api_response.GetNetworkStatistics()}; } + +BlobApi::DataResponse BlobApi::GetBlobByKey( + const client::OlpClient& client, const std::string& layer_id, + const std::string& key, porting::optional billing_tag, + porting::optional range, + const client::CancellationContext& context) { + std::multimap header_params; + header_params.emplace("Accept", "application/octet-stream"); + if (range) { + header_params.emplace("Range", *range); + } + + std::multimap query_params; + if (billing_tag) { + query_params.emplace("billingTag", *billing_tag); + } + + std::string metadata_uri = + "/layers/" + layer_id + "/keys/" + olp::utils::Url::Encode(key); + + std::vector buffer; + + auto data_callback = [&](const std::uint8_t* data, const std::uint64_t offset, + const std::size_t length) { + if (!offset) { + buffer.clear(); + } + + const auto buffer_size = buffer.size(); + buffer.resize(buffer_size + length); + std::memcpy(buffer.data() + buffer_size, data, length); + }; + + auto api_response = + client.CallApiStream(metadata_uri, "GET", query_params, header_params, + data_callback, nullptr, "", context); + + if (api_response.GetStatus() != http::HttpStatusCode::OK) { + return {client::ApiError(api_response.GetStatus()), + api_response.GetNetworkStatistics()}; + } + + return {std::make_shared>(std::move(buffer)), + api_response.GetNetworkStatistics()}; +} } // namespace read } // namespace dataservice } // namespace olp diff --git a/olp-cpp-sdk-dataservice-read/src/generated/api/BlobApi.h b/olp-cpp-sdk-dataservice-read/src/generated/api/BlobApi.h index 399b58309..b212dec93 100644 --- a/olp-cpp-sdk-dataservice-read/src/generated/api/BlobApi.h +++ b/olp-cpp-sdk-dataservice-read/src/generated/api/BlobApi.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2024 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,6 +71,32 @@ class BlobApi { porting::optional billing_tag, porting::optional range, const client::CancellationContext& context); + + /** + * @brief Retrieves a plain data blob for the specified layer and key. + * @param client Instance of OlpClient used to make REST request. + * @param layer_id Layer id. + * @param key The key of the blob to be retrieved. + * @param billing_tag An optional free-form tag which is used for grouping + * billing records together. If supplied, it must be between 4 - 16 + * characters, contain only alpha/numeric ASCII characters [A-Za-z0-9]. + * @param range Use this parameter to resume download of a large response + * when there is a connection issue between the client and server. + * Specify a single byte range offset like this: Range: bytes=10-. + * This parameter is compliant with RFC 7233, but note that this parameter + * only supports a single byte range. The range parameter can also be + * specified as a query parameter, i.e. range=bytes=10-. + * @param context A CancellationContext, which can be used to cancel the + * pending request. + * + * @return Data response. + */ + static DataResponse GetBlobByKey(const client::OlpClient& client, + const std::string& layer_id, + const std::string& key, + porting::optional billing_tag, + porting::optional range, + const client::CancellationContext& context); }; } // namespace read diff --git a/olp-cpp-sdk-dataservice-read/src/repositories/DataRepository.cpp b/olp-cpp-sdk-dataservice-read/src/repositories/DataRepository.cpp index 037fa3f4d..6493c816f 100644 --- a/olp-cpp-sdk-dataservice-read/src/repositories/DataRepository.cpp +++ b/olp-cpp-sdk-dataservice-read/src/repositories/DataRepository.cpp @@ -249,6 +249,25 @@ BlobApi::DataResponse DataRepository::GetBlobData( return storage_response; } +BlobApi::DataResponse DataRepository::GetBlobDataByKey( + const std::string& layer_id, const KeyDataRequest& request, + client::CancellationContext context) { + if (!request.GetKey()) { + return client::ApiError::InvalidArgument("Key is missing"); + } + + auto storage_api_lookup = lookup_client_.LookupApi( + kBlobService, "v1", client::OnlineIfNotFound, context); + + if (!storage_api_lookup.IsSuccessful()) { + return storage_api_lookup.GetError(); + } + + return BlobApi::GetBlobByKey(storage_api_lookup.GetResult(), layer_id, + *request.GetKey(), request.GetBillingTag(), + olp::porting::none, context); +} + BlobApi::DataResponse DataRepository::GetVolatileData( const std::string& layer_id, const DataRequest& request, client::CancellationContext context, const bool fail_on_cache_error) { diff --git a/olp-cpp-sdk-dataservice-read/src/repositories/DataRepository.h b/olp-cpp-sdk-dataservice-read/src/repositories/DataRepository.h index 290086661..8a747ab00 100644 --- a/olp-cpp-sdk-dataservice-read/src/repositories/DataRepository.h +++ b/olp-cpp-sdk-dataservice-read/src/repositories/DataRepository.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2024 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ #include #include #include "olp/dataservice/read/DataRequest.h" +#include "olp/dataservice/read/KeyDataRequest.h" #include "olp/dataservice/read/Types.h" #include "NamedMutex.h" @@ -63,6 +64,10 @@ class DataRepository final { const porting::optional& billing_tag, client::CancellationContext context, bool fail_on_cache_error); + BlobApi::DataResponse GetBlobDataByKey(const std::string& layer_id, + const KeyDataRequest& request, + client::CancellationContext context); + private: client::HRN catalog_; client::OlpClientSettings settings_; diff --git a/olp-cpp-sdk-dataservice-read/tests/BlobApiTest.cpp b/olp-cpp-sdk-dataservice-read/tests/BlobApiTest.cpp new file mode 100644 index 000000000..56fcff9d6 --- /dev/null +++ b/olp-cpp-sdk-dataservice-read/tests/BlobApiTest.cpp @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2026 HERE Europe B.V. + * + * 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 + * + * http://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. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#include +#include +#include +#include +#include +#include "generated/api/BlobApi.h" + +namespace { +using ::testing::_; +using ::testing::AllOf; +using ::testing::Mock; +namespace http = olp::http; +namespace client = olp::client; +namespace read = olp::dataservice::read; +namespace model = olp::dataservice::read::model; + +std::string ApiErrorToString(const client::ApiError& error) { + std::ostringstream result_stream; + result_stream << "ERROR: code: " << static_cast(error.GetErrorCode()) + << ", status: " << error.GetHttpStatusCode() + << ", message: " << error.GetMessage(); + return result_stream.str(); +} + +const std::string kBaseUrl{ + "https://some.blob.base.url/blobstore/v1/catalogs/" + "hrn:here:data::olp-here-test:hereos-internal-test-v2"}; +const std::string kLayerId{"testlayer"}; +const std::string kDataHandle{"d5d73b64-7365-41c3-8faf-aa6ad5bab135"}; +const std::string kBlobData{"plain-data-blob"}; + +class BlobApiTest : public testing::Test { + protected: + void SetUp() override { + network_mock_ = std::make_shared(); + + client::OlpClientSettings settings; + settings.network_request_handler = network_mock_; + settings.task_scheduler = + client::OlpClientSettingsFactory::CreateDefaultTaskScheduler(1); + olp_client_ = client::OlpClient(settings, kBaseUrl); + } + + void TearDown() override { network_mock_.reset(); } + + protected: + client::OlpClient olp_client_; + std::shared_ptr network_mock_; +}; + +TEST_F(BlobApiTest, GetBlobByKey) { + { + SCOPED_TRACE("GetBlobByKey succeeds"); + + const auto kUrl = kBaseUrl + "/layers/" + kLayerId + "/keys/test-key"; + + EXPECT_CALL(*network_mock_, Send(IsGetRequest(kUrl), _, _, _, _)) + .WillOnce(ReturnHttpResponse( + http::NetworkResponse().WithStatus(http::HttpStatusCode::OK), + kBlobData)); + + client::CancellationContext context; + const auto response = read::BlobApi::GetBlobByKey( + olp_client_, kLayerId, "test-key", olp::porting::none, + olp::porting::none, context); + + ASSERT_TRUE(response.IsSuccessful()) + << ApiErrorToString(response.GetError()); + ASSERT_NE(nullptr, response.GetResult()); + EXPECT_EQ(kBlobData.size(), response.GetResult()->size()); + EXPECT_EQ(kBlobData, std::string(response.GetResult()->begin(), + response.GetResult()->end())); + + Mock::VerifyAndClearExpectations(network_mock_.get()); + } + { + SCOPED_TRACE("GetBlobByKey URL-encodes the key"); + + const auto kUrl = kBaseUrl + "/layers/" + kLayerId + "/keys/test%2Fkey"; + + EXPECT_CALL(*network_mock_, Send(IsGetRequest(kUrl), _, _, _, _)) + .WillOnce(ReturnHttpResponse( + http::NetworkResponse().WithStatus(http::HttpStatusCode::OK), + kBlobData)); + + client::CancellationContext context; + const auto response = read::BlobApi::GetBlobByKey( + olp_client_, kLayerId, "test/key", olp::porting::none, + olp::porting::none, context); + + ASSERT_TRUE(response.IsSuccessful()) + << ApiErrorToString(response.GetError()); + EXPECT_EQ(kBlobData, std::string(response.GetResult()->begin(), + response.GetResult()->end())); + + Mock::VerifyAndClearExpectations(network_mock_.get()); + } + { + SCOPED_TRACE("GetBlobByKey passes billing tag and range"); + + const auto kUrl = + kBaseUrl + "/layers/" + kLayerId + "/keys/test-key?billingTag=tag12345"; + + EXPECT_CALL(*network_mock_, Send(IsGetRequest(kUrl), _, _, _, _)) + .WillOnce(ReturnHttpResponse( + http::NetworkResponse().WithStatus(http::HttpStatusCode::OK), + kBlobData)); + + client::CancellationContext context; + const auto response = read::BlobApi::GetBlobByKey( + olp_client_, kLayerId, "test-key", std::string("tag12345"), + std::string("bytes=10-"), context); + + ASSERT_TRUE(response.IsSuccessful()) + << ApiErrorToString(response.GetError()); + EXPECT_EQ(kBlobData, std::string(response.GetResult()->begin(), + response.GetResult()->end())); + + Mock::VerifyAndClearExpectations(network_mock_.get()); + } + { + SCOPED_TRACE("GetBlobByKey fails with 404"); + + const auto kUrl = kBaseUrl + "/layers/" + kLayerId + "/keys/test-key"; + + EXPECT_CALL(*network_mock_, Send(IsGetRequest(kUrl), _, _, _, _)) + .WillOnce(ReturnHttpResponse( + http::NetworkResponse().WithStatus(http::HttpStatusCode::NOT_FOUND), + "")); + + client::CancellationContext context; + const auto response = read::BlobApi::GetBlobByKey( + olp_client_, kLayerId, "test-key", olp::porting::none, + olp::porting::none, context); + + ASSERT_FALSE(response.IsSuccessful()); + EXPECT_EQ(response.GetError().GetHttpStatusCode(), + http::HttpStatusCode::NOT_FOUND); + + Mock::VerifyAndClearExpectations(network_mock_.get()); + } + { + SCOPED_TRACE("GetBlobByKey fails with 403"); + + const auto kUrl = kBaseUrl + "/layers/" + kLayerId + "/keys/test-key"; + + EXPECT_CALL(*network_mock_, Send(IsGetRequest(kUrl), _, _, _, _)) + .WillOnce(ReturnHttpResponse( + http::NetworkResponse().WithStatus(http::HttpStatusCode::FORBIDDEN), + "Forbidden")); + + client::CancellationContext context; + const auto response = read::BlobApi::GetBlobByKey( + olp_client_, kLayerId, "test-key", olp::porting::none, + olp::porting::none, context); + + ASSERT_FALSE(response.IsSuccessful()); + EXPECT_EQ(response.GetError().GetHttpStatusCode(), + http::HttpStatusCode::FORBIDDEN); + + Mock::VerifyAndClearExpectations(network_mock_.get()); + } + { + SCOPED_TRACE("GetBlobByKey is cancelled before the request is sent"); + + client::CancellationContext context; + context.CancelOperation(); + ASSERT_TRUE(context.IsCancelled()); + + const auto response = read::BlobApi::GetBlobByKey( + olp_client_, kLayerId, "test-key", olp::porting::none, + olp::porting::none, context); + + ASSERT_FALSE(response.IsSuccessful()); + EXPECT_EQ(response.GetError().GetErrorCode(), client::ErrorCode::Cancelled); + } +} + +TEST_F(BlobApiTest, GetBlob) { + { + SCOPED_TRACE("GetBlob succeeds"); + + const auto kUrl = kBaseUrl + "/layers/" + kLayerId + "/data/" + kDataHandle; + + EXPECT_CALL( + *network_mock_, + Send(AllOf(IsGetRequest(kUrl), + HeadersContain(http::Header("Accept", "application/json"))), + _, _, _, _)) + .WillOnce(ReturnHttpResponse( + http::NetworkResponse().WithStatus(http::HttpStatusCode::OK), + kBlobData)); + + model::Partition partition; + partition.SetDataHandle(kDataHandle); + partition.SetDataSize(1024); + + client::CancellationContext context; + const auto response = + read::BlobApi::GetBlob(olp_client_, kLayerId, partition, + olp::porting::none, olp::porting::none, context); + + ASSERT_TRUE(response.IsSuccessful()) + << ApiErrorToString(response.GetError()); + ASSERT_NE(nullptr, response.GetResult()); + EXPECT_EQ(kBlobData.size(), response.GetResult()->size()); + EXPECT_EQ(kBlobData, std::string(response.GetResult()->begin(), + response.GetResult()->end())); + + Mock::VerifyAndClearExpectations(network_mock_.get()); + } + { + SCOPED_TRACE("GetBlob passes billing tag and range"); + + const auto kUrl = kBaseUrl + "/layers/" + kLayerId + "/data/" + + kDataHandle + "?billingTag=tag12345"; + + EXPECT_CALL(*network_mock_, Send(IsGetRequest(kUrl), _, _, _, _)) + .WillOnce(ReturnHttpResponse( + http::NetworkResponse().WithStatus(http::HttpStatusCode::OK), + kBlobData)); + + model::Partition partition; + partition.SetDataHandle(kDataHandle); + + client::CancellationContext context; + const auto response = read::BlobApi::GetBlob( + olp_client_, kLayerId, partition, std::string("tag12345"), + std::string("bytes=10-"), context); + + ASSERT_TRUE(response.IsSuccessful()) + << ApiErrorToString(response.GetError()); + EXPECT_EQ(kBlobData, std::string(response.GetResult()->begin(), + response.GetResult()->end())); + + Mock::VerifyAndClearExpectations(network_mock_.get()); + } + { + SCOPED_TRACE("GetBlob fails with 404"); + + const auto kUrl = kBaseUrl + "/layers/" + kLayerId + "/data/" + kDataHandle; + + EXPECT_CALL(*network_mock_, Send(IsGetRequest(kUrl), _, _, _, _)) + .WillOnce(ReturnHttpResponse( + http::NetworkResponse().WithStatus(http::HttpStatusCode::NOT_FOUND), + "")); + + model::Partition partition; + partition.SetDataHandle(kDataHandle); + + client::CancellationContext context; + const auto response = + read::BlobApi::GetBlob(olp_client_, kLayerId, partition, + olp::porting::none, olp::porting::none, context); + + ASSERT_FALSE(response.IsSuccessful()); + EXPECT_EQ(response.GetError().GetHttpStatusCode(), + http::HttpStatusCode::NOT_FOUND); + + Mock::VerifyAndClearExpectations(network_mock_.get()); + } + { + SCOPED_TRACE("GetBlob fails with 403"); + + const auto kUrl = kBaseUrl + "/layers/" + kLayerId + "/data/" + kDataHandle; + + EXPECT_CALL(*network_mock_, Send(IsGetRequest(kUrl), _, _, _, _)) + .WillOnce(ReturnHttpResponse( + http::NetworkResponse().WithStatus(http::HttpStatusCode::FORBIDDEN), + "Forbidden")); + + model::Partition partition; + partition.SetDataHandle(kDataHandle); + + client::CancellationContext context; + const auto response = + read::BlobApi::GetBlob(olp_client_, kLayerId, partition, + olp::porting::none, olp::porting::none, context); + + ASSERT_FALSE(response.IsSuccessful()); + EXPECT_EQ(response.GetError().GetHttpStatusCode(), + http::HttpStatusCode::FORBIDDEN); + + Mock::VerifyAndClearExpectations(network_mock_.get()); + } + { + SCOPED_TRACE("GetBlob is cancelled before the request is sent"); + + model::Partition partition; + partition.SetDataHandle(kDataHandle); + + client::CancellationContext context; + context.CancelOperation(); + ASSERT_TRUE(context.IsCancelled()); + + const auto response = + read::BlobApi::GetBlob(olp_client_, kLayerId, partition, + olp::porting::none, olp::porting::none, context); + + ASSERT_FALSE(response.IsSuccessful()); + EXPECT_EQ(response.GetError().GetErrorCode(), client::ErrorCode::Cancelled); + } +} + +} // namespace diff --git a/olp-cpp-sdk-dataservice-read/tests/CMakeLists.txt b/olp-cpp-sdk-dataservice-read/tests/CMakeLists.txt index be3e515ee..1e4237064 100644 --- a/olp-cpp-sdk-dataservice-read/tests/CMakeLists.txt +++ b/olp-cpp-sdk-dataservice-read/tests/CMakeLists.txt @@ -18,6 +18,7 @@ set(OLP_SDK_DATASERVICE_READ_TEST_SOURCES ApiClientLookupTest.cpp AsyncJsonStreamTest.cpp + BlobApiTest.cpp CatalogCacheRepositoryTest.cpp CatalogClientTest.cpp CatalogRepositoryTest.cpp diff --git a/olp-cpp-sdk-dataservice-read/tests/DataRepositoryTest.cpp b/olp-cpp-sdk-dataservice-read/tests/DataRepositoryTest.cpp index 2f8fa8578..4e240bf96 100644 --- a/olp-cpp-sdk-dataservice-read/tests/DataRepositoryTest.cpp +++ b/olp-cpp-sdk-dataservice-read/tests/DataRepositoryTest.cpp @@ -50,6 +50,9 @@ constexpr auto kUrlBlobData5904591 = constexpr auto kUrlBlobData1476147 = R"(https://blob-ireland.data.api.platform.here.com/blobstore/v1/catalogs/hereos-internal-test-v2/layers/testlayer/data/95c5c703-e00e-4c38-841e-e419367474f1)"; +constexpr auto kUrlBlobDataByKey = + R"(https://blob-ireland.data.api.platform.here.com/blobstore/v1/catalogs/hereos-internal-test-v2/layers/testlayer/keys/1%2F1%2F2)"; + constexpr auto kUrlResponseLookup = R"jsonString([{"api":"query","version":"v1","baseURL":"https://sab.query.data.api.platform.here.com/query/v1/catalogs/hrn:here:data::olp-here-test:hereos-internal-test-v2","parameters":{}},{"api":"blob","version":"v1","baseURL":"https://blob-ireland.data.api.platform.here.com/blobstore/v1/catalogs/hereos-internal-test-v2","parameters":{}}])jsonString"; @@ -809,4 +812,109 @@ TEST_F(DataRepositoryTest, GetVersionedDataTileFailedToCache) { ASSERT_EQ(response.GetError().GetErrorCode(), olp::client::ErrorCode::CacheIO); } + +TEST_F(DataRepositoryTest, GetBlobDataByKey) { + EXPECT_CALL(*network_mock_, Send(IsGetRequest(kUrlLookup), _, _, _, _)) + .WillOnce(ReturnHttpResponse(olp::http::NetworkResponse().WithStatus( + olp::http::HttpStatusCode::OK), + kUrlResponseLookup)); + + EXPECT_CALL(*network_mock_, Send(IsGetRequest(kUrlBlobDataByKey), _, _, _, _)) + .WillOnce(ReturnHttpResponse(olp::http::NetworkResponse().WithStatus( + olp::http::HttpStatusCode::OK), + "keyData")); + + olp::client::CancellationContext context; + + olp::client::HRN hrn(GetTestCatalog()); + ApiLookupClient lookup_client(hrn, *settings_); + DataRepository repository(hrn, *settings_, lookup_client); + + const auto request = + olp::dataservice::read::KeyDataRequest().WithKey("1/1/2"); + auto response = repository.GetBlobDataByKey(kLayerId, request, context); + + ASSERT_TRUE(response.IsSuccessful()); + ASSERT_TRUE(response.GetResult() != nullptr); + ASSERT_EQ(7u, response.GetResult()->size()); +} + +TEST_F(DataRepositoryTest, GetBlobDataByKeyNotFound) { + EXPECT_CALL(*network_mock_, Send(IsGetRequest(kUrlLookup), _, _, _, _)) + .WillOnce(ReturnHttpResponse(olp::http::NetworkResponse().WithStatus( + olp::http::HttpStatusCode::OK), + kUrlResponseLookup)); + + EXPECT_CALL(*network_mock_, Send(IsGetRequest(kUrlBlobDataByKey), _, _, _, _)) + .WillOnce(ReturnHttpResponse(olp::http::NetworkResponse().WithStatus( + olp::http::HttpStatusCode::NOT_FOUND), + "")); + + olp::client::CancellationContext context; + + olp::client::HRN hrn(GetTestCatalog()); + ApiLookupClient lookup_client(hrn, *settings_); + DataRepository repository(hrn, *settings_, lookup_client); + + const auto request = + olp::dataservice::read::KeyDataRequest().WithKey("1/1/2"); + auto response = repository.GetBlobDataByKey(kLayerId, request, context); + + ASSERT_FALSE(response.IsSuccessful()); + ASSERT_EQ(response.GetError().GetErrorCode(), + olp::client::ErrorCode::NotFound); +} + +TEST_F(DataRepositoryTest, GetBlobDataByKeyApiLookupFailed403) { + EXPECT_CALL(*network_mock_, Send(IsGetRequest(kUrlLookup), _, _, _, _)) + .WillOnce(ReturnHttpResponse(olp::http::NetworkResponse().WithStatus( + olp::http::HttpStatusCode::FORBIDDEN), + kUrlResponse403)); + + olp::client::CancellationContext context; + + olp::client::HRN hrn(GetTestCatalog()); + ApiLookupClient lookup_client(hrn, *settings_); + DataRepository repository(hrn, *settings_, lookup_client); + + const auto request = + olp::dataservice::read::KeyDataRequest().WithKey("1/1/2"); + auto response = repository.GetBlobDataByKey(kLayerId, request, context); + + ASSERT_FALSE(response.IsSuccessful()); +} + +TEST_F(DataRepositoryTest, GetBlobDataByKeyImmediateCancel) { + olp::client::CancellationContext context; + + context.CancelOperation(); + ASSERT_TRUE(context.IsCancelled()); + + olp::client::HRN hrn(GetTestCatalog()); + ApiLookupClient lookup_client(hrn, *settings_); + DataRepository repository(hrn, *settings_, lookup_client); + + const auto request = + olp::dataservice::read::KeyDataRequest().WithKey("1/1/2"); + auto response = repository.GetBlobDataByKey(kLayerId, request, context); + + ASSERT_FALSE(response.IsSuccessful()); + ASSERT_EQ(response.GetError().GetErrorCode(), + olp::client::ErrorCode::Cancelled); +} + +TEST_F(DataRepositoryTest, GetBlobDataByKeyMissingKey) { + olp::client::CancellationContext context; + + olp::client::HRN hrn(GetTestCatalog()); + ApiLookupClient lookup_client(hrn, *settings_); + DataRepository repository(hrn, *settings_, lookup_client); + + const auto request = olp::dataservice::read::KeyDataRequest(); + auto response = repository.GetBlobDataByKey(kLayerId, request, context); + + ASSERT_FALSE(response.IsSuccessful()); + ASSERT_EQ(response.GetError().GetErrorCode(), + olp::client::ErrorCode::InvalidArgument); +} } // namespace diff --git a/olp-cpp-sdk-dataservice-read/tests/VersionedLayerClientImplTest.cpp b/olp-cpp-sdk-dataservice-read/tests/VersionedLayerClientImplTest.cpp index 7e41bc113..63e8ae6e1 100644 --- a/olp-cpp-sdk-dataservice-read/tests/VersionedLayerClientImplTest.cpp +++ b/olp-cpp-sdk-dataservice-read/tests/VersionedLayerClientImplTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2024 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -110,6 +110,72 @@ TEST(VersionedLayerClientTest, GetData) { Mock::VerifyAndClearExpectations(network_mock.get()); } +TEST(VersionedLayerClientTest, GetDataByKey) { + std::shared_ptr network_mock = std::make_shared(); + std::shared_ptr cache_mock = std::make_shared(); + olp::client::OlpClientSettings settings; + settings.network_request_handler = network_mock; + settings.cache = cache_mock; + + const auto apis = ApiDefaultResponses::GenerateResourceApisResponse(kCatalog); + const auto api_response = ResponseGenerator::ResourceApis(apis); + const std::string blob_key_url = + "https://tmp.blob.data.api.platform.here.com/blob/v1/catalogs/" + + kCatalog + "/layers/" + kLayerId + "/keys/" + + olp::utils::Url::Encode("1/1/2"); + + read::VersionedLayerClient client(kHrn, kLayerId, olp::porting::none, + settings); + { + SCOPED_TRACE("Get data by key"); + + EXPECT_CALL(*cache_mock, Get(_, _)) + .WillOnce(testing::Return(olp::porting::any())); + EXPECT_CALL(*cache_mock, Put(_, _, _, _)) + .WillRepeatedly(testing::Return(true)); + + EXPECT_CALL(*network_mock, Send(IsGetRequest(kUrlLookup), _, _, _, _)) + .WillOnce(ReturnHttpResponse(olp::http::NetworkResponse().WithStatus( + olp::http::HttpStatusCode::OK), + api_response)); + EXPECT_CALL(*network_mock, Send(IsGetRequest(blob_key_url), _, _, _, _)) + .WillOnce(ReturnHttpResponse(olp::http::NetworkResponse().WithStatus( + olp::http::HttpStatusCode::OK), + "keyData")); + + auto future = client.GetDataByKey(read::KeyDataRequest().WithKey("1/1/2")) + .GetFuture(); + const auto& response = future.get(); + ASSERT_TRUE(response.IsSuccessful()); + ASSERT_TRUE(response.GetResult() != nullptr); + ASSERT_EQ(7u, response.GetResult()->size()); + } + Mock::VerifyAndClearExpectations(network_mock.get()); + { + SCOPED_TRACE("Get data by not existing key"); + + EXPECT_CALL(*network_mock, Send(IsGetRequest(blob_key_url), _, _, _, _)) + .WillOnce(ReturnHttpResponse(olp::http::NetworkResponse().WithStatus( + olp::http::HttpStatusCode::NOT_FOUND), + "")); + + auto future = client.GetDataByKey(read::KeyDataRequest().WithKey("1/1/2")) + .GetFuture(); + const auto& response = future.get(); + ASSERT_FALSE(response.IsSuccessful()); + EXPECT_EQ(response.GetError().GetErrorCode(), ErrorCode::NotFound); + } + Mock::VerifyAndClearExpectations(network_mock.get()); + { + SCOPED_TRACE("Get data by key without the key"); + + auto future = client.GetDataByKey(read::KeyDataRequest()).GetFuture(); + const auto& response = future.get(); + ASSERT_FALSE(response.IsSuccessful()); + EXPECT_EQ(response.GetError().GetErrorCode(), ErrorCode::InvalidArgument); + } +} + TEST(VersionedLayerClientTest, DeleteFromCachePartition) { olp::client::OlpClientSettings settings; std::shared_ptr cache_mock = std::make_shared(); diff --git a/tests/functional/olp-cpp-sdk-dataservice-read/ApiTest.cpp b/tests/functional/olp-cpp-sdk-dataservice-read/ApiTest.cpp index 1f525fb21..d8e1cf99b 100644 --- a/tests/functional/olp-cpp-sdk-dataservice-read/ApiTest.cpp +++ b/tests/functional/olp-cpp-sdk-dataservice-read/ApiTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2024 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -287,6 +287,32 @@ TEST_F(ApiTest, GetBlob) { ASSERT_EQ("DT_2_0031", data_string); } +TEST_F(ApiTest, DISABLED_GetBlobByKey) { + olp::client::HRN hrn(GetTestCatalog()); + + auto client_response = olp::dataservice::read::ApiClientLookup::LookupApi( + hrn, {}, "blob", "v1", + olp::dataservice::read::FetchOptions::OnlineIfNotFound, *settings_); + + ASSERT_TRUE(client_response.IsSuccessful()) + << ApiErrorToString(client_response.GetError()); + auto blob_client = client_response.MoveResult(); + + olp::client::CancellationContext context; + + const auto start_time = std::chrono::high_resolution_clock::now(); + auto data_response = olp::dataservice::read::BlobApi::GetBlobByKey( + blob_client, "testlayer", "test-key", olp::porting::none, + olp::porting::none, context); + const auto end = std::chrono::high_resolution_clock::now(); + + std::chrono::duration time = end - start_time; + std::cout << "duration: " << time.count() * 1000000 << " us" << std::endl; + ASSERT_TRUE(data_response.IsSuccessful()) + << ApiErrorToString(data_response.GetError()); + ASSERT_LT(0, data_response.GetResult()->size()); +} + TEST_F(ApiTest, DISABLED_GetVolatileBlob) { olp::client::HRN hrn(GetTestCatalog()); diff --git a/tests/functional/olp-cpp-sdk-dataservice-read/DataserviceReadVersionedLayerClientTest.cpp b/tests/functional/olp-cpp-sdk-dataservice-read/DataserviceReadVersionedLayerClientTest.cpp index 4bb42d515..6d5fae9db 100644 --- a/tests/functional/olp-cpp-sdk-dataservice-read/DataserviceReadVersionedLayerClientTest.cpp +++ b/tests/functional/olp-cpp-sdk-dataservice-read/DataserviceReadVersionedLayerClientTest.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -26,8 +27,8 @@ #include #include #include -#include #include +#include #include #include #include "Utils.h" @@ -653,4 +654,27 @@ TEST_F(DataserviceReadVersionedLayerClientTest, GetTileEmptyField) { data_response_compressed.GetError().GetErrorCode()); } +TEST_F(DataserviceReadVersionedLayerClientTest, DISABLED_GetDataByKey) { + const auto catalog = + olp::client::HRN::FromString(CustomParameters::getArgument( + "dataservice_read_test_versioned_prefetch_catalog")); + const auto kLayerId = CustomParameters::getArgument( + "dataservice_read_test_versioned_prefetch_layer"); + + auto client = std::make_unique( + catalog, kLayerId, olp::porting::none, *settings_); + + const auto request = + olp::dataservice::read::KeyDataRequest().WithKey("1/1/2"); + + auto data_response = GetExecutionTime([&] { + auto future = client->GetDataByKey(request); + return future.GetFuture().get(); + }); + + EXPECT_SUCCESS(data_response); + ASSERT_TRUE(data_response.GetResult() != nullptr); + ASSERT_GT(data_response.GetResult()->size(), 0u); +} + } // namespace diff --git a/tests/functional/olp-cpp-sdk-dataservice-read/VersionedLayerClientPrefetchTest.cpp b/tests/functional/olp-cpp-sdk-dataservice-read/VersionedLayerClientPrefetchTest.cpp index 7c2010393..aa2e0a589 100644 --- a/tests/functional/olp-cpp-sdk-dataservice-read/VersionedLayerClientPrefetchTest.cpp +++ b/tests/functional/olp-cpp-sdk-dataservice-read/VersionedLayerClientPrefetchTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 HERE Europe B.V. + * Copyright (C) 2020-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ #include #include +#include #include #include "ApiDefaultResponses.h" #include "ReadDefaultResponses.h" diff --git a/tests/functional/olp-cpp-sdk-dataservice-read/VersionedLayerClientProtectTest.cpp b/tests/functional/olp-cpp-sdk-dataservice-read/VersionedLayerClientProtectTest.cpp index 88c4182ff..5c0afa740 100644 --- a/tests/functional/olp-cpp-sdk-dataservice-read/VersionedLayerClientProtectTest.cpp +++ b/tests/functional/olp-cpp-sdk-dataservice-read/VersionedLayerClientProtectTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2024 HERE Europe B.V. + * Copyright (C) 2020-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,8 +23,8 @@ #include #include #include +#include #include -#include #include "ApiDefaultResponses.h" #include "MockServerHelper.h" #include "ReadDefaultResponses.h" diff --git a/tests/integration/olp-cpp-sdk-dataservice-read/VersionedLayerClientTest.cpp b/tests/integration/olp-cpp-sdk-dataservice-read/VersionedLayerClientTest.cpp index 7f4637d89..500f695b0 100644 --- a/tests/integration/olp-cpp-sdk-dataservice-read/VersionedLayerClientTest.cpp +++ b/tests/integration/olp-cpp-sdk-dataservice-read/VersionedLayerClientTest.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include