diff --git a/rcheevos b/rcheevos index e3f7b7d20..f06b14aa4 160000 --- a/rcheevos +++ b/rcheevos @@ -1 +1 @@ -Subproject commit e3f7b7d20c9253ae9340cd433199b838d061716c +Subproject commit f06b14aa41fa1ed41e0680c319314742c3cf1ebe diff --git a/src/RA_Integration.vcxproj b/src/RA_Integration.vcxproj index f2cb0c821..71756d265 100644 --- a/src/RA_Integration.vcxproj +++ b/src/RA_Integration.vcxproj @@ -197,7 +197,6 @@ - diff --git a/src/RA_Integration.vcxproj.filters b/src/RA_Integration.vcxproj.filters index b55ed28de..f2cfdcc79 100644 --- a/src/RA_Integration.vcxproj.filters +++ b/src/RA_Integration.vcxproj.filters @@ -794,9 +794,6 @@ UI\ViewModels - - API - UI\ViewModels diff --git a/src/api/ApiCall.cpp b/src/api/ApiCall.cpp index c4b402e17..0141c0f58 100644 --- a/src/api/ApiCall.cpp +++ b/src/api/ApiCall.cpp @@ -15,7 +15,6 @@ static ra::api::IServer& Server() ResolveHash::Response ResolveHash::Request::Call() const { return Server().ResolveHash(*this); } UpdateAchievement::Response UpdateAchievement::Request::Call() const { return Server().UpdateAchievement(*this); } FetchAchievementInfo::Response FetchAchievementInfo::Request::Call() const { return Server().FetchAchievementInfo(*this); } -UpdateLeaderboard::Response UpdateLeaderboard::Request::Call() const { return Server().UpdateLeaderboard(*this); } FetchLeaderboardInfo::Response FetchLeaderboardInfo::Request::Call() const { return Server().FetchLeaderboardInfo(*this); } UpdateRichPresence::Response UpdateRichPresence::Request::Call() const { return Server().UpdateRichPresence(*this); } UploadBadge::Response UploadBadge::Request::Call() const { return Server().UploadBadge(*this); } diff --git a/src/api/IServer.hh b/src/api/IServer.hh index e31621184..95424c87d 100644 --- a/src/api/IServer.hh +++ b/src/api/IServer.hh @@ -6,7 +6,6 @@ #include "api/FetchLeaderboardInfo.hh" #include "api/ResolveHash.hh" #include "api/UpdateAchievement.hh" -#include "api/UpdateLeaderboard.hh" #include "api/UpdateRichPresence.hh" #include "api/UploadBadge.hh" @@ -22,7 +21,6 @@ public: virtual ResolveHash::Response ResolveHash(const ResolveHash::Request& request) = 0; virtual UpdateAchievement::Response UpdateAchievement(const UpdateAchievement::Request& request) = 0; virtual FetchAchievementInfo::Response FetchAchievementInfo(const FetchAchievementInfo::Request& request) = 0; - virtual UpdateLeaderboard::Response UpdateLeaderboard(const UpdateLeaderboard::Request& request) = 0; virtual FetchLeaderboardInfo::Response FetchLeaderboardInfo(const FetchLeaderboardInfo::Request& request) = 0; virtual UpdateRichPresence::Response UpdateRichPresence(const UpdateRichPresence::Request& request) = 0; diff --git a/src/api/UpdateLeaderboard.hh b/src/api/UpdateLeaderboard.hh deleted file mode 100644 index e0bd3a652..000000000 --- a/src/api/UpdateLeaderboard.hh +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef RA_API_UPDATE_LEADERBOARD_HH -#define RA_API_UPDATE_LEADERBOARD_HH -#pragma once - -#include "ApiCall.hh" - -#include "data\Value.hh" - -namespace ra { -namespace api { - -class UpdateLeaderboard -{ -public: - static constexpr const char* const Name() noexcept { return "UpdateLeaderboard"; } - - struct Response : ApiResponseBase - { - unsigned int LeaderboardId{ 0U }; - }; - - struct Request : ApiRequestBase - { - unsigned int LeaderboardId{ 0U }; - unsigned int GameId{ 0U }; - std::wstring Title; - std::wstring Description; - std::string StartTrigger; - std::string SubmitTrigger; - std::string CancelTrigger; - std::string ValueDefinition; - ra::data::Value::Format Format{ ra::data::Value::Format::Value }; - bool LowerIsBetter{ false }; - - using Callback = std::function; - - Response Call() const; - - void CallAsync(Callback&& callback) const - { - ApiRequestBase::CallAsync(*this, std::move(callback)); - } - }; -}; - -} // namespace api -} // namespace ra - -#endif // !RA_API_UPDATE_LEADERBOARD_HH diff --git a/src/api/impl/ConnectedServer.cpp b/src/api/impl/ConnectedServer.cpp index 0a0ec043e..4d0361804 100644 --- a/src/api/impl/ConnectedServer.cpp +++ b/src/api/impl/ConnectedServer.cpp @@ -531,63 +531,6 @@ FetchAchievementInfo::Response ConnectedServer::FetchAchievementInfo(const Fetch return response; } -UpdateLeaderboard::Response ConnectedServer::UpdateLeaderboard(const UpdateLeaderboard::Request& request) -{ - UpdateLeaderboard::Response response; - - rc_api_update_leaderboard_request_t api_params; - memset(&api_params, 0, sizeof(api_params)); - - const auto& pUserContext = ra::services::ServiceLocator::Get(); - api_params.username = pUserContext.GetUsername().c_str(); - api_params.api_token = pUserContext.GetApiToken().c_str(); - - const std::string sTitle = ra::util::String::Narrow(request.Title); - const std::string sDescription = ra::util::String::Narrow(request.Description); - - api_params.leaderboard_id = request.LeaderboardId; - api_params.game_id = request.GameId; - api_params.title = sTitle.c_str(); - api_params.description = sDescription.c_str(); - api_params.start_trigger = request.StartTrigger.c_str(); - api_params.submit_trigger = request.SubmitTrigger.c_str(); - api_params.cancel_trigger = request.CancelTrigger.c_str(); - api_params.value_definition = request.ValueDefinition.c_str(); - api_params.lower_is_better = request.LowerIsBetter ? 1 : 0; - api_params.format = ra::data::Value::FormatToServerEnum(request.Format); - - rc_api_request_t api_request; - const int result = rc_api_init_update_leaderboard_request(&api_request, &api_params); - if (result == RC_OK) - { - ra::services::Http::Response httpResponse; - if (DoRequest(api_request, UpdateLeaderboard::Name(), httpResponse, response)) - { - rc_api_update_leaderboard_response_t api_response; - rc_api_server_response_t server_response; - HttpResponseToServerResponse(httpResponse, &server_response); - - const auto nResult = rc_api_process_update_leaderboard_server_response(&api_response, &server_response); - - if (ValidateResponse(nResult, api_response.response, UpdateLeaderboard::Name(), httpResponse.StatusCode(), response)) - { - response.Result = ApiResult::Success; - response.LeaderboardId = api_response.leaderboard_id; - } - - rc_api_destroy_update_leaderboard_response(&api_response); - } - } - else - { - response.Result = ApiResult::Failed; - response.ErrorMessage = rc_error_str(result); - } - - rc_api_destroy_request(&api_request); - return response; -} - FetchLeaderboardInfo::Response ConnectedServer::FetchLeaderboardInfo(const FetchLeaderboardInfo::Request& request) { FetchLeaderboardInfo::Response response; diff --git a/src/api/impl/ConnectedServer.hh b/src/api/impl/ConnectedServer.hh index 52acd6091..b71026cf8 100644 --- a/src/api/impl/ConnectedServer.hh +++ b/src/api/impl/ConnectedServer.hh @@ -18,7 +18,6 @@ public: ResolveHash::Response ResolveHash(const ResolveHash::Request& request) override; UpdateAchievement::Response UpdateAchievement(const UpdateAchievement::Request& request) override; FetchAchievementInfo::Response FetchAchievementInfo(const FetchAchievementInfo::Request& request) override; - UpdateLeaderboard::Response UpdateLeaderboard(const UpdateLeaderboard::Request& request) override; FetchLeaderboardInfo::Response FetchLeaderboardInfo(const FetchLeaderboardInfo::Request& request) override; UpdateRichPresence::Response UpdateRichPresence(const UpdateRichPresence::Request& request) override; UploadBadge::Response UploadBadge(const UploadBadge::Request& request) override; diff --git a/src/api/impl/ServerBase.hh b/src/api/impl/ServerBase.hh index bb92592ac..7eb59d378 100644 --- a/src/api/impl/ServerBase.hh +++ b/src/api/impl/ServerBase.hh @@ -32,11 +32,6 @@ public: return UnsupportedApi(FetchAchievementInfo::Name()); } - UpdateLeaderboard::Response UpdateLeaderboard(const UpdateLeaderboard::Request&) override - { - return UnsupportedApi(UpdateLeaderboard::Name()); - } - FetchLeaderboardInfo::Response FetchLeaderboardInfo(const FetchLeaderboardInfo::Request&) override { return UnsupportedApi(FetchLeaderboardInfo::Name()); diff --git a/src/devkit/data/models/LeaderboardModel.cpp b/src/devkit/data/models/LeaderboardModel.cpp index daaca8732..96c2ce26c 100644 --- a/src/devkit/data/models/LeaderboardModel.cpp +++ b/src/devkit/data/models/LeaderboardModel.cpp @@ -318,6 +318,21 @@ void LeaderboardModel::SyncTrackerToRuntime() const } } +void LeaderboardModel::SyncCategoryToRuntime() const +{ + switch (GetCategory()) + { + case ra::data::models::AssetCategory::Promoted: + case ra::data::models::AssetCategory::Local: + m_pLeaderboardInfo->public_.category = RC_CLIENT_LEADERBOARD_CATEGORY_PROMOTED; + break; + + default: + m_pLeaderboardInfo->public_.category = RC_CLIENT_LEADERBOARD_CATEGORY_UNPROMOTED; + break; + } +} + void LeaderboardModel::SyncDefinitionToRuntime() { Expects(m_pLeaderboardInfo != nullptr); @@ -477,7 +492,7 @@ void LeaderboardModel::InitializeFromPublishedLeaderboard( SetID(pLeaderboard.public_.id); SetName(ra::util::String::Widen(pLeaderboard.public_.title)); SetDescription(ra::util::String::Widen(pLeaderboard.public_.description)); - SetCategory(AssetCategory::Promoted); + SetCategory(pLeaderboard.public_.category == RC_CLIENT_LEADERBOARD_CATEGORY_PROMOTED ? AssetCategory::Promoted : AssetCategory::Unpromoted); SetValueFormat(Value::FormatFromRcheevosFormat(pLeaderboard.format)); SetLowerIsBetter(pLeaderboard.public_.lower_is_better); SetHidden(pLeaderboard.hidden); @@ -535,6 +550,7 @@ void LeaderboardModel::SyncToLocalLeaderboardInfo() SyncTitleToRuntime(); SyncDescriptionToRuntime(); SyncDefinitionToRuntime(); + SyncCategoryToRuntime(); SyncValueFormatToRuntime(); } diff --git a/src/devkit/data/models/LeaderboardModel.hh b/src/devkit/data/models/LeaderboardModel.hh index 4773d53d2..c3c84249e 100644 --- a/src/devkit/data/models/LeaderboardModel.hh +++ b/src/devkit/data/models/LeaderboardModel.hh @@ -233,6 +233,7 @@ private: void SyncStateToRuntime(AssetState nNewState) const; void SyncValueFormatToRuntime() const; void SyncTrackerToRuntime() const; + void SyncCategoryToRuntime() const; void SyncDefinitionToRuntime(); void SyncStateFromRuntime(uint8_t nState); diff --git a/src/ui/viewmodels/AssetListViewModel.cpp b/src/ui/viewmodels/AssetListViewModel.cpp index 9f94b554b..fae502649 100644 --- a/src/ui/viewmodels/AssetListViewModel.cpp +++ b/src/ui/viewmodels/AssetListViewModel.cpp @@ -1250,9 +1250,6 @@ void AssetListViewModel::SaveSelected() { switch (pAsset->GetType()) { - case ra::data::models::AssetType::Leaderboard: - ra::ui::viewmodels::MessageBoxViewModel::ShowWarningMessage(L"Leaderboards cannot be demoted."); - return; case ra::data::models::AssetType::RichPresence: ra::ui::viewmodels::MessageBoxViewModel::ShowWarningMessage(L"Rich Presence cannot be demoted."); return; diff --git a/src/ui/viewmodels/AssetUploadViewModel.cpp b/src/ui/viewmodels/AssetUploadViewModel.cpp index 65a486f6f..4a99c9eea 100644 --- a/src/ui/viewmodels/AssetUploadViewModel.cpp +++ b/src/ui/viewmodels/AssetUploadViewModel.cpp @@ -1,7 +1,6 @@ #include "AssetUploadViewModel.hh" #include "api\UpdateAchievement.hh" -#include "api\UpdateLeaderboard.hh" #include "api\UpdateRichPresence.hh" #include "api\UploadBadge.hh" @@ -21,6 +20,7 @@ #include #include +#include namespace ra { namespace ui { @@ -346,51 +346,108 @@ void AssetUploadViewModel::UploadAchievement(ra::data::models::AchievementModel& void AssetUploadViewModel::UploadLeaderboard(ra::data::models::LeaderboardModel& pLeaderboard) { const auto& pGameContext = ra::services::ServiceLocator::Get(); + std::string sNarrowTitle = ra::util::String::Narrow(pLeaderboard.GetName()); + std::string sNarrowDescription = ra::util::String::Narrow(pLeaderboard.GetDescription()); - ra::api::UpdateLeaderboard::Request request; - request.GameId = pGameContext.GetGameId(pLeaderboard.GetSubsetID()); - request.Title = pLeaderboard.GetName(); - request.Description = pLeaderboard.GetDescription(); - request.StartTrigger = pLeaderboard.GetStartTrigger(); - request.SubmitTrigger = pLeaderboard.GetSubmitTrigger(); - request.CancelTrigger = pLeaderboard.GetCancelTrigger(); - request.ValueDefinition = pLeaderboard.GetValueDefinition(); - request.Format = pLeaderboard.GetValueFormat(); - request.LowerIsBetter = pLeaderboard.IsLowerBetter(); + // construct the API request + rc_api_update_leaderboard_request_t api_params; + memset(&api_params, 0, sizeof(api_params)); + const auto& pUserContext = ra::services::ServiceLocator::Get(); + api_params.username = pUserContext.GetUsername().c_str(); + api_params.api_token = pUserContext.GetApiToken().c_str(); + api_params.game_id = pGameContext.GetGameId(pLeaderboard.GetSubsetID()); // TODO: switch to achievement_set_id + api_params.title = sNarrowTitle.c_str(); + api_params.description = sNarrowDescription.c_str(); + api_params.start_trigger = pLeaderboard.GetStartTrigger().c_str(); + api_params.submit_trigger = pLeaderboard.GetSubmitTrigger().c_str(); + api_params.cancel_trigger = pLeaderboard.GetCancelTrigger().c_str(); + api_params.value_definition = pLeaderboard.GetValueDefinition().c_str(); + api_params.format = ra::data::Value::FormatToServerEnum(pLeaderboard.GetValueFormat()); + api_params.lower_is_better = pLeaderboard.IsLowerBetter() ? 1 : 0; + + switch (pLeaderboard.GetCategory()) + { + case ra::data::models::AssetCategory::Local: + // new leaderboard, submit to unpromoted: + api_params.state = RC_LEADERBOARD_STATE_UNPROMOTED; + break; - if (pLeaderboard.GetCategory() != ra::data::models::AssetCategory::Local) - request.LeaderboardId = pLeaderboard.GetID(); + case ra::data::models::AssetCategory::Promoted: + // promoted leaderboard + api_params.leaderboard_id = pLeaderboard.GetID(); + api_params.state = RC_LEADERBOARD_STATE_ACTIVE; + break; - const auto& response = request.Call(); + default: + // unpromoted leaderboard + api_params.leaderboard_id = pLeaderboard.GetID(); + api_params.state = RC_LEADERBOARD_STATE_UNPROMOTED; + break; + } - // update the leaderboard model - if (response.Succeeded()) + auto& pClient = ra::services::ServiceLocator::Get(); + rc_api_request_t api_request; + auto nResult = rc_api_init_update_leaderboard_request_hosted(&api_request, &api_params, pClient.GetHost()); + std::string sErrorMessage = rc_error_str(nResult); + + if (nResult == RC_OK) { - if (pLeaderboard.GetCategory() == ra::data::models::AssetCategory::Local) + bool bRetry = false; + std::string sResponseBuffer; + + do { - pLeaderboard.SetCategory(ra::data::models::AssetCategory::Promoted); - pLeaderboard.SetID(response.LeaderboardId); - } + bRetry = false; - pLeaderboard.UpdateLocalCheckpoint(); - pLeaderboard.UpdateServerCheckpoint(); - } - else if (response.Result == ra::api::ApiResult::Incomplete) - { - Rest(); - UploadLeaderboard(pLeaderboard); - return; + // send the API request + rc_api_server_response_t api_response; + pClient.SendRequest(api_request, api_response, sResponseBuffer); + + // process the response + rc_api_update_leaderboard_response_t response; + nResult = rc_api_process_update_leaderboard_server_response(&response, &api_response); + if (nResult == RC_OK) + { + if (pLeaderboard.GetCategory() == ra::data::models::AssetCategory::Local) + { + pLeaderboard.SetCategory(ra::data::models::AssetCategory::Unpromoted); + pLeaderboard.SetID(response.leaderboard_id); + } + + pLeaderboard.UpdateLocalCheckpoint(); + pLeaderboard.UpdateServerCheckpoint(); + } + else + { + bRetry = rc_client_should_retry(&api_response); + if (!bRetry) + { + if (response.response.error_message) + sErrorMessage = response.response.error_message; + else + sErrorMessage = rc_error_str(nResult); + } + } + + rc_api_destroy_update_leaderboard_response(&response); + + if (!bRetry) + break; + + Rest(); + } while (true); } - // update the queue + rc_api_destroy_request(&api_request); + std::lock_guard pLock(m_pMutex); for (auto& pScan : m_vUploadQueue) { if (pScan.pAsset == &pLeaderboard) { - pScan.sErrorMessage = response.ErrorMessage; + pScan.sErrorMessage = sErrorMessage; - if (response.ErrorMessage == "Invalid state") + if (nResult == RC_INVALID_STATE) { // generic API failure. Try to guess what went wrong if (pLeaderboard.GetName().empty()) @@ -407,7 +464,7 @@ void AssetUploadViewModel::UploadLeaderboard(ra::data::models::LeaderboardModel& pScan.sErrorMessage = "At least one value condition is required"; } - pScan.nState = response.Succeeded() ? UploadState::Success : UploadState::Failed; + pScan.nState = (nResult == RC_OK) ? UploadState::Success : UploadState::Failed; break; } } @@ -587,8 +644,7 @@ void AssetUploadViewModel::UploadMemoryNotes(ra::data::models::MemoryNotesModel& } else { - bRetry = api_response.http_status_code == ra::etoi(ra::services::Http::StatusCode::TooManyRequests) - || api_response.http_status_code == RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR; + bRetry = rc_client_should_retry(&api_response); if (!bRetry) { if (response.response.error_message) diff --git a/tests/data/context/GameContext_Tests.cpp b/tests/data/context/GameContext_Tests.cpp index ada3a869d..5714dfaa0 100644 --- a/tests/data/context/GameContext_Tests.cpp +++ b/tests/data/context/GameContext_Tests.cpp @@ -151,7 +151,7 @@ TEST_CLASS(GameContext_Tests) mockAchievementRuntime.MockUser("Username", "ApiToken"); mockRcClient.MockResponse( - "r=achievementsets&u=Username&t=ApiToken&m=" + sHash, + "r=achievementsets&u=Username&t=ApiToken&m=" + sHash + "&v=2", "{\"Success\":true," "\"GameId\":" + std::to_string(nGameID) + "," "\"Title\":\"GameTitle\"," @@ -917,7 +917,7 @@ TEST_CLASS(GameContext_Tests) GameContextHarness game; game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321"); game.mockRcClient.MockResponse( - "r=achievementsets&u=Username&t=ApiToken&m=0123456789abcdeffedcba987654321", + "r=achievementsets&u=Username&t=ApiToken&m=0123456789abcdeffedcba987654321&v=2", "{\"Success\":true," "\"GameId\":1," "\"Title\":\"GameTitle\"," @@ -1315,7 +1315,7 @@ TEST_CLASS(GameContext_Tests) game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321"); bool bBeforeResponseCalled = false; - game.mockRcClient.OnBeforeResponse("r=achievementsets&u=Username&t=ApiToken&m=0123456789abcdeffedcba987654321", + game.mockRcClient.OnBeforeResponse("r=achievementsets&u=Username&t=ApiToken&m=0123456789abcdeffedcba987654321&v=2", [&game, &bBeforeResponseCalled]() { bBeforeResponseCalled = true; Assert::IsTrue(game.mockAchievementRuntime.IsPaused()); @@ -1333,7 +1333,7 @@ TEST_CLASS(GameContext_Tests) game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321"); bool bBeforeResponseCalled = false; - game.mockRcClient.OnBeforeResponse("r=achievementsets&u=Username&t=ApiToken&m=0123456789abcdeffedcba987654321", + game.mockRcClient.OnBeforeResponse("r=achievementsets&u=Username&t=ApiToken&m=0123456789abcdeffedcba987654321&v=2", [&game, &bBeforeResponseCalled]() { bBeforeResponseCalled = true; Assert::IsTrue(game.mockAchievementRuntime.IsPaused()); diff --git a/tests/devkit/context/mocks/MockRcClient.cpp b/tests/devkit/context/mocks/MockRcClient.cpp index 58c09607b..c6a0b1459 100644 --- a/tests/devkit/context/mocks/MockRcClient.cpp +++ b/tests/devkit/context/mocks/MockRcClient.cpp @@ -94,9 +94,12 @@ void MockRcClient::SendRequest(const rc_api_request_t& pRequest, rc_api_server_r } } - pServerResponse.http_status_code = 504; // Gateway Timeout (upstream server took too long to response to intermediary proxy) - pServerResponse.body = ""; - pServerResponse.body_length = 0; + // 410 Gone indicates the resource is not available. + // More importantly, it's not classified as a retryable error, so a test that's not using the correct + // request string won't retry infinitely. + pServerResponse.http_status_code = 410; + pServerResponse.body = "Gone"; + pServerResponse.body_length = 4; } void MockRcClient::MockResponse(const std::string& sRequestParams, const std::string& sResponseBody, int nHttpStatusCode) diff --git a/tests/mocks/MockAchievementRuntime.cpp b/tests/mocks/MockAchievementRuntime.cpp index 42e55a308..851a1a45d 100644 --- a/tests/mocks/MockAchievementRuntime.cpp +++ b/tests/mocks/MockAchievementRuntime.cpp @@ -262,6 +262,7 @@ static rc_client_leaderboard_info_t* AddLeaderboard(const rc_client_t* client, r const std::string sGeneratedDescripton = ra::util::String::Printf("Description %u", nId); leaderboard->public_.description = rc_buffer_strcpy(&game->buffer, sGeneratedDescripton.c_str()); + leaderboard->public_.category = RC_CLIENT_LEADERBOARD_CATEGORY_PROMOTED; leaderboard->public_.state = static_cast(rc_client_get_hardcore_enabled(client) ? RC_CLIENT_LEADERBOARD_STATE_ACTIVE : RC_CLIENT_LEADERBOARD_STATE_INACTIVE); diff --git a/tests/mocks/MockServer.hh b/tests/mocks/MockServer.hh index a2cdce642..fb7ec00e1 100644 --- a/tests/mocks/MockServer.hh +++ b/tests/mocks/MockServer.hh @@ -73,11 +73,6 @@ public: return HandleRequest(request); } - UpdateLeaderboard::Response UpdateLeaderboard(const UpdateLeaderboard::Request& request) override - { - return HandleRequest(request); - } - FetchLeaderboardInfo::Response FetchLeaderboardInfo(const FetchLeaderboardInfo::Request& request) override { return HandleRequest(request); diff --git a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp index b731c2ec4..8b0fb904b 100644 --- a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp +++ b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp @@ -3221,8 +3221,10 @@ TEST_CLASS(AssetListViewModel_Tests) vmAssetList.mockDesktop.ExpectWindow([&bDialogSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) { bDialogSeen = true; - Assert::AreEqual(std::wstring(L"Leaderboards cannot be demoted."), vmMessageBox.GetMessage()); - return DialogResult::OK; + Assert::AreEqual(std::wstring(L"Are you sure you want to demote 2 items?"), vmMessageBox.GetHeader()); + Assert::AreEqual(std::wstring(L"Unpromoted items can no longer be earned by players."), vmMessageBox.GetMessage()); + Assert::AreEqual(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo, vmMessageBox.GetButtons()); + return DialogResult::Yes; }); const auto* pItem1 = vmAssetList.mockGameContext.Assets().GetItemAt(0); @@ -3242,13 +3244,13 @@ TEST_CLASS(AssetListViewModel_Tests) Assert::IsTrue(bDialogSeen); Assert::AreEqual(AssetChanges::None, pItem1->GetChanges()); - Assert::AreEqual(AssetCategory::Promoted, pItem1->GetCategory()); + Assert::AreEqual(AssetCategory::Unpromoted, pItem1->GetCategory()); Assert::AreEqual(AssetChanges::None, pItem2->GetChanges()); - Assert::AreEqual(AssetCategory::Promoted, pItem2->GetCategory()); + Assert::AreEqual(AssetCategory::Unpromoted, pItem2->GetCategory()); - // item will have been moved to Promoted, so nothing will be selected + // item will have been moved to Unpromoted, so nothing will be selected vmAssetList.ForceUpdateButtons(); - vmAssetList.AssertButtonState(SaveButtonState::Demote); + vmAssetList.AssertButtonState(SaveButtonState::SaveAllDisabled); } TEST_METHOD(TestSaveSelectedPublishLocal) diff --git a/tests/ui/viewmodels/AssetUploadViewModel_Tests.cpp b/tests/ui/viewmodels/AssetUploadViewModel_Tests.cpp index eca6103d1..ab3001947 100644 --- a/tests/ui/viewmodels/AssetUploadViewModel_Tests.cpp +++ b/tests/ui/viewmodels/AssetUploadViewModel_Tests.cpp @@ -1076,39 +1076,24 @@ TEST_CLASS(AssetUploadViewModel_Tests) TEST_METHOD(TestSingleLocalLeaderboard) { AssetUploadViewModelHarness vmUpload; + vmUpload.mockUserContext.Initialize("User", "APITOKEN"); auto& pLeaderboard = vmUpload.AddLeaderboard(AssetCategory::Local, L"Title1", L"Desc1", "0xH1234=1", "0xH1234=2", "0xH1234=3", "0xH2345", ra::data::Value::Format::Score); Assert::AreEqual(AssetChanges::Unpublished, pLeaderboard.GetChanges()); vmUpload.QueueAsset(pLeaderboard); Assert::AreEqual({ 1U }, vmUpload.TaskCount()); - bool bApiCalled = false; - vmUpload.mockServer.HandleRequest([&bApiCalled] - (const ra::api::UpdateLeaderboard::Request& pRequest, ra::api::UpdateLeaderboard::Response& pResponse) - { - bApiCalled = true; - Assert::AreEqual(AssetUploadViewModelHarness::GameId, pRequest.GameId); - Assert::AreEqual(std::wstring(L"Title1"), pRequest.Title); - Assert::AreEqual(std::wstring(L"Desc1"), pRequest.Description); - Assert::AreEqual(std::string("0xH1234=1"), pRequest.StartTrigger); - Assert::AreEqual(std::string("0xH1234=2"), pRequest.SubmitTrigger); - Assert::AreEqual(std::string("0xH1234=3"), pRequest.CancelTrigger); - Assert::AreEqual(std::string("0xH2345"), pRequest.ValueDefinition); - Assert::AreEqual(ra::data::Value::Format::Score, pRequest.Format); - Assert::IsFalse(pRequest.LowerIsBetter); - Assert::AreEqual(0U, pRequest.LeaderboardId); - - pResponse.LeaderboardId = 7716U; - pResponse.Result = ra::api::ApiResult::Success; - return true; - }); + vmUpload.mockRcClient.MockResponse( + "r=uploadleaderboard&u=User&t=APITOKEN&g=22&n=Title1&d=Desc1&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=0&f=SCORE&m=unpromoted&h=b9e48151adc764317296ac777ab550f1", + "{\"Success\":\"true\",\"LeaderboardID\":7716}" + ); vmUpload.DoUpload(); - Assert::IsTrue(bApiCalled); + vmUpload.mockRcClient.AssertNoPendingRequests(); - // published local leaderboard should be changed to promoted and have it's ID updated - Assert::AreEqual(AssetCategory::Promoted, pLeaderboard.GetCategory()); + // newly published local leaderboard should be changed to unpromoted and have it's ID updated + Assert::AreEqual(AssetCategory::Unpromoted, pLeaderboard.GetCategory()); Assert::AreEqual(7716U, pLeaderboard.GetID()); Assert::AreEqual(AssetChanges::None, pLeaderboard.GetChanges()); @@ -1118,6 +1103,7 @@ TEST_CLASS(AssetUploadViewModel_Tests) TEST_METHOD(TestSingleLocalLeaderboardSubset) { AssetUploadViewModelHarness vmUpload; + vmUpload.mockUserContext.Initialize("User", "APITOKEN"); vmUpload.mockGameContext.Assets().AddAchievementSet(22, 33, L"Subset", ra::data::models::AchievementSetType::Bonus); auto& pLeaderboard = vmUpload.AddLeaderboard(AssetCategory::Local, L"Title1", L"Desc1", "0xH1234=1", "0xH1234=2", "0xH1234=3", "0xH2345", ra::data::Value::Format::Score); @@ -1127,39 +1113,49 @@ TEST_CLASS(AssetUploadViewModel_Tests) vmUpload.QueueAsset(pLeaderboard); Assert::AreEqual({1U}, vmUpload.TaskCount()); - bool bApiCalled = false; - vmUpload.mockServer.HandleRequest( - [&bApiCalled](const ra::api::UpdateLeaderboard::Request& pRequest, - ra::api::UpdateLeaderboard::Response& pResponse) { - bApiCalled = true; - Assert::AreEqual(33U, pRequest.GameId); - Assert::AreEqual(std::wstring(L"Title1"), pRequest.Title); - Assert::AreEqual(std::wstring(L"Desc1"), pRequest.Description); - Assert::AreEqual(std::string("0xH1234=1"), pRequest.StartTrigger); - Assert::AreEqual(std::string("0xH1234=2"), pRequest.SubmitTrigger); - Assert::AreEqual(std::string("0xH1234=3"), pRequest.CancelTrigger); - Assert::AreEqual(std::string("0xH2345"), pRequest.ValueDefinition); - Assert::AreEqual(ra::data::Value::Format::Score, pRequest.Format); - Assert::IsFalse(pRequest.LowerIsBetter); - Assert::AreEqual(0U, pRequest.LeaderboardId); - - pResponse.LeaderboardId = 7716U; - pResponse.Result = ra::api::ApiResult::Success; - return true; - }); + vmUpload.mockRcClient.MockResponse( + "r=uploadleaderboard&u=User&t=APITOKEN&g=33&n=Title1&d=Desc1&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=0&f=SCORE&m=unpromoted&h=b9e48151adc764317296ac777ab550f1", + "{\"Success\":\"true\",\"LeaderboardID\":7716}" + ); vmUpload.DoUpload(); - Assert::IsTrue(bApiCalled); + vmUpload.mockRcClient.AssertNoPendingRequests(); - // published local leaderboard should be changed to promoted and have it's ID updated - Assert::AreEqual(AssetCategory::Promoted, pLeaderboard.GetCategory()); + // newly published local leaderboard should be changed to unpromoted and have it's ID updated + Assert::AreEqual(AssetCategory::Unpromoted, pLeaderboard.GetCategory()); Assert::AreEqual(7716U, pLeaderboard.GetID()); Assert::AreEqual(AssetChanges::None, pLeaderboard.GetChanges()); vmUpload.AssertSuccess(1); } + TEST_METHOD(TestSingleLocalLeaderboardNoClaim) + { + AssetUploadViewModelHarness vmUpload; + vmUpload.mockUserContext.Initialize("User", "APITOKEN"); + auto& pLeaderboard = vmUpload.AddLeaderboard(AssetCategory::Local, L"Title1", L"Desc1", "0xH1234=1", "0xH1234=2", "0xH1234=3", "0xH2345", ra::data::Value::Format::Score); + Assert::AreEqual(AssetChanges::Unpublished, pLeaderboard.GetChanges()); + + vmUpload.QueueAsset(pLeaderboard); + Assert::AreEqual({ 1U }, vmUpload.TaskCount()); + + vmUpload.mockRcClient.MockResponse( + "r=uploadleaderboard&u=User&t=APITOKEN&g=22&n=Title1&d=Desc1&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=0&f=SCORE&m=unpromoted&h=b9e48151adc764317296ac777ab550f1", + "{\"Success\":false,\"Status\":403,\"Code\":\"access_denied\",\"Error\":\"You must have an active claim on this game to perform this action.\"}" + ); + + vmUpload.DoUpload(); + + vmUpload.mockRcClient.AssertNoPendingRequests(); + + // newly published local leaderboard should be changed to unofficial and have it's ID updated + Assert::AreEqual(AssetCategory::Local, pLeaderboard.GetCategory()); + Assert::AreEqual(AssetChanges::Unpublished, pLeaderboard.GetChanges()); + + vmUpload.AssertFailed(0, 1, L"* Title1: You must have an active claim on this game to perform this action."); + } + TEST_METHOD(TestRichPresence) { AssetUploadViewModelHarness vmUpload; @@ -1442,9 +1438,6 @@ TEST_CLASS(AssetUploadViewModel_Tests) AssetUploadViewModelHarness vmUpload; vmUpload.mockUserContext.Initialize("User", "APITOKEN"); - const std::string sLastRequest = "r=submitcodenotes&u=User&t=APITOKEN&g=22&n=5296:Note+5296%0a5298:Note+5298%0a5300:Note+5300%0a5302:Note+5302%0a5304:Note+5304%0a5306:Note+5306%0a5308:Note+5308%0a5310:Note+5310%0a5312:Note+5312%0a5314:Note+5314%0a5316:Note+5316%0a5318:Note+5318%0a5320:Note+5320%0a5322:Note+5322%0a"; - vmUpload.mockRcClient.MockResponse(sLastRequest, "", 504); // dummy timeout response, but allows us to see if the request got called - constexpr size_t NumNotes = 614; for (uint32_t nAddress = 0x1000; nAddress < 0x1000 + NumNotes * 2; nAddress += 2) vmUpload.MemoryNotes().SetNote(nAddress, ra::util::String::Printf(L"Note %u", nAddress)); @@ -1461,7 +1454,6 @@ TEST_CLASS(AssetUploadViewModel_Tests) Assert::AreEqual({ 100 }, vmUpload.GetProgress()); // 100% vmUpload.mockRcClient.AssertNumRequestsHandled(7); // 6 * 100 + 1 * 14 - vmUpload.mockRcClient.AssertCalled(sLastRequest); } TEST_METHOD(TestMultipleMemoryNotesWithNewlines)