From 1fbfd13f5d7220122b2a127627cc9105013b4893 Mon Sep 17 00:00:00 2001 From: Aotrix <9aotrix@protonmail.com> Date: Wed, 29 Jul 2026 18:31:43 +0200 Subject: Add tests for HearthstoneTokenBuilder --- battleNetApi/include/hearthstoneTokenBuilder.hpp | 20 ++-- battleNetApi/include/token.hpp | 16 ++- battleNetApi/include/tokenBuilder.hpp | 2 +- battleNetApi/src/hearthstoneTokenBuilder.cpp | 122 ++++++++++++--------- battleNetApi/src/token.cpp | 24 +++- battleNetApi/tests/CMakeLists.txt | 2 + .../tests/resources/credentials_expired_token.json | 10 ++ battleNetApi/tests/src/tests.cpp | 4 +- battleNetApi/tests/src/tokenTests.cpp | 59 ++++++++-- 9 files changed, 180 insertions(+), 79 deletions(-) create mode 100644 battleNetApi/tests/resources/credentials_expired_token.json diff --git a/battleNetApi/include/hearthstoneTokenBuilder.hpp b/battleNetApi/include/hearthstoneTokenBuilder.hpp index e946fe8..571554f 100644 --- a/battleNetApi/include/hearthstoneTokenBuilder.hpp +++ b/battleNetApi/include/hearthstoneTokenBuilder.hpp @@ -6,29 +6,32 @@ #include #include +namespace bnet { using nlohmann::json; static constexpr std::uint64_t kTOKEN_CONTENT_MIN_LENGTH{10}; class HearthstoneTokenBuilder : TokenBuilder { private: - std::shared_ptr mCurrentToken; - bool mUpdateSourceFile{false}; + std::shared_ptr _currentToken; + bool _allowBattleNetApiCall{false}; + bool _allowUpdateSourceFile{false}; + bool _allowExpiredToken{false}; std::vector _credentialJsonPaths; [[nodiscard]] bool isUpdateSourceFileAllowed() const; [[nodiscard]] - const bool hasExpired(json iTokenJson) const; + bool isTokenValid(const json &iTokenJson) const; static size_t curlCallbackStringOutput(void *iPtr, size_t iSize, size_t iNmemb, std::string *iUserData); [[nodiscard]] std::chrono::seconds getCurrentTime() const; [[nodiscard]] - std::pair - getTokenWithApi(const std::string &iAgentJsonFilePath) const; + Token getTokenWithApi(const std::string &iCredentialsJsonFilePath) const; [[nodiscard]] - std::string getTokenFromLocalFile() const; - void writeTokenToLocalFile(const std::pair &iToken) const; + Token getTokenWithApi(const json &iCredentials) const; + void writeTokenToLocalFile(std::shared_ptr &iToken, + const std::string &iTokenFileOutputPath) const; public: HearthstoneTokenBuilder(const std::span &iJsonPaths); @@ -40,7 +43,9 @@ class HearthstoneTokenBuilder : TokenBuilder { HearthstoneTokenBuilder &addCredentialJsonFiles(CredentialJsonPaths... iCredentialJsonPaths) { (_credentialJsonPaths.push_back(iCredentialJsonPaths), ...); } + HearthstoneTokenBuilder &allowBattleNetApiCall(); HearthstoneTokenBuilder &allowUpdateSourceFile(); + HearthstoneTokenBuilder &allowExpiredToken(); /** * @return Create an access token to perform API requests. * First, the cache files are checked. (Which ones?) @@ -51,3 +56,4 @@ class HearthstoneTokenBuilder : TokenBuilder { [[nodiscard]] std::shared_ptr getToken() override; }; +} // namespace bnet diff --git a/battleNetApi/include/token.hpp b/battleNetApi/include/token.hpp index 4a66c53..f943b81 100644 --- a/battleNetApi/include/token.hpp +++ b/battleNetApi/include/token.hpp @@ -3,16 +3,21 @@ #include #include +namespace bnet { +using TokenText = std::string; + class Token { public: - explicit Token(const std::string &iTokenText, const std::chrono::seconds &iLifetime); - std::string text() const; - void setText(const std::string &iTokenText); - void setLifetime(const std::chrono::seconds &iLifetime); + explicit Token(); + std::optional text() const; + std::optional expirationTime() const; + Token &setText(const TokenText &iTokenText); + Token &setLifetime(const std::chrono::seconds &iLifetime); + Token &setExpirationTime(const std::chrono::seconds &iExpirationTime); bool isExpired(); private: - std::string _tokenText; + std::optional _tokenText; std::chrono::seconds _expirationTime; }; @@ -22,3 +27,4 @@ auto currentTime = [] { std::chrono::steady_clock::now().time_since_epoch()); }; } // namespace +} // namespace bnet diff --git a/battleNetApi/include/tokenBuilder.hpp b/battleNetApi/include/tokenBuilder.hpp index 2893728..5af4791 100644 --- a/battleNetApi/include/tokenBuilder.hpp +++ b/battleNetApi/include/tokenBuilder.hpp @@ -8,5 +8,5 @@ class TokenBuilder { TokenBuilder() = default; virtual ~TokenBuilder() = default; virtual TokenBuilder &buildToken() = 0; - virtual std::shared_ptr getToken() = 0; + virtual std::shared_ptr getToken() = 0; }; diff --git a/battleNetApi/src/hearthstoneTokenBuilder.cpp b/battleNetApi/src/hearthstoneTokenBuilder.cpp index 89466a5..668aaa5 100644 --- a/battleNetApi/src/hearthstoneTokenBuilder.cpp +++ b/battleNetApi/src/hearthstoneTokenBuilder.cpp @@ -1,46 +1,69 @@ -#include #include #include #include #include #include +namespace bnet { HearthstoneTokenBuilder::HearthstoneTokenBuilder(const std::span &iJsonPaths) { _credentialJsonPaths.append_range(iJsonPaths); } HearthstoneTokenBuilder &HearthstoneTokenBuilder::buildToken() { - std::ranges::find_if(_credentialJsonPaths, - [](const std::string &iCredentialJsonPath) { return true; }); - if (hasExpired()) { - if (isUpdateSourceFileAllowed()) { + for (const std::string &aCredentialJsonPath : _credentialJsonPaths) { + std::ifstream aCredentialJsonFile{aCredentialJsonPath}; + json aCredentialJson = json::parse(aCredentialJsonFile); + if (isTokenValid(aCredentialJson)) { + _currentToken = std::make_shared(); + _currentToken->setText(aCredentialJson["Token"]["Content"]); + _currentToken->setExpirationTime( + std::chrono::seconds(aCredentialJson["Token"]["ExpirationTime"])); + return *this; } - const std::pair kTokenContent{getTokenWithApi()}; - writeTokenToLocalFile(kTokenContent); } - // return getTokenFromLocalFile(); - if (aTokenContainer.empty()) { - return std::nullopt; + + if (!_allowBattleNetApiCall) { + return *this; + } + + for (const std::string &aCredentialJsonPath : _credentialJsonPaths) { + std::ifstream aCredentialJsonFile{aCredentialJsonPath}; + json aCredentialJson = json::parse(aCredentialJsonFile); + _currentToken = std::make_shared(getTokenWithApi(aCredentialJson)); + if (_currentToken) { + if (_allowUpdateSourceFile) { + writeTokenToLocalFile(_currentToken, aCredentialJsonPath); + } + break; + } } - Token aOutputToken{aTokenContainer.front()}; - aTokenContainer.pop(); - // mCurrentToken = std::make_unique(...); return *this; } -std::shared_ptr HearthstoneTokenBuilder::getToken() { return mCurrentToken; } +std::shared_ptr HearthstoneTokenBuilder::getToken() { return _currentToken; } +HearthstoneTokenBuilder &HearthstoneTokenBuilder::allowBattleNetApiCall() { + _allowBattleNetApiCall = true; + return *this; +} HearthstoneTokenBuilder &HearthstoneTokenBuilder::allowUpdateSourceFile() { - mUpdateSourceFile = true; + _allowUpdateSourceFile = true; + return *this; +} +HearthstoneTokenBuilder &HearthstoneTokenBuilder::allowExpiredToken() { + _allowExpiredToken = true; return *this; } -bool HearthstoneTokenBuilder::isUpdateSourceFileAllowed() const { return mUpdateSourceFile; } +bool HearthstoneTokenBuilder::isUpdateSourceFileAllowed() const { return _allowUpdateSourceFile; } -const bool HearthstoneTokenBuilder::hasExpired(json iTokenJson) const { +bool HearthstoneTokenBuilder::isTokenValid(const json &iTokenJson) const { if (not iTokenJson.contains("Token") or not iTokenJson["Token"].contains("ExpirationTime") or not iTokenJson["Token"].contains("Content")) { - return true; + return false; } if (iTokenJson["Token"]["Content"].dump().length() < kTOKEN_CONTENT_MIN_LENGTH) { + return false; + } + if (_allowExpiredToken) { return true; } const std::uint64_t kExpirationTime{iTokenJson["Token"]["ExpirationTime"]}; @@ -48,7 +71,18 @@ const bool HearthstoneTokenBuilder::hasExpired(json iTokenJson) const { static_cast(std::chrono::duration_cast( std::chrono::high_resolution_clock::now().time_since_epoch()) .count())}; - return kCurrentTime >= kExpirationTime; + return kCurrentTime <= kExpirationTime; +} + +std::chrono::seconds HearthstoneTokenBuilder::getCurrentTime() const { + return std::chrono::duration_cast( + std::chrono::utc_clock::now().time_since_epoch()); +} + +Token HearthstoneTokenBuilder::getTokenWithApi(const std::string &iCredentialsJsonFilePath) const { + std::ifstream aAgentJsonFile{iCredentialsJsonFilePath}; + json aCredentials = json::parse(aAgentJsonFile); + return getTokenWithApi(aCredentials); } size_t HearthstoneTokenBuilder::curlCallbackStringOutput(void *iPtr, size_t iSize, size_t iNmemb, @@ -63,33 +97,25 @@ size_t HearthstoneTokenBuilder::curlCallbackStringOutput(void *iPtr, size_t iSiz return newLength; } -std::chrono::seconds HearthstoneTokenBuilder::getCurrentTime() const { - return std::chrono::duration_cast( - std::chrono::utc_clock::now().time_since_epoch()); -} - -std::pair -HearthstoneTokenBuilder::getTokenWithApi(const std::string &iAgentJsonFilePath) const { - std::ifstream aAgentJsonFile{iAgentJsonFilePath}; - json aCredentials = json::parse(aAgentJsonFile); - if (not aCredentials.contains("Agent") or not aCredentials["Agent"].contains("Username") or - not aCredentials["Agent"].contains("Password")) { +Token HearthstoneTokenBuilder::getTokenWithApi(const json &iCredentials) const { + if (not iCredentials.contains("Agent") or not iCredentials["Agent"].contains("Username") or + not iCredentials["Agent"].contains("Password")) { std::cerr << "Please define Agent/Username and Agent/Password in the " "JSON credentials file\n"; std::exit(EXIT_FAILURE); } - const std::string kAgentUsername{aCredentials["Agent"]["Username"]}; - const std::string kAgentPassword{aCredentials["Agent"]["Password"]}; + const std::string aAgentUsername{iCredentials["Agent"]["Username"]}; + const std::string aAgentPassword{iCredentials["Agent"]["Password"]}; CURL *aCurl{curl_easy_init()}; std::string aTokenInfo; - if (aCurl and not kAgentUsername.empty() and not kAgentPassword.empty()) { + if (aCurl and not aAgentUsername.empty() and not aAgentPassword.empty()) { curl_easy_setopt(aCurl, CURLOPT_URL, "https://oauth.battle.net/token"); curl_easy_setopt(aCurl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(aCurl, CURLOPT_SSL_VERIFYHOST, 0L); curl_easy_setopt(aCurl, CURLOPT_WRITEFUNCTION, curlCallbackStringOutput); curl_easy_setopt(aCurl, CURLOPT_WRITEDATA, &aTokenInfo); curl_easy_setopt(aCurl, CURLOPT_USERPWD, - std::format("{}:{}", kAgentUsername, kAgentPassword).data()); + std::format("{}:{}", aAgentUsername, aAgentPassword).data()); curl_easy_setopt(aCurl, CURLOPT_POSTFIELDS, "grant_type=client_credentials"); CURLcode result_code{curl_easy_perform(aCurl)}; curl_easy_cleanup(aCurl); @@ -99,24 +125,18 @@ HearthstoneTokenBuilder::getTokenWithApi(const std::string &iAgentJsonFilePath) } } json aTokenJson = json::parse(aTokenInfo); - return std::make_pair(aTokenJson["access_token"], aTokenJson["expires_in"]); + return Token{} + .setText(aTokenJson["access_token"]) + .setLifetime(std::chrono::seconds{aTokenJson["expires_in"]}); } -std::string HearthstoneTokenBuilder::getTokenFromLocalFile() const { - std::ifstream aTokenFile{mFilePath}; - return json::parse(aTokenFile)["Token"]["Content"]; -} - -void HearthstoneTokenBuilder::writeTokenToLocalFile( - const std::pair &iToken) const { - std::ifstream aTokenFileInput{mFilePath}; - json aCredentialsFile = json::parse(aTokenFileInput); - aCredentialsFile["Token"]["Content"] = iToken.first; - aCredentialsFile["Token"]["ExpirationTime"] = - std::chrono::duration_cast(getCurrentTime() + - std::chrono::seconds(iToken.second)) - .count(); - - std::ofstream aTokenFileOutput{mFilePath}; +void HearthstoneTokenBuilder::writeTokenToLocalFile(std::shared_ptr &iToken, + const std::string &iTokenFileOutputPath) const { + std::ifstream aTokenFile{iTokenFileOutputPath}; + json aCredentialsFile = json::parse(aTokenFile); + aCredentialsFile["Token"]["Content"] = iToken->text(); + aCredentialsFile["Token"]["ExpirationTime"] = iToken->expirationTime()->count(); + std::ofstream aTokenFileOutput{iTokenFileOutputPath}; aTokenFileOutput << std::setw(4) << aCredentialsFile; } +} // namespace bnet diff --git a/battleNetApi/src/token.cpp b/battleNetApi/src/token.cpp index 276c334..a54136c 100644 --- a/battleNetApi/src/token.cpp +++ b/battleNetApi/src/token.cpp @@ -1,14 +1,26 @@ #include "token.hpp" -Token::Token(const std::string &iTokenText, const std::chrono::seconds &iLifetime) - : _tokenText{iTokenText}, _expirationTime{iLifetime + currentTime()} {} +namespace bnet { +Token::Token() {} -std::string Token::text() const { return _tokenText; } +std::optional Token::text() const { return _tokenText; } -void Token::setText(const std::string &iTokenText) { _tokenText = iTokenText; } +std::optional Token::expirationTime() const { return _expirationTime; } -void Token::setLifetime(const std::chrono::seconds &iExpirationTime) { - _expirationTime = iExpirationTime + currentTime(); +Token &Token::setText(const TokenText &iTokenText) { + _tokenText = iTokenText; + return *this; +} + +Token &Token::setLifetime(const std::chrono::seconds &iLifetime) { + _expirationTime = iLifetime + currentTime(); + return *this; +} + +Token &Token::setExpirationTime(const std::chrono::seconds &iExpirationTime) { + _expirationTime = iExpirationTime; + return *this; } bool Token::isExpired() { return _expirationTime <= currentTime(); } +} // namespace bnet diff --git a/battleNetApi/tests/CMakeLists.txt b/battleNetApi/tests/CMakeLists.txt index 6918fb5..b59fd8a 100644 --- a/battleNetApi/tests/CMakeLists.txt +++ b/battleNetApi/tests/CMakeLists.txt @@ -36,6 +36,8 @@ set(BATTLE_NET_API_DIR ${GIT_REPOSITORY_FOLDER_PATH}/battleNetApi) set(INCLUDE_TOKEN_DIR ${BATTLE_NET_API_DIR}/include) set(SRC_TOKEN_TESTS ${BATTLE_NET_API_DIR}/src/token.cpp + ${BATTLE_NET_API_DIR}/src/hearthstoneTokenBuilder.cpp + ${BATTLE_NET_API_DIR}/src/tokenBuilder.cpp ${BATTLE_NET_API_DIR}/tests/src/tokenTests.cpp ) add_executable(tokenTests ${SRC_TOKEN_TESTS}) diff --git a/battleNetApi/tests/resources/credentials_expired_token.json b/battleNetApi/tests/resources/credentials_expired_token.json new file mode 100644 index 0000000..a5ebb3b --- /dev/null +++ b/battleNetApi/tests/resources/credentials_expired_token.json @@ -0,0 +1,10 @@ +{ + "Agent": { + "Password": "MrMkhVMNdOeqPL4Q4XVQAsRPR14J7Yq6", + "Username": "2b50f1123b424d5e9c21971a110fdd04" + }, + "Token": { + "Content": "EUaRHt538jT3Wco5A59cFF5LIQmH6eYRfG", + "ExpirationTime": 0 + } +} \ No newline at end of file diff --git a/battleNetApi/tests/src/tests.cpp b/battleNetApi/tests/src/tests.cpp index d3c09c7..edc0641 100644 --- a/battleNetApi/tests/src/tests.cpp +++ b/battleNetApi/tests/src/tests.cpp @@ -103,7 +103,7 @@ TEST_CASE("Retrieve pages 10 times with parallelization", "[retrievePagesParalle REQUIRE(aNumberOfPages >= aNumberOfRetrievals); for (int i{1}; i < aNumberOfRetrievals; ++i) { aApiUrl.setPage(std::to_string(i + 1)); - aRequests.push_back(aApiUrl.text()); + aRequests.emplace_back(aApiUrl.text()); } std::vector aApiResponses = HearthstoneApi::makeApiRequest(aRequests, aTokenText); aApiResponses.emplace(aApiResponses.begin(), aFirstPageJson); @@ -121,7 +121,7 @@ TEST_CASE("Merge two card lists from two page requests", "[Merge pages]") { json::reference aMergedCards = aFirstPageJson["cards"]; for (auto &card : aSecondPageJson["cards"]) { - aMergedCards.push_back(std::move(card)); + aMergedCards.emplace_back(std::move(card)); } REQUIRE(aMergedCards.size() == 32); std::ofstream aOutputJson("build/output_pages_merge.json"); diff --git a/battleNetApi/tests/src/tokenTests.cpp b/battleNetApi/tests/src/tokenTests.cpp index 2e2d5b4..b702024 100644 --- a/battleNetApi/tests/src/tokenTests.cpp +++ b/battleNetApi/tests/src/tokenTests.cpp @@ -6,18 +6,63 @@ #include #include #include +#include +#include +#include #include -#include #include TEST_CASE("Token", "[Token is expired]") { - const std::string aCredentialsFile{"../tests/resources/credentials.json"}; - Token aToken(aCredentialsFile, std::chrono::seconds(0)); - REQUIRE(aToken.isExpired()); + REQUIRE(bnet::Token{}.setText("").setLifetime(std::chrono::seconds{0}).isExpired()); } TEST_CASE("Token", "[Token is not expired]") { - const std::string aCredentialsFile{"../tests/resources/credentials.json"}; - Token aToken(aCredentialsFile, std::chrono::seconds(20)); - REQUIRE_FALSE(aToken.isExpired()); + REQUIRE_FALSE(bnet::Token{}.setText("").setLifetime(std::chrono::seconds{20}).isExpired()); +} + +TEST_CASE("Token", "[Check token text]") { + bnet::Token aToken; + aToken.setText("currenttext"); + REQUIRE(aToken.text() == "currenttext"); +} + +TEST_CASE("Token", "[Change token text]") { + bnet::Token aToken; + aToken.setText("currenttext"); + REQUIRE(aToken.text() == "currenttext"); + aToken.setText("newtext"); + REQUIRE(aToken.text() == "newtext"); +} + +TEST_CASE("TokenBuilder", "[Retrieve token from JSON]") { + bnet::HearthstoneTokenBuilder aTokenBuilder{"resources/credentials.json"}; + REQUIRE(aTokenBuilder.allowExpiredToken().buildToken().getToken()); + REQUIRE(aTokenBuilder.getToken()->text() == "ThisIsTheContentOfTokenContenthehe"); +} + +TEST_CASE("TokenBuilder", "[Expired token in JSON file : generate new one]") { + bnet::HearthstoneTokenBuilder aTokenBuilder{"resources/credentials_expired_token.json"}; + REQUIRE(aTokenBuilder.allowBattleNetApiCall().buildToken().getToken()->text()->length() > 20); +} + +TEST_CASE("TokenBuilder", "[Overwrite file with generated token]") { + const std::string aCredentialsFilePath{"resources/credentials_expired_token.json"}; + std::ifstream aCredentialsFileRead{aCredentialsFilePath}; + nlohmann::json aCredentialsJson = nlohmann::json::parse(aCredentialsFileRead); + aCredentialsJson["Token"]["ExpirationTime"] = 0; + aCredentialsFileRead.close(); + std::ofstream aCredentialsFileWrite{aCredentialsFilePath}; + aCredentialsFileWrite << std::setw(4) << aCredentialsJson; + aCredentialsFileWrite.close(); + bnet::HearthstoneTokenBuilder aTokenBuilder{aCredentialsFilePath}; + REQUIRE(aTokenBuilder.allowBattleNetApiCall() + .allowUpdateSourceFile() + .buildToken() + .getToken() + ->text() + ->length() > 20); + aCredentialsJson["Token"]["ExpirationTime"] = 0; + aCredentialsFileWrite.open(aCredentialsFilePath); + aCredentialsFileWrite << std::setw(4) << aCredentialsJson; + aCredentialsFileWrite.close(); } -- cgit v1.2.3