diff --git a/olp-cpp-sdk-authentication/include/olp/authentication/AuthenticationClient.h b/olp-cpp-sdk-authentication/include/olp/authentication/AuthenticationClient.h index c527532e3..eec6a4fcf 100644 --- a/olp-cpp-sdk-authentication/include/olp/authentication/AuthenticationClient.h +++ b/olp-cpp-sdk-authentication/include/olp/authentication/AuthenticationClient.h @@ -83,6 +83,13 @@ class AUTHENTICATION_API AuthenticationClient { * than the default expiration time supported by the access token endpoint. */ std::chrono::seconds expires_in{0}; + + /** + * @brief (Optional) Custom body to be passed, if authentication service + * requires it. Fully overrides default body and resets the request content + * type. + */ + boost::optional custom_body{boost::none}; }; /** diff --git a/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.cpp b/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.cpp index eec62c650..55a0f0ab9 100644 --- a/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.cpp +++ b/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.cpp @@ -45,6 +45,7 @@ #include "olp/core/http/NetworkUtils.h" #include "olp/core/logging/Log.h" #include "olp/core/thread/TaskScheduler.h" +#include "olp/core/utils/Url.h" namespace olp { namespace authentication { @@ -53,7 +54,6 @@ namespace { using RequestBodyData = client::OlpClient::RequestBodyType::element_type; // Tags -constexpr auto kApplicationJson = "application/json"; const std::string kOauthEndpoint = "/oauth2/token"; const std::string kSignoutEndpoint = "/logout"; const std::string kTermsEndpoint = "/terms"; @@ -150,8 +150,16 @@ client::HttpResponse CallApi(const client::OlpClient& client, client::OlpClient::ParametersType headers{ {http::kAuthorizationHeader, auth_header}}; - return client.CallApi(endpoint, "POST", {}, std::move(headers), {}, - std::move(body), kApplicationJson, std::move(context)); + return client.CallApi( + endpoint, "POST", {}, std::move(headers), {}, std::move(body), + AuthenticationClientImpl::kApplicationJson, std::move(context)); +} + +std::string DeduceContentType(const SignInProperties& properties) { + if (properties.custom_body.has_value()) { + return ""; + } + return AuthenticationClientImpl::kApplicationJson; } } // namespace @@ -188,8 +196,16 @@ olp::client::HttpResponse AuthenticationClientImpl::CallAuth( const client::OlpClient& client, const std::string& endpoint, client::CancellationContext context, const AuthenticationCredentials& credentials, - client::OlpClient::RequestBodyType body, std::time_t timestamp) { - const auto url = settings_.token_endpoint_url + endpoint; + client::OlpClient::RequestBodyType body, std::time_t timestamp, + const std::string& content_type) { + // When credentials specify authentication endpoint, it means that + // Authorization header must be created for the corresponding host. + const auto url = [&]() { + if (!credentials.GetEndpointUrl().empty()) { + return credentials.GetEndpointUrl(); + } + return settings_.token_endpoint_url + endpoint; + }(); auto auth_header = GenerateAuthorizationHeader(credentials, url, timestamp, GenerateUid()); @@ -198,7 +214,7 @@ olp::client::HttpResponse AuthenticationClientImpl::CallAuth( {http::kAuthorizationHeader, std::move(auth_header)}}; return client.CallApi(endpoint, "POST", {}, std::move(headers), {}, - std::move(body), kApplicationJson, std::move(context)); + std::move(body), content_type, std::move(context)); } SignInResult AuthenticationClientImpl::ParseAuthResponse( @@ -303,11 +319,31 @@ client::CancellationToken AuthenticationClientImpl::SignInClient( return client::ApiError::Cancelled(); } - auto client = CreateOlpClient(settings_, boost::none, false); + auto olp_client_host = settings_.token_endpoint_url; + auto endpoint = kOauthEndpoint; + // If credentials contain URL for the token endpoint then override default + // endpoint with it. Construction of the `OlpClient` requires the host part + // of URL, while `CallAuth` method - the rest of URL, hence we need to split + // URL passed in the Credentials object. + const auto credentials_endpoint = credentials.GetEndpointUrl(); + const auto maybe_host_and_rest = + olp::utils::Url::ParseHostAndRest(credentials_endpoint); + if (maybe_host_and_rest.has_value()) { + const auto& host_and_rest = maybe_host_and_rest.value(); + olp_client_host = host_and_rest.first; + endpoint = host_and_rest.second; + } + + // To pass correct URL we need to create and modify local copy of shared + // settings object. + auto settings = settings_; + settings.token_endpoint_url = olp_client_host; + auto client = CreateOlpClient(settings, boost::none, false); RequestTimer timer = CreateRequestTimer(client, context); const auto request_body = GenerateClientBody(properties); + const auto content_type = DeduceContentType(properties); SignInClientResponse response; @@ -319,8 +355,8 @@ client::CancellationToken AuthenticationClientImpl::SignInClient( } auto auth_response = - CallAuth(client, kOauthEndpoint, context, credentials, request_body, - timer.GetRequestTime()); + CallAuth(client, endpoint, context, credentials, request_body, + timer.GetRequestTime(), content_type); const auto status = auth_response.GetStatus(); if (status < 0) { @@ -778,6 +814,11 @@ client::CancellationToken AuthenticationClientImpl::GetMyAccount( client::OlpClient::RequestBodyType AuthenticationClientImpl::GenerateClientBody( const SignInProperties& properties) { + if (properties.custom_body.has_value()) { + const auto& content = properties.custom_body.value(); + return std::make_shared(content.data(), + content.data() + content.size()); + }; rapidjson::StringBuffer data; rapidjson::Writer writer(data); writer.StartObject(); diff --git a/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.h b/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.h index 3941263f4..1473f6bfd 100644 --- a/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.h +++ b/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.h @@ -68,6 +68,8 @@ class AuthenticationClientImpl { using SignInUserCacheType = thread::Atomic>; + static constexpr auto kApplicationJson = "application/json"; + explicit AuthenticationClientImpl(AuthenticationSettings settings); virtual ~AuthenticationClientImpl(); @@ -162,7 +164,8 @@ class AuthenticationClientImpl { const client::OlpClient& client, const std::string& endpoint, client::CancellationContext context, const AuthenticationCredentials& credentials, - client::OlpClient::RequestBodyType body, std::time_t timestamp); + client::OlpClient::RequestBodyType body, std::time_t timestamp, + const std::string& content_type = kApplicationJson); SignInResult ParseAuthResponse(int status, std::stringstream& auth_response); diff --git a/olp-cpp-sdk-authentication/src/SignInResultImpl.cpp b/olp-cpp-sdk-authentication/src/SignInResultImpl.cpp index da162ab2c..89920e6ae 100644 --- a/olp-cpp-sdk-authentication/src/SignInResultImpl.cpp +++ b/olp-cpp-sdk-authentication/src/SignInResultImpl.cpp @@ -22,14 +22,61 @@ #include "Constants.h" #include "olp/core/http/HttpStatusCode.h" +namespace olp { +namespace authentication { + namespace { constexpr auto kTokenType = "tokenType"; constexpr auto kUserId = "userId"; constexpr auto kScope = "scope"; -} // namespace +constexpr auto kTokenTypeSnakeCase = "token_type"; +constexpr auto kAccessTokenSnakeCase = "access_token"; +constexpr auto kExpiresInSnakeCase = "expires_in"; -namespace olp { -namespace authentication { +bool HasAccessToken(const rapidjson::Document& document) { + return document.HasMember(Constants::ACCESS_TOKEN) || + document.HasMember(kAccessTokenSnakeCase); +} + +std::string ParseAccessToken(const rapidjson::Document& document) { + if (document.HasMember(Constants::ACCESS_TOKEN)) { + return document[Constants::ACCESS_TOKEN].GetString(); + } + + return document[kAccessTokenSnakeCase].GetString(); +} + +bool HasExpiresIn(const rapidjson::Document& document) { + return document.HasMember(Constants::EXPIRES_IN) || + document.HasMember(kExpiresInSnakeCase); +} + +unsigned ParseExpiresIn(const rapidjson::Document& document) { + if (document.HasMember(Constants::EXPIRES_IN)) { + return document[Constants::EXPIRES_IN].GetUint(); + } + return document[kExpiresInSnakeCase].GetUint(); +} + +bool HasTokenType(const rapidjson::Document& document) { + return document.HasMember(kTokenType) || + document.HasMember(kTokenTypeSnakeCase); +} + +std::string ParseTokenType(const rapidjson::Document& document) { + if (document.HasMember(kTokenType)) { + return document[kTokenType].GetString(); + } + + return document[kTokenTypeSnakeCase].GetString(); +} + +bool IsDocumentValid(const rapidjson::Document& document) { + return HasAccessToken(document) && HasExpiresIn(document) && + HasTokenType(document); +} + +} // namespace SignInResultImpl::SignInResultImpl() noexcept : SignInResultImpl(http::HttpStatusCode::SERVICE_UNAVAILABLE, @@ -41,10 +88,7 @@ SignInResultImpl::SignInResultImpl( : BaseResult(status, std::move(error), json_document), expiry_time_(), expires_in_() { - is_valid_ = this->BaseResult::IsValid() && - json_document->HasMember(Constants::ACCESS_TOKEN) && - json_document->HasMember(kTokenType) && - json_document->HasMember(Constants::EXPIRES_IN); + is_valid_ = this->BaseResult::IsValid() && IsDocumentValid(*json_document); // Extra response data if no errors reported if (!HasError()) { @@ -52,17 +96,16 @@ SignInResultImpl::SignInResultImpl( status_ = http::HttpStatusCode::SERVICE_UNAVAILABLE; error_.message = Constants::ERROR_HTTP_SERVICE_UNAVAILABLE; } else { - if (json_document->HasMember(Constants::ACCESS_TOKEN)) - access_token_ = (*json_document)[Constants::ACCESS_TOKEN].GetString(); - if (json_document->HasMember(kTokenType)) - token_type_ = (*json_document)[kTokenType].GetString(); + if (HasAccessToken(*json_document)) + access_token_ = ParseAccessToken(*json_document); + if (HasTokenType(*json_document)) + token_type_ = ParseTokenType(*json_document); if (json_document->HasMember(Constants::REFRESH_TOKEN)) refresh_token_ = (*json_document)[Constants::REFRESH_TOKEN].GetString(); - if (json_document->HasMember(Constants::EXPIRES_IN)) { - expiry_time_ = std::time(nullptr) + - (*json_document)[Constants::EXPIRES_IN].GetUint(); - expires_in_ = std::chrono::seconds( - (*json_document)[Constants::EXPIRES_IN].GetUint()); + if (HasExpiresIn(*json_document)) { + const auto expires_in = ParseExpiresIn(*json_document); + expiry_time_ = std::time(nullptr) + expires_in; + expires_in_ = std::chrono::seconds(expires_in); } if (json_document->HasMember(kUserId)) user_identifier_ = (*json_document)[kUserId].GetString(); diff --git a/olp-cpp-sdk-authentication/tests/AuthenticationClientTest.cpp b/olp-cpp-sdk-authentication/tests/AuthenticationClientTest.cpp index db3a3f43f..c2e529c90 100644 --- a/olp-cpp-sdk-authentication/tests/AuthenticationClientTest.cpp +++ b/olp-cpp-sdk-authentication/tests/AuthenticationClientTest.cpp @@ -51,8 +51,20 @@ class AuthenticationClientImplTestable : public auth::AuthenticationClientImpl { (const client::OlpClient&, const std::string&, client::CancellationContext, const auth::AuthenticationCredentials&, - client::OlpClient::RequestBodyType, std::time_t), + client::OlpClient::RequestBodyType, std::time_t, + const std::string&), (override)); + + client::HttpResponse RealCallAuth( + const client::OlpClient& client, const std::string& endpoint, + client::CancellationContext context, + const auth::AuthenticationCredentials& credentials, + client::OlpClient::RequestBodyType body, std::time_t time, + const std::string& content_type) { + return auth::AuthenticationClientImpl::CallAuth( + client, endpoint, std::move(context), credentials, std::move(body), + time, content_type); + } }; ACTION_P(Wait, time) { std::this_thread::sleep_for(time); } @@ -168,7 +180,7 @@ TEST(AuthenticationClientTest, Timestamp) { std::time_t time = 0; - EXPECT_CALL(auth_impl, CallAuth(_, _, _, _, _, timestamp_predicate)) + EXPECT_CALL(auth_impl, CallAuth(_, _, _, _, _, timestamp_predicate, _)) .Times(3) .WillRepeatedly(testing::DoAll(testing::SaveArg<5>(&time), Wait(request_time), @@ -186,7 +198,7 @@ TEST(AuthenticationClientTest, Timestamp) { std::time_t time = 0; - EXPECT_CALL(auth_impl, CallAuth(_, _, _, _, _, timestamp_predicate)) + EXPECT_CALL(auth_impl, CallAuth(_, _, _, _, _, timestamp_predicate, _)) .Times(3) .WillRepeatedly(testing::DoAll(testing::SaveArg<5>(&time), Wait(request_time), @@ -204,7 +216,7 @@ TEST(AuthenticationClientTest, Timestamp) { std::time_t time = 0; - EXPECT_CALL(auth_impl, CallAuth(_, _, _, _, _, timestamp_predicate)) + EXPECT_CALL(auth_impl, CallAuth(_, _, _, _, _, timestamp_predicate, _)) .Times(3) .WillRepeatedly(testing::DoAll(testing::SaveArg<5>(&time), Wait(request_time), @@ -222,7 +234,7 @@ TEST(AuthenticationClientTest, Timestamp) { std::time_t time = 0; - EXPECT_CALL(auth_impl, CallAuth(_, _, _, _, _, timestamp_predicate)) + EXPECT_CALL(auth_impl, CallAuth(_, _, _, _, _, timestamp_predicate, _)) .Times(3) .WillRepeatedly(testing::DoAll(testing::SaveArg<5>(&time), Wait(request_time), @@ -263,3 +275,59 @@ TEST(AuthenticationClientTest, GenerateAuthorizationHeader) { "3D\""; EXPECT_EQ(sig, expected_sig); } + +TEST(AuthenticationClientTest, SignInWithCustomUrlAndBody) { + // Making CPPLINT happy + using testing::_; + using testing::Contains; + using testing::DoAll; + using testing::ElementsAreArray; + using testing::Not; + using testing::Pair; + using testing::Return; + using testing::SaveArg; + + using std::placeholders::_1; + using std::placeholders::_2; + using std::placeholders::_3; + using std::placeholders::_4; + using std::placeholders::_5; + using std::placeholders::_6; + using std::placeholders::_7; + + constexpr auto custom_url = "https://example.com/user/login"; + const auto custom_body = std::string("custom_body"); + olp::http::NetworkRequest expected_request{""}; + + const auth::AuthenticationCredentials credentials("", "", custom_url); + auth::SignInProperties properties; + properties.custom_body = custom_body; + + auth::AuthenticationSettings settings; + auto network_mock = std::make_shared(); + settings.network_request_handler = network_mock; + + AuthenticationClientImplTestable auth_impl(settings); + + EXPECT_CALL(*network_mock, Send) + .WillOnce(DoAll( + SaveArg<0>(&expected_request), + Return(olp::http::SendOutcome(olp::http::ErrorCode::UNKNOWN_ERROR)))); + + EXPECT_CALL(auth_impl, CallAuth) + .WillOnce(std::bind(&AuthenticationClientImplTestable::RealCallAuth, + &auth_impl, _1, _2, _3, _4, _5, _6, _7)); + + auth_impl.SignInClient( + credentials, properties, + [=](const auth::AuthenticationClient::SignInClientResponse& response) { + EXPECT_FALSE(response.IsSuccessful()); + EXPECT_EQ(response.GetError().GetErrorCode(), + client::ErrorCode::Unknown); + }); + + EXPECT_EQ(expected_request.GetUrl(), custom_url); + EXPECT_THAT(*expected_request.GetBody(), ElementsAreArray(custom_body)); + EXPECT_THAT(expected_request.GetHeaders(), + Not(Contains(Pair("Content-Type", _)))); +} diff --git a/olp-cpp-sdk-authentication/tests/CMakeLists.txt b/olp-cpp-sdk-authentication/tests/CMakeLists.txt index 62d726b0d..e8d973f40 100644 --- a/olp-cpp-sdk-authentication/tests/CMakeLists.txt +++ b/olp-cpp-sdk-authentication/tests/CMakeLists.txt @@ -20,6 +20,7 @@ set(OLP_AUTHENTICATION_TEST_SOURCES AuthenticationClientTest.cpp DecisionApiClientTest.cpp CryptoTest.cpp + SignInResultImplTest.cpp ) if (ANDROID OR IOS) diff --git a/olp-cpp-sdk-authentication/tests/SignInResultImplTest.cpp b/olp-cpp-sdk-authentication/tests/SignInResultImplTest.cpp index 13d5e4b59..c6881b769 100644 --- a/olp-cpp-sdk-authentication/tests/SignInResultImplTest.cpp +++ b/olp-cpp-sdk-authentication/tests/SignInResultImplTest.cpp @@ -22,10 +22,8 @@ #include "../src/SignInResultImpl.h" namespace { -constexpr auto kClientId = "4GaUh8X9CpOax4CCC3of"; -constexpr auto kTokenResponse = - "{\"accessToken\":" - "\"eyJhbGciOiJSUzUxMiIsImN0eSI6IkpXVCIsImlzcyI6IkhFUkUiLCJhaWQiOiI0R2FVaDhY" +constexpr auto kToken = + "eyJhbGciOiJSUzUxMiIsImN0eSI6IkpXVCIsImlzcyI6IkhFUkUiLCJhaWQiOiI0R2FVaDhY" "OUNwT2F4NENDQzNvZiIsImlhdCI6MTU3ODMxMDExNCwiZXhwIjoxNTc4MzEzNzE0LCJraWQiOi" "JqMSJ9." "ZXlKaGJHY2lPaUprYVhJaUxDSmxibU1pT2lKQk1qVTJRMEpETFVoVE5URXlJbjAuLnFMb0Jmd0" @@ -38,11 +36,12 @@ constexpr auto kTokenResponse = "3xIlw0WjEAEfI6hOZnx89FdkKGXuUp7cOPaM-b133mFsE-" "S5uuuQ3vYnztKcfmEwc8SSzgCiyRTuUYUE_ESZdpwQi1jx1rbag7fBFqIIGNSVN_" "d6Scpg2s6ybHftoXajMVydnvmG4Iyls8eOeCcLDYMUPcgy05Uqc5CMvGSsL8WUOJAoLWw3eF_" - "ik1Xyv4iHRz3-67EAzzdwtL6nPbN8c0S16leVshnfdUZA\",\"tokenType\":\"bearer\"," - "\"expiresIn\":3599}"; -constexpr auto kInvalidTokenResponse = + "ik1Xyv4iHRz3-67EAzzdwtL6nPbN8c0S16leVshnfdUZA"; +constexpr auto kTokenType = "bearer"; +constexpr auto kExpiresIn = std::chrono::seconds(3599); +constexpr auto kTokenResponse = "{\"accessToken\":" - "\"eyJhbGciOiJSUzUxbx5sImN0eSI6IkpXVCIsImlzcyI6IkhFUkUiLCJhaWQiOiI0R2FVaDhY" + "\"eyJhbGciOiJSUzUxMiIsImN0eSI6IkpXVCIsImlzcyI6IkhFUkUiLCJhaWQiOiI0R2FVaDhY" "OUNwT2F4NENDQzNvZiIsImlhdCI6MTU3ODMxMDExNCwiZXhwIjoxNTc4MzEzNzE0LCJraWQiOi" "JqMSJ9." "ZXlKaGJHY2lPaUprYVhJaUxDSmxibU1pT2lKQk1qVTJRMEpETFVoVE5URXlJbjAuLnFMb0Jmd0" @@ -57,26 +56,11 @@ constexpr auto kInvalidTokenResponse = "d6Scpg2s6ybHftoXajMVydnvmG4Iyls8eOeCcLDYMUPcgy05Uqc5CMvGSsL8WUOJAoLWw3eF_" "ik1Xyv4iHRz3-67EAzzdwtL6nPbN8c0S16leVshnfdUZA\",\"tokenType\":\"bearer\"," "\"expiresIn\":3599}"; -constexpr auto kNoIdFieldInToken = - "{\"accessToken\":" - "\"eyJhbGciOiJSUzUxMiIsImN0eSI6IkpXVCIsImlzcyI6IkhFUkUiLCJhaWQiOjMsImlhdCI6" - "MTU3ODMxMDExNCwiZXhwIjoxNTc4MzEzNzE0LCJraWQiOiJqMSJ9." - "ZXlKaGJHY2lPaUprYVhJaUxDSmxibU1pT2lKQk1qVTJRMEpETFVoVE5URXlJbjAuLnFMb0Jmd0" - "1rSVktRHhuRFA1Ym9kYlEubmdZZkJIUFBsNXVBazZvY0w5djVTa3R4QU4xalE4RENjTzBiYnJO" - "Y2FuUkdKWFRvaU8tRXRuNGFNZE44OTBXaDR5QUlKaWI4LTUzY3lVMWVDZF9iX0ZCRmYxWEFsVU" - "ZkU1lFdUtFWHpoVUVVVnB0ZE1QdlY2VnVEVWktbTJoYWQ3ZEd4YUtXdVNoVVJNVFpOa1NHZy13" - "Lm9wdHJQUzRYQi1CNnBxdTZxa1Y4a09WQlNLbXFLUVhBODRMUV9IS0RZTms." - "UGyfwSjA8g6gDfLY4sCEu4fkrgp8WNm-uq5F2KLJ-zcDJIwoPKNrJjd2FEZdllG8-" - "tbbVWx5fwxn9qV6vLbm5BOMdvX3eMw_FQgsEPQAX3cOyv3he-" - "3xIlw0WjEAEfI6hOZnx89FdkKGXuUp7cOPaM-b133mFsE-" - "S5uuuQ3vYnztKcfmEwc8SSzgCiyRTuUYUE_ESZdpwQi1jx1rbag7fBFqIIGNSVN_" - "d6Scpg2s6ybHftoXajMVydnvmG4Iyls8eOeCcLDYMUPcgy05Uqc5CMvGSsL8WUOJAoLWw3eF_" - "ik1Xyv4iHRz3-67EAzzdwtL6nPbN8c0S16leVshnfdUZA\",\"tokenType\":\"bearer\"," - "\"expiresIn\":3599}"; -constexpr auto kWrongIdFieldInToken = - "{\"accessToken\":" - "\"eyJhbGciOiJSUzUxMiIsImN0eSI6IkpXVCIsImlzcyI6IkhFUkUiLCJhaWQiOjMsImlhdCI6" - "MTU3ODMxMDExNCwiZXhwIjoxNTc4MzEzNzE0LCJraWQiOiJqMSJ9." +constexpr auto kTokenResponseSnakeCase = + "{\"access_token\":" + "\"eyJhbGciOiJSUzUxMiIsImN0eSI6IkpXVCIsImlzcyI6IkhFUkUiLCJhaWQiOiI0R2FVaDhY" + "OUNwT2F4NENDQzNvZiIsImlhdCI6MTU3ODMxMDExNCwiZXhwIjoxNTc4MzEzNzE0LCJraWQiOi" + "JqMSJ9." "ZXlKaGJHY2lPaUprYVhJaUxDSmxibU1pT2lKQk1qVTJRMEpETFVoVE5URXlJbjAuLnFMb0Jmd0" "1rSVktRHhuRFA1Ym9kYlEubmdZZkJIUFBsNXVBazZvY0w5djVTa3R4QU4xalE4RENjTzBiYnJO" "Y2FuUkdKWFRvaU8tRXRuNGFNZE44OTBXaDR5QUlKaWI4LTUzY3lVMWVDZF9iX0ZCRmYxWEFsVU" @@ -87,51 +71,50 @@ constexpr auto kWrongIdFieldInToken = "3xIlw0WjEAEfI6hOZnx89FdkKGXuUp7cOPaM-b133mFsE-" "S5uuuQ3vYnztKcfmEwc8SSzgCiyRTuUYUE_ESZdpwQi1jx1rbag7fBFqIIGNSVN_" "d6Scpg2s6ybHftoXajMVydnvmG4Iyls8eOeCcLDYMUPcgy05Uqc5CMvGSsL8WUOJAoLWw3eF_" - "ik1Xyv4iHRz3-67EAzzdwtL6nPbN8c0S16leVshnfdUZA\",\"tokenType\":\"bearer\"," - "\"expiresIn\":3599}"; -constexpr auto kWrongFormatToken = - "{\"accessToken\":" + "ik1Xyv4iHRz3-67EAzzdwtL6nPbN8c0S16leVshnfdUZA\",\"token_type\":\"bearer\"," + "\"expires_in\":3599}"; +constexpr auto kWrongFormatResponse = + "{\"access_Token\":" "\"eyJhbGciOiJSUzUxMiIsImN0eSI6IkpXVCIsImlzcyI6IkhFUkUiLCJhaWQiOiI0R2FVaDhY" "OUNwT2F4NENDQzNvZiIsImlhdCI6MTU3ODMxMDExNCwiZXhwIjoxNTc4MzEzNzE0LCJraWQiOi" "JqMSJ9\",\"tokenType\":\"bearer\",\"expiresIn\":3599}"; -TEST(SignInResultImplTest, ValidToken) { - auto doc = std::make_shared(); - doc->Parse(kTokenResponse); - olp::authentication::SignInResultImpl result(olp::http::HttpStatusCode::OK, - std::string(), doc); - EXPECT_EQ(result.GetClientId(), kClientId); -} +TEST(SignInResultImplTest, Constructor) { + { + SCOPED_TRACE("Valid token"); + auto doc = std::make_shared(); + doc->Parse(kTokenResponse); + olp::authentication::SignInResultImpl result(olp::http::HttpStatusCode::OK, + std::string(), doc); -TEST(SignInResultImplTest, WrongIdField) { - auto doc = std::make_shared(); - doc->Parse(kWrongIdFieldInToken); - olp::authentication::SignInResultImpl result(olp::http::HttpStatusCode::OK, - std::string(), doc); - EXPECT_TRUE(result.GetClientId().empty()); -} + ASSERT_TRUE(result.IsValid()); + EXPECT_EQ(result.GetAccessToken(), kToken); + EXPECT_EQ(result.GetTokenType(), kTokenType); + EXPECT_EQ(result.GetExpiresIn(), kExpiresIn); + } -TEST(SignInResultImplTest, NoIdField) { - auto doc = std::make_shared(); - doc->Parse(kNoIdFieldInToken); - olp::authentication::SignInResultImpl result(olp::http::HttpStatusCode::OK, - std::string(), doc); - EXPECT_TRUE(result.GetClientId().empty()); -} + { + SCOPED_TRACE("Valid token from snake case response"); + auto doc = std::make_shared(); + doc->Parse(kTokenResponseSnakeCase); + olp::authentication::SignInResultImpl result(olp::http::HttpStatusCode::OK, + std::string(), doc); -TEST(SignInResultImplTest, NotSupportedFormat) { - auto doc = std::make_shared(); - doc->Parse(kWrongFormatToken); - olp::authentication::SignInResultImpl result(olp::http::HttpStatusCode::OK, - std::string(), doc); - EXPECT_TRUE(result.GetClientId().empty()); -} + ASSERT_TRUE(result.IsValid()); + EXPECT_EQ(result.GetAccessToken(), kToken); + EXPECT_EQ(result.GetTokenType(), kTokenType); + EXPECT_EQ(result.GetExpiresIn(), kExpiresIn); + } -TEST(SignInResultImplTest, InvalidToken) { - auto doc = std::make_shared(); - doc->Parse(kInvalidTokenResponse); - olp::authentication::SignInResultImpl result(olp::http::HttpStatusCode::OK, - std::string(), doc); - EXPECT_TRUE(result.GetClientId().empty()); + { + SCOPED_TRACE("Bad response"); + auto doc = std::make_shared(); + doc->Parse(kWrongFormatResponse); + olp::authentication::SignInResultImpl result(olp::http::HttpStatusCode::OK, + std::string(), doc); + + ASSERT_FALSE(result.IsValid()); + } } + } // namespace diff --git a/olp-cpp-sdk-core/include/olp/core/utils/Url.h b/olp-cpp-sdk-core/include/olp/core/utils/Url.h index 65df1d1f5..89de556e6 100644 --- a/olp-cpp-sdk-core/include/olp/core/utils/Url.h +++ b/olp-cpp-sdk-core/include/olp/core/utils/Url.h @@ -25,6 +25,7 @@ #include #include +#include namespace olp { namespace utils { @@ -65,6 +66,22 @@ class CORE_API Url { static std::string Construct( const std::string& base, const std::string& path, const std::multimap& query_params); + + /** + * @brief Represents the host part and rest of full URL. + */ + using HostAndRest = std::pair; + + /** + * @brief Separates full URL to the host part the rest. Helps to split URL + * from credentials to parts passed to the OlpClient and NetworkRequest. + * + * @param url Full URL. + * + * @return An optional pair representing host part and the rest of URL. + * Returns boost::none when url cannot be split. + */ + static boost::optional ParseHostAndRest(const std::string& url); }; } // namespace utils diff --git a/olp-cpp-sdk-core/src/utils/Url.cpp b/olp-cpp-sdk-core/src/utils/Url.cpp index a78628b71..698c0f099 100644 --- a/olp-cpp-sdk-core/src/utils/Url.cpp +++ b/olp-cpp-sdk-core/src/utils/Url.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -124,5 +125,20 @@ std::string Url::Construct( return url_ss.str(); } +boost::optional Url::ParseHostAndRest( + const std::string& url) { + const auto host_and_rest_regex = + R"(^(.*:\/\/[A-Za-z0-9\-\.]+(:[0-9]+)?)(.*)$)"; + std::regex regex{host_and_rest_regex}; + std::smatch match; + if (!std::regex_search(url, match, regex) || match.size() != 4) { + return boost::none; + } + + const auto host = std::string{match[1]}; + const auto rest = std::string{match[3]}; + return std::make_pair(host, rest); +} + } // namespace utils } // namespace olp diff --git a/olp-cpp-sdk-core/tests/CMakeLists.txt b/olp-cpp-sdk-core/tests/CMakeLists.txt index 447ab7c38..14a4c60ad 100644 --- a/olp-cpp-sdk-core/tests/CMakeLists.txt +++ b/olp-cpp-sdk-core/tests/CMakeLists.txt @@ -72,6 +72,7 @@ set(OLP_CPP_SDK_CORE_TESTS_SOURCES ./http/NetworkUtils.cpp ./utils/UtilsTest.cpp + ./utils/UrlTest.cpp ) if (ANDROID OR IOS) diff --git a/olp-cpp-sdk-core/tests/utils/UrlTest.cpp b/olp-cpp-sdk-core/tests/utils/UrlTest.cpp new file mode 100644 index 000000000..ba3b86d6c --- /dev/null +++ b/olp-cpp-sdk-core/tests/utils/UrlTest.cpp @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025 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 + +namespace { + +using UrlTest = testing::Test; + +TEST(UrlTest, ParseHostAndRest) { + { + SCOPED_TRACE("Bad Url"); + EXPECT_FALSE(olp::utils::Url::ParseHostAndRest("bad url").has_value()); + } + + { + SCOPED_TRACE("Good Url"); + const auto result = olp::utils::Url::ParseHostAndRest( + "https://account.api.here.com/oauth2/token"); + ASSERT_TRUE(result.has_value()); + const auto& host = result.value().first; + const auto& rest = result.value().second; + EXPECT_EQ(host, "https://account.api.here.com"); + EXPECT_EQ(rest, "/oauth2/token"); + } + + { + SCOPED_TRACE("Good Url with port"); + const auto result = olp::utils::Url::ParseHostAndRest( + "https://account.api.here.com:8080/oauth2/token"); + ASSERT_TRUE(result.has_value()); + const auto& host = result.value().first; + const auto& rest = result.value().second; + EXPECT_EQ(host, "https://account.api.here.com:8080"); + EXPECT_EQ(rest, "/oauth2/token"); + } +} +} // namespace