From ab1013383e0692751387466058e2d323ac956ea9 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 22:42:33 -0300 Subject: [PATCH 01/28] Finally clang-format handles newline at EOF :tada: Requires LLVM 16. CI already ensures that for free. --- .clang-format | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.clang-format b/.clang-format index 3b08f9ba0..18a59e8e5 100644 --- a/.clang-format +++ b/.clang-format @@ -1,5 +1,7 @@ BasedOnStyle: Google --- Language: Cpp +AllowShortFunctionsOnASingleLine: All DerivePointerAlignment: false +InsertNewlineAtEOF: true PointerAlignment: Left From 2ec64438f1b1c44f574a363919c6cb69f5883860 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 13:43:10 -0300 Subject: [PATCH 02/28] The code we want to write at the high level The purchase method must now be callback based That means the result will be provided to the callback That implies the method channel `result` must be a shared_ptr Since we may need to access it in the exception handler as well. Since fetching a product will be blocking we need to call it on a background thread Purchasing needs to start on the main (UI) thread, though All exceptions are captured. Note: The DefaultContext header must select what Context impl tol use. More headers being included to be pedantic (don't accept transitive) Rule: don't #include in cpp what my component header provides directly. --- .../windows/p4w_ms_store_plugin_impl.cpp | 48 +++++++++++++------ .../windows/p4w_ms_store_plugin_impl.h | 11 ++++- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/gui/packages/p4w_ms_store/windows/p4w_ms_store_plugin_impl.cpp b/gui/packages/p4w_ms_store/windows/p4w_ms_store_plugin_impl.cpp index 46b3edc23..b21d47d86 100644 --- a/gui/packages/p4w_ms_store/windows/p4w_ms_store_plugin_impl.cpp +++ b/gui/packages/p4w_ms_store/windows/p4w_ms_store_plugin_impl.cpp @@ -1,26 +1,44 @@ #include "p4w_ms_store_plugin_impl.h" -#include - +#include +#include +#include #include +#include +#include + namespace p4w_ms_store { winrt::fire_and_forget PurchaseSubscription( HWND topLevelWindow, std::string productId, - std::unique_ptr> result) { - try { - StoreApi::ClientStoreService service{topLevelWindow}; - const auto res = - co_await service.PromptUserToSubscribe(std::move(productId)); - result->Success(static_cast(res)); - } catch (StoreApi::Exception& err) { - result->Error(channelName, err.what()); - } catch (winrt::hresult_error& err) { - result->Error(channelName, winrt::to_string(err.message())); - } catch (std::exception& err) { - result->Error(channelName, err.what()); - } + std::shared_ptr> + result) try { + winrt::apartment_context windowContext{}; + StoreApi::ClientStoreService service{topLevelWindow}; + // Blocks a background thread while retrieving the product. + co_await winrt::resume_background(); + const auto product = service.FetchAvailableProduct(productId); + // Resumes in the UI thread to display the native dialog. + co_await windowContext; + product.PromptUserForPurchase( + [result](StoreApi::PurchaseStatus status, int32_t error) { + if (error < 0) { + winrt::hresult_error err{error}; + result->Error(channelName, winrt::to_string(err.message())); + return; + } + + result->Success(static_cast(status)); + }); +} catch (StoreApi::Exception& err) { + result->Error(channelName, err.what()); +} catch (winrt::hresult_error& err) { + result->Error(channelName, winrt::to_string(err.message())); +} catch (std::exception& err) { + result->Error(channelName, err.what()); +} catch (...) { + result->Error(channelName, "Unknown exception thrown in the native layer."); } } // namespace p4w_ms_store diff --git a/gui/packages/p4w_ms_store/windows/p4w_ms_store_plugin_impl.h b/gui/packages/p4w_ms_store/windows/p4w_ms_store_plugin_impl.h index 290e9e44a..9765cad2b 100644 --- a/gui/packages/p4w_ms_store/windows/p4w_ms_store_plugin_impl.h +++ b/gui/packages/p4w_ms_store/windows/p4w_ms_store_plugin_impl.h @@ -1,9 +1,16 @@ #ifndef FLUTTER_PLUGIN_P4W_MS_STORE_PLUGIN_IMPL_H_ #define FLUTTER_PLUGIN_P4W_MS_STORE_PLUGIN_IMPL_H_ +#include +#include + +#include #include #include -#include + +#include +#include + namespace p4w_ms_store { @@ -19,7 +26,7 @@ inline HWND GetRootWindow(flutter::FlutterView* view) { winrt::fire_and_forget PurchaseSubscription( HWND topLevelWindow, std::string productId, - std::unique_ptr> result); + std::shared_ptr> result); } // namespace p4w_ms_store From 22cbac412e14145b913520883a608530f52a6457 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 13:45:50 -0300 Subject: [PATCH 03/28] Add support to select the Context impl in build The Flutter plugin Windows specific CMakeLists.txt is changed. Set the env var TEST_WITH_MS_STORE_MOCK. The plugin will not be built with the production version of Context. --- .../p4w_ms_store/windows/CMakeLists.txt | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/gui/packages/p4w_ms_store/windows/CMakeLists.txt b/gui/packages/p4w_ms_store/windows/CMakeLists.txt index f58106716..a4c62df64 100644 --- a/gui/packages/p4w_ms_store/windows/CMakeLists.txt +++ b/gui/packages/p4w_ms_store/windows/CMakeLists.txt @@ -60,14 +60,26 @@ include_directories(BEFORE SYSTEM ${CMAKE_BINARY_DIR}/include) # Relative to the top level cmake project which will be either this plugin or the app # consuming it. Either way, same relative distance to the directory we are looking for. +set(STORE_API_DEFINES "") set(STORE_API_DIR "${CMAKE_SOURCE_DIR}/../../../../storeapi/") set(STORE_API_SRC "${STORE_API_DIR}/gui/ClientStoreService.hpp" "${STORE_API_DIR}/base/StoreService.hpp" + "${STORE_API_DIR}/base/Purchase.hpp" "${STORE_API_DIR}/base/Exception.hpp" - "${STORE_API_DIR}/base/Context.cpp" - "${STORE_API_DIR}/base/Context.hpp" + "${STORE_API_DIR}/base/DefaultContext.hpp" ) +if(DEFINED ENV{TEST_WITH_MS_STORE_MOCK}) + # TODO: Change to warning to be informative and include the appropriate files. + message(FATAL_ERROR "Unsupported build with the MS Store Mock client API due environment variable 'TEST_WITH_MS_STORE_MOCK' set to '$ENV{TEST_WITH_MS_STORE_MOCK}'.") + list(APPEND STORE_API_DEFINES "TEST_WITH_MS_STORE_MOCK") +else() + message(STATUS "Building with the production version of MS Store client API. Set the environment variable 'TEST_WITH_MS_STORE_MOCK' if you want to build with the mock store API.") + list(APPEND STORE_API_SRC + "${STORE_API_DIR}/base/impl/StoreContext.cpp" + "${STORE_API_DIR}/base/impl/StoreContext.hpp" + ) +endif() # Any new source files that you add to the plugin should be added here. list(APPEND PLUGIN_SOURCES @@ -97,6 +109,8 @@ apply_standard_settings(${PLUGIN_NAME}) set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +# Further #defines we may want to pass. +target_compile_definitions(${PLUGIN_NAME} PRIVATE ${STORE_API_DEFINES}) # Source include directories and library dependencies. Add any plugin-specific # dependencies here. From 759ba7ebbe56899faf3f054eb60959885df88a04 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 13:50:37 -0300 Subject: [PATCH 04/28] Purchase enum and callback goes to its own file The enum is platform agnostic. The callback type declaration as well. Both will be required in different places. The translate function is specific to WinRT. Thus, it will be restored in the specific context impl later. --- storeapi/base/Purchase.hpp | 30 ++++++++++++++++++++++++ storeapi/gui/ClientStoreService.hpp | 36 ----------------------------- 2 files changed, 30 insertions(+), 36 deletions(-) create mode 100644 storeapi/base/Purchase.hpp diff --git a/storeapi/base/Purchase.hpp b/storeapi/base/Purchase.hpp new file mode 100644 index 000000000..98fb8b086 --- /dev/null +++ b/storeapi/base/Purchase.hpp @@ -0,0 +1,30 @@ +#pragma once + +/// Types and aliases that must be present in all Context implementations. + +#include +#include + +namespace StoreApi { +// We'll certainly want to show in the UI the result of the purchase operation +// in a localizable way. Thus we must agree on the values returned across the +// different languages involved. Since we don't control the Windows Runtime +// APIs, it wouldn't be future-proof to return the raw value of +// StorePurchaseStatus enum right away. +// This must strictly in sync the Dart PurchaseStatus enum in +// https://github.com/canonical/ubuntu-pro-for-windows/blob/main/gui/packages/p4w_ms_store/lib/p4w_ms_store_platform_interface.dart#L8-L16 +// so we don't misinterpret the native call return values. +enum class PurchaseStatus : std::int8_t { + Succeeded = 0, + AlreadyPurchased = 1, + UserGaveUp = 2, + NetworkError = 3, + ServerError = 4, + Unknown = 5, +}; + +/// A callable the client application must provide to receive the result of the +/// asynchronous purchase operation. +using PurchaseCallback = std::function; + +} // namespace StoreApi \ No newline at end of file diff --git a/storeapi/gui/ClientStoreService.hpp b/storeapi/gui/ClientStoreService.hpp index 6fcb19980..e24794265 100644 --- a/storeapi/gui/ClientStoreService.hpp +++ b/storeapi/gui/ClientStoreService.hpp @@ -3,42 +3,6 @@ #include namespace StoreApi { - -// We'll certainly want to show in the UI the result of the purchase operation -// in a localizable way. Thus we must agree on the values returned across the -// different languages involved. Since we don't control the Windows Runtime -// APIs, it wouldn't be future-proof to return the raw value of -// StorePurchaseStatus enum right away. -// This must strictly in sync the Dart PurchaseStatus enum in -// https://github.com/canonical/ubuntu-pro-for-windows/blob/main/gui/packages/p4w_ms_store/lib/p4w_ms_store_platform_interface.dart#L8-L16 -// so we don't misinterpret the native call return values. -enum class PurchaseStatus : std::int8_t { - Succeeded = 0, - AlreadyPurchased = 1, - UserGaveUp = 2, - NetworkError = 3, - ServerError = 4, - Unknown = 5, -}; - -PurchaseStatus translate( - winrt::Windows::Services::Store::StorePurchaseStatus purchaseStatus) { - using winrt::Windows::Services::Store::StorePurchaseStatus; - switch (purchaseStatus) { - case StorePurchaseStatus::Succeeded: - return PurchaseStatus::Succeeded; - case StorePurchaseStatus::AlreadyPurchased: - return PurchaseStatus::AlreadyPurchased; - case StorePurchaseStatus::NotPurchased: - return PurchaseStatus::UserGaveUp; - case StorePurchaseStatus::NetworkError: - return PurchaseStatus::NetworkError; - case StorePurchaseStatus::ServerError: - return PurchaseStatus::ServerError; - } - return PurchaseStatus::Unknown; // To be future proof. -} - // Adds functionality on top of the [StoreService] interesting to Client UI // applications. template From 3eeaac9a74a27d8c288ceee8fe0c9adea6f40024 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 13:53:55 -0300 Subject: [PATCH 05/28] Separates fetching the product from purchasing it The product purchase method is made protected So it cannot be called outside of its hierarchy. There was no such hierarchy. The AvailableProduct is just a type system trick. Those who use ClientStoreService can call purchase. Those who doesn't, can't. A cheap API enhancement, since calling purchase has specific preconditions The type system trick turns impossible to break them. Product constructor was added because we made self private. --- storeapi/base/Context.hpp | 8 ++++-- storeapi/base/Exception.hpp | 1 + storeapi/gui/ClientStoreService.hpp | 41 ++++++++++++++++++----------- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/storeapi/base/Context.hpp b/storeapi/base/Context.hpp index a681b7a7a..b74a5a988 100644 --- a/storeapi/base/Context.hpp +++ b/storeapi/base/Context.hpp @@ -27,8 +27,8 @@ class Context { // direct usage in high level code. The API is loose, the caller services must // tighten it up. struct Product { - winrt::Windows::Services::Store::StoreProduct self{nullptr}; - + public: + Product(winrt::Windows::Services::Store::StoreProduct self) : self{self} {} // Whether the current user owns this product. bool IsInUserCollection() { return self.IsInUserCollection(); } @@ -36,6 +36,7 @@ class Context { // returns the expiration date of the current billing period. winrt::Windows::Foundation::DateTime CurrentExpirationDate(); + protected: // Assuming this is a Subcription add-on product the current user __does not // own__, requests the runtime to display a purchase flow so users can // subscribe to this product. This must be called from a UI thread with the @@ -45,6 +46,9 @@ class Context { winrt::Windows::Foundation::IAsyncOperation< winrt::Windows::Services::Store::StorePurchaseStatus> PromptUserForPurchase(); + + private: + winrt::Windows::Services::Store::StoreProduct self{nullptr}; }; // Returns a collection of products matching the supplied [kinds] and [ids]. diff --git a/storeapi/base/Exception.hpp b/storeapi/base/Exception.hpp index c1287578a..b5cfe3e7b 100644 --- a/storeapi/base/Exception.hpp +++ b/storeapi/base/Exception.hpp @@ -12,6 +12,7 @@ enum class ErrorCode { NoLocalUser, TooManyLocalUsers, EmptyJwt, + InvalidProductId, // ABI Boundary errors: AllocationFailure = -10, // - input string argument errors diff --git a/storeapi/gui/ClientStoreService.hpp b/storeapi/gui/ClientStoreService.hpp index e24794265..5390f4718 100644 --- a/storeapi/gui/ClientStoreService.hpp +++ b/storeapi/gui/ClientStoreService.hpp @@ -1,11 +1,15 @@ #pragma once -#include +#include +#include #include +#include +#include +#include namespace StoreApi { // Adds functionality on top of the [StoreService] interesting to Client UI // applications. -template +template class ClientStoreService : public StoreService { public: // Initializes a client store service with the top level window handle so the @@ -13,24 +17,31 @@ class ClientStoreService : public StoreService { // desirable to have the supplied window handle referring to a stable window, // so we don't incur in handle reuse problems. The top level window that // doesn't change throughout the app lifetime is the best candidate. - ClientStoreService(HWND topLevelWindow) { + explicit ClientStoreService(ContextType::Window topLevelWindow) { this->context.InitDialogs(topLevelWindow); } - // Requests the runtime to display the purchase flow dialog for the - // [productId]. - concurrency::task PromptUserToSubscribe( - std::string productId) { - auto product = co_await this->GetSubscriptionProduct(productId); + /// Leverages the type system to promote access to the PromptUserForPurchase() + /// method on Product, which should not be available on non-GUI clients. + class AvailableProduct : public ContextType::Product { + using base = ContextType::Product; + + public: + using base::PromptUserForPurchase; + AvailableProduct(base B) : base::Product{B} {} + }; + + /// Fetches a subscription product matching the provided product ID available + /// for purchase. An Exception is thrown if the product is already purchased + /// or not found. + AvailableProduct FetchAvailableProduct(std::string productId) { + auto product = this->GetSubscriptionProduct(productId); if (product.IsInUserCollection() && - product.CurrentExpirationDate() > winrt::clock::now()) { - // No need to purchase this, right? It would end up with - // the following status: - co_return PurchaseStatus::AlreadyPurchased; + product.CurrentExpirationDate() > std::chrono::system_clock::now()) { + throw Exception(ErrorCode::InvalidProductId, + std::format("product {} already purchased", productId)); } - - auto res = co_await product.PromptUserForPurchase(); - co_return translate(res); + return {product}; } }; From b15b31435c7705f5f6e83305decb6bda5063642a Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 14:51:41 -0300 Subject: [PATCH 06/28] Renames the Context impl Context => impl/StoreContext --- storeapi/base/{Context.cpp => impl/StoreContext.cpp} | 0 storeapi/base/{Context.hpp => impl/StoreContext.hpp} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename storeapi/base/{Context.cpp => impl/StoreContext.cpp} (100%) rename storeapi/base/{Context.hpp => impl/StoreContext.hpp} (100%) diff --git a/storeapi/base/Context.cpp b/storeapi/base/impl/StoreContext.cpp similarity index 100% rename from storeapi/base/Context.cpp rename to storeapi/base/impl/StoreContext.cpp diff --git a/storeapi/base/Context.hpp b/storeapi/base/impl/StoreContext.hpp similarity index 100% rename from storeapi/base/Context.hpp rename to storeapi/base/impl/StoreContext.hpp From a87be14937bdb27ca2e250d556c5b94978306c20 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 14:55:55 -0300 Subject: [PATCH 07/28] Completes the renaming By fixing relative #includes Nested namspace impl And renaming the class: Context => :StoreContext --- storeapi/base/impl/StoreContext.cpp | 16 ++++++++-------- storeapi/base/impl/StoreContext.hpp | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/storeapi/base/impl/StoreContext.cpp b/storeapi/base/impl/StoreContext.cpp index 5079ea7cf..8572c53e3 100644 --- a/storeapi/base/impl/StoreContext.cpp +++ b/storeapi/base/impl/StoreContext.cpp @@ -1,12 +1,12 @@ -#include "Context.hpp" +#include "StoreContext.hpp" #include #include -#include "Exception.hpp" +#include "../Exception.hpp" -namespace StoreApi { +namespace StoreApi::impl { using concurrency::task; using winrt::Windows::Foundation::DateTime; @@ -16,7 +16,7 @@ using winrt::Windows::Services::Store::StoreProductQueryResult; using winrt::Windows::Services::Store::StorePurchaseStatus; using winrt::Windows::Services::Store::StoreSku; -DateTime Context::Product::CurrentExpirationDate() { +DateTime StoreContext::Product::CurrentExpirationDate() { // A single product might have more than one SKU. for (auto sku : self.Skus()) { if (sku.IsInUserCollection() && sku.IsSubscription()) { @@ -32,14 +32,14 @@ DateTime Context::Product::CurrentExpirationDate() { }; } -IAsyncOperation Context::Product::PromptUserForPurchase() { +IAsyncOperation StoreContext::Product::PromptUserForPurchase() { const auto& res = co_await self.RequestPurchaseAsync(); // throws winrt::hresult_error if query contains an error HRESULT. winrt::check_hresult(res.ExtendedError()); co_return res.Status(); } -task> Context::GetProducts( +task> StoreContext::GetProducts( std::vector kinds, std::vector ids) { // Gets Microsoft Store listing info for the specified products that are // associated with the current app. Requires "arrays" of product kinds and @@ -56,7 +56,7 @@ task> Context::GetProducts( co_return products; } -void Context::InitDialogs(HWND parentWindow) { +void StoreContext::InitDialogs(HWND parentWindow) { // Apps that do not feature a [CoreWindow] must inform the runtime the parent // window handle in order to render runtime provided UI elements, such as // authorization and purchase dialogs. @@ -64,4 +64,4 @@ void Context::InitDialogs(HWND parentWindow) { iiw->Initialize(parentWindow); } -} // namespace StoreApi +} // namespace StoreApi::impl diff --git a/storeapi/base/impl/StoreContext.hpp b/storeapi/base/impl/StoreContext.hpp index b74a5a988..05eddc57c 100644 --- a/storeapi/base/impl/StoreContext.hpp +++ b/storeapi/base/impl/StoreContext.hpp @@ -15,10 +15,10 @@ // For HWND and GUI-related Windows types. #include -namespace StoreApi { +namespace StoreApi::impl { // Wraps MS StoreContext type for testability purposes. -class Context { +class StoreContext { winrt::Windows::Services::Store::StoreContext self = winrt::Windows::Services::Store::StoreContext::GetDefault(); @@ -71,4 +71,4 @@ class Context { void InitDialogs(HWND parentWindow); }; -} // namespace StoreApi +} // namespace StoreApi::impl From ec4da56a70e7120fb00af17772c61b21d82e51aa Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 15:05:20 -0300 Subject: [PATCH 08/28] The DefaultContext header Selects which context is appropriate Whether mocking the store and if platform is Windows or not. --- storeapi/base/DefaultContext.hpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 storeapi/base/DefaultContext.hpp diff --git a/storeapi/base/DefaultContext.hpp b/storeapi/base/DefaultContext.hpp new file mode 100644 index 000000000..d83c10e40 --- /dev/null +++ b/storeapi/base/DefaultContext.hpp @@ -0,0 +1,16 @@ +#pragma once + +#ifdef TEST_WITH_MS_STORE_MOCK +// TODO: Handle the mock case +#ifdef _MSC_VER +// windows specific mocked impl (may use winrt) +#else // _MSC_VER +// non-windows specific mocked impl (Linux friendly) +#endif // _MSC_VER + +#else // TEST_WITH_MS_STORE_MOCK +#include "impl/StoreContext.hpp" +namespace StoreApi { +using DefaultContext = impl::StoreContext; +} +#endif // TEST_WITH_MS_STORE_MOCK From a08105c5b82fd4fd4cc1c9bbd225b34d3d84ef6c Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 15:15:19 -0300 Subject: [PATCH 09/28] Sync and non-windows specific base StoreService No ppl or winrt, no coroutines GetsubscriptionProduct is now blocking. Should not be called in UI thread. Ok for the agent. --- storeapi/base/StoreService.hpp | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/storeapi/base/StoreService.hpp b/storeapi/base/StoreService.hpp index 2bb78ee37..c38679f0e 100644 --- a/storeapi/base/StoreService.hpp +++ b/storeapi/base/StoreService.hpp @@ -1,11 +1,8 @@ #pragma once -// For the underlying Store API -#include - -// To provide coroutines capable of returning non-WinRT types. -#include +#include #include +#include #include "Exception.hpp" @@ -20,27 +17,26 @@ class StoreService { // The underlying store context. ContextType context{}; // We only care about subscription add-ons. - static constexpr wchar_t _productKind[] = L"Durable"; + static constexpr char _productKind[] = "Durable"; - // An asynchronous operation that eventually returns an instance of - // [ContextType::Product] subscription add-on matching the provided product - // [id]. Callers must ensure the underlying string pointed by the [id] - // remains valid until this function completes. - concurrency::task GetSubscriptionProduct( - std::string_view id) { - auto products = - co_await context.GetProducts({{_productKind}}, {winrt::to_hstring(id)}); + // A blocking operation that returns an instance of [ContextType::Product] + // subscription add-on matching the provided product [id]. + typename ContextType::Product GetSubscriptionProduct(std::string id) { + std::array ids{std::move(id)}; + std::array kinds{_productKind}; + auto products = context.GetProducts(kinds, ids); auto size = products.size(); switch (size) { case 0: - throw Exception(ErrorCode::NoProductsFound, std::format("id={}", id)); + throw Exception(ErrorCode::NoProductsFound, + std::format("id={}", ids[0])); case 1: - co_return products[0]; + return products[0]; default: throw Exception( ErrorCode::TooManyProductsFound, std::format("Expected one but found {} products for id {}", size, - id)); + ids[0])); } } }; From 71e985cb376f0c966d98879ae46a68f0c81baa52 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 15:24:36 -0300 Subject: [PATCH 10/28] Meets StoreService expectation of sync GetProducts and non-windows specific. That means no coroutines nor winrt::hstrings. to_hstrings helper is needed to convert a collection of strings into a collection of hstrings suitable for async operations. See https://learn.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/pass-parms-to-abi#iterable-parameters --- storeapi/base/impl/StoreContext.cpp | 24 ++++++++++++++++++++---- storeapi/base/impl/StoreContext.hpp | 7 +++++-- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/storeapi/base/impl/StoreContext.cpp b/storeapi/base/impl/StoreContext.cpp index 8572c53e3..ab9a9b733 100644 --- a/storeapi/base/impl/StoreContext.cpp +++ b/storeapi/base/impl/StoreContext.cpp @@ -16,6 +16,12 @@ using winrt::Windows::Services::Store::StoreProductQueryResult; using winrt::Windows::Services::Store::StorePurchaseStatus; using winrt::Windows::Services::Store::StoreSku; +namespace { +// Converts a span of strings into a vector of hstrings, needed when passing +// a collection of string as a parameter to an async operation. +std::vector to_hstrings(std::span input); +} // namespace + DateTime StoreContext::Product::CurrentExpirationDate() { // A single product might have more than one SKU. for (auto sku : self.Skus()) { @@ -39,13 +45,13 @@ IAsyncOperation StoreContext::Product::PromptUserForPurchas co_return res.Status(); } -task> StoreContext::GetProducts( - std::vector kinds, std::vector ids) { +std::vector StoreContext::GetProducts( + std::span kinds, std::span ids) { // Gets Microsoft Store listing info for the specified products that are // associated with the current app. Requires "arrays" of product kinds and // ids. StoreProductQueryResult query = - co_await self.GetStoreProductsAsync(std::move(kinds), std::move(ids)); + self.GetStoreProductsAsync(to_hstrings(kinds), to_hstrings(ids)).get(); winrt::check_hresult(query.ExtendedError()); std::vector products; @@ -53,7 +59,7 @@ task> StoreContext::GetProducts( for (auto p : query.Products()) { products.emplace_back(p.Value()); } - co_return products; + return products; } void StoreContext::InitDialogs(HWND parentWindow) { @@ -64,4 +70,14 @@ void StoreContext::InitDialogs(HWND parentWindow) { iiw->Initialize(parentWindow); } +namespace { +std::vector to_hstrings(std::span input) { + std::vector hStrs; + hStrs.reserve(input.size()); + std::ranges::transform(input, std::back_inserter(hStrs), + &winrt::to_hstring); + return hStrs; +} +} // namespace + } // namespace StoreApi::impl diff --git a/storeapi/base/impl/StoreContext.hpp b/storeapi/base/impl/StoreContext.hpp index 05eddc57c..1943f7f4e 100644 --- a/storeapi/base/impl/StoreContext.hpp +++ b/storeapi/base/impl/StoreContext.hpp @@ -15,6 +15,9 @@ // For HWND and GUI-related Windows types. #include +#include +#include + namespace StoreApi::impl { // Wraps MS StoreContext type for testability purposes. @@ -55,8 +58,8 @@ class StoreContext { // Ids must match the Product IDs in Partner Center. Kinds can be: // Application; Game; Consumable; UnmanagedConsumable; Durable. See // https://learn.microsoft.com/en-us/uwp/api/windows.services.store.storeproduct.productkind#remarks - concurrency::task> GetProducts( - std::vector kinds, std::vector ids); + std::vector GetProducts(std::span kinds, + std::span ids); // Generates the user ID key (a.k.a the JWT) provided the server AAD [hToken] // and the [hUserId] the caller wants to have encoded in the JWT. From f4698c963c17e3815ec524ec9f7fefd5d5a028a1 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 15:29:34 -0300 Subject: [PATCH 11/28] Meets ClientStoreService expectation of no DateTime That's WinRT specific. Instead we return std::chrono::system_clock::time_point; which is standard, thus no Windows specific. Also, this makes that method const. There is no mutation around it. --- storeapi/base/impl/StoreContext.cpp | 6 +++--- storeapi/base/impl/StoreContext.hpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/storeapi/base/impl/StoreContext.cpp b/storeapi/base/impl/StoreContext.cpp index ab9a9b733..292054aaa 100644 --- a/storeapi/base/impl/StoreContext.cpp +++ b/storeapi/base/impl/StoreContext.cpp @@ -9,7 +9,6 @@ namespace StoreApi::impl { using concurrency::task; -using winrt::Windows::Foundation::DateTime; using winrt::Windows::Foundation::IAsyncOperation; using winrt::Windows::Services::Store::StoreProduct; using winrt::Windows::Services::Store::StoreProductQueryResult; @@ -22,12 +21,13 @@ namespace { std::vector to_hstrings(std::span input); } // namespace -DateTime StoreContext::Product::CurrentExpirationDate() { +std::chrono::system_clock::time_point +StoreContext::Product::CurrentExpirationDate() const { // A single product might have more than one SKU. for (auto sku : self.Skus()) { if (sku.IsInUserCollection() && sku.IsSubscription()) { auto collected = sku.CollectionData(); - return collected.EndDate(); + return winrt::clock::to_sys(collected.EndDate()); } } diff --git a/storeapi/base/impl/StoreContext.hpp b/storeapi/base/impl/StoreContext.hpp index 1943f7f4e..4f2ff47ea 100644 --- a/storeapi/base/impl/StoreContext.hpp +++ b/storeapi/base/impl/StoreContext.hpp @@ -35,9 +35,9 @@ class StoreContext { // Whether the current user owns this product. bool IsInUserCollection() { return self.IsInUserCollection(); } - // Assuming this is a Subcription add-on product the current user __owns__, + // Assuming this is a Subscription add-on product the current user __owns__, // returns the expiration date of the current billing period. - winrt::Windows::Foundation::DateTime CurrentExpirationDate(); + std::chrono::system_clock::time_point CurrentExpirationDate() const; protected: // Assuming this is a Subcription add-on product the current user __does not From b05f13fa49e46a94857d5bc7674a63c5579b8fd1 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 15:35:06 -0300 Subject: [PATCH 12/28] Implements debug_assert macro on Exception.hpp Using std::source_location like the exception does. Pedantic #inlude check: avoid relying on transitive includes. --- storeapi/base/Exception.hpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/storeapi/base/Exception.hpp b/storeapi/base/Exception.hpp index b5cfe3e7b..ab98673cd 100644 --- a/storeapi/base/Exception.hpp +++ b/storeapi/base/Exception.hpp @@ -1,5 +1,10 @@ #pragma once +#include +#include +#include #include +#include +#include namespace StoreApi { @@ -72,4 +77,21 @@ class Exception { m_loc.file_name(), m_loc.line(), m_loc.function_name()); } }; + +#ifdef NDEBUG +#define debug_assert(expr, msg) ((void)0) +#else // NDEBUG +inline void AssertFail( + std::string_view condition, std::string_view msg, + std::source_location loc = std::source_location::current()) { + std::cerr << std::format( + "[ASSERTION FAILURE]: {}:{} {}\n\tunmet condition: {} ({})\n", + loc.file_name(), loc.line(), loc.function_name(), condition, msg); + + std::terminate(); +} +#define debug_assert(expr, msg) \ + (static_cast(expr) ? ((void)0) : AssertFail("'" #expr "'", msg)) +#endif // NDEBUG + } // namespace StoreApi From de23a2c80629db6ae617b32f41cc532a453d6e44 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 15:44:53 -0300 Subject: [PATCH 13/28] Applies debug_assert on GetProducts --- storeapi/base/impl/StoreContext.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/storeapi/base/impl/StoreContext.cpp b/storeapi/base/impl/StoreContext.cpp index 292054aaa..6a8976f8c 100644 --- a/storeapi/base/impl/StoreContext.cpp +++ b/storeapi/base/impl/StoreContext.cpp @@ -47,6 +47,8 @@ IAsyncOperation StoreContext::Product::PromptUserForPurchas std::vector StoreContext::GetProducts( std::span kinds, std::span ids) { + debug_assert(!kinds.empty(), "kinds vector cannot be empty"); + debug_assert(!ids.empty(), "ids vector cannot be empty"); // Gets Microsoft Store listing info for the specified products that are // associated with the current app. Requires "arrays" of product kinds and // ids. From e70146ed365e8dc26bad2a569b92060ef53595df Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 15:38:04 -0300 Subject: [PATCH 14/28] ClientStoreService expects a nested Window type or type alias So we can hide the HWND. Here it's just a type alias, but a Linux impl might be an integer wrapped in a struct e.g. --- storeapi/base/impl/StoreContext.cpp | 2 +- storeapi/base/impl/StoreContext.hpp | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/storeapi/base/impl/StoreContext.cpp b/storeapi/base/impl/StoreContext.cpp index 6a8976f8c..ab0ade115 100644 --- a/storeapi/base/impl/StoreContext.cpp +++ b/storeapi/base/impl/StoreContext.cpp @@ -64,7 +64,7 @@ std::vector StoreContext::GetProducts( return products; } -void StoreContext::InitDialogs(HWND parentWindow) { +void StoreContext::InitDialogs(Window parentWindow) { // Apps that do not feature a [CoreWindow] must inform the runtime the parent // window handle in order to render runtime provided UI elements, such as // authorization and purchase dialogs. diff --git a/storeapi/base/impl/StoreContext.hpp b/storeapi/base/impl/StoreContext.hpp index 4f2ff47ea..5ec196786 100644 --- a/storeapi/base/impl/StoreContext.hpp +++ b/storeapi/base/impl/StoreContext.hpp @@ -26,6 +26,7 @@ class StoreContext { winrt::Windows::Services::Store::StoreContext::GetDefault(); public: + using Window = HWND; // Wraps MS StoreProduct type for testability purposes. This is not meant for // direct usage in high level code. The API is loose, the caller services must // tighten it up. @@ -71,7 +72,7 @@ class StoreContext { // Initializes the GUI "subsystem" with the [parentWindow] handle so we can // render native dialogs, such as when purchase or other kinds of // authorization are required. - void InitDialogs(HWND parentWindow); + void InitDialogs(Window parentWindow); }; } // namespace StoreApi::impl From 975161802901eb32293d720b7557fac27b84e916 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 15:47:33 -0300 Subject: [PATCH 15/28] Callback based PromptUserForPurchase Thus, non-blocking, meeting the expectation of ClientStoreService. It sets a completion handler to the IAsyncOperation that calls the callback with the results already translated to our domain, thus avoiding WinRT types to leak. --- storeapi/base/impl/StoreContext.cpp | 42 +++++++++++++++++++++++++---- storeapi/base/impl/StoreContext.hpp | 20 ++++++++------ 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/storeapi/base/impl/StoreContext.cpp b/storeapi/base/impl/StoreContext.cpp index ab0ade115..509a9109b 100644 --- a/storeapi/base/impl/StoreContext.cpp +++ b/storeapi/base/impl/StoreContext.cpp @@ -9,9 +9,11 @@ namespace StoreApi::impl { using concurrency::task; +using winrt::Windows::Foundation::AsyncStatus; using winrt::Windows::Foundation::IAsyncOperation; using winrt::Windows::Services::Store::StoreProduct; using winrt::Windows::Services::Store::StoreProductQueryResult; +using winrt::Windows::Services::Store::StorePurchaseResult; using winrt::Windows::Services::Store::StorePurchaseStatus; using winrt::Windows::Services::Store::StoreSku; @@ -19,6 +21,9 @@ namespace { // Converts a span of strings into a vector of hstrings, needed when passing // a collection of string as a parameter to an async operation. std::vector to_hstrings(std::span input); + +// Translates a [StorePurchaseStatus] into the [PurchaseStatus] enum. +PurchaseStatus translate(StorePurchaseStatus purchaseStatus) noexcept; } // namespace std::chrono::system_clock::time_point @@ -38,11 +43,21 @@ StoreContext::Product::CurrentExpirationDate() const { }; } -IAsyncOperation StoreContext::Product::PromptUserForPurchase() { - const auto& res = co_await self.RequestPurchaseAsync(); - // throws winrt::hresult_error if query contains an error HRESULT. - winrt::check_hresult(res.ExtendedError()); - co_return res.Status(); +void StoreContext::Product::PromptUserForPurchase( + PurchaseCallback callback) const { + debug_assert(callback, "callback must have a target function"); + self.RequestPurchaseAsync().Completed( + // The lambda will be called once the RequestPurchaseAsync completes. + [cb = std::move(callback)]( + IAsyncOperation const& async, + AsyncStatus const& status) { + // We just translate the results (and/or errors) + auto res = async.GetResults(); + auto error = res.ExtendedError().value; + + // And run the supplied callback. + cb(translate(res.Status()), error); + }); } std::vector StoreContext::GetProducts( @@ -80,6 +95,23 @@ std::vector to_hstrings(std::span input) { &winrt::to_hstring); return hStrs; } + +PurchaseStatus translate(StorePurchaseStatus purchaseStatus) noexcept { + using winrt::Windows::Services::Store::StorePurchaseStatus; + switch (purchaseStatus) { + case StorePurchaseStatus::Succeeded: + return PurchaseStatus::Succeeded; + case StorePurchaseStatus::AlreadyPurchased: + return PurchaseStatus::AlreadyPurchased; + case StorePurchaseStatus::NotPurchased: + return PurchaseStatus::UserGaveUp; + case StorePurchaseStatus::NetworkError: + return PurchaseStatus::NetworkError; + case StorePurchaseStatus::ServerError: + return PurchaseStatus::ServerError; + } + return StoreApi::PurchaseStatus::Unknown; // To be future proof. +} } // namespace } // namespace StoreApi::impl diff --git a/storeapi/base/impl/StoreContext.hpp b/storeapi/base/impl/StoreContext.hpp index 5ec196786..a1594e8dd 100644 --- a/storeapi/base/impl/StoreContext.hpp +++ b/storeapi/base/impl/StoreContext.hpp @@ -18,6 +18,8 @@ #include #include +#include "../Purchase.hpp" + namespace StoreApi::impl { // Wraps MS StoreContext type for testability purposes. @@ -41,15 +43,17 @@ class StoreContext { std::chrono::system_clock::time_point CurrentExpirationDate() const; protected: - // Assuming this is a Subcription add-on product the current user __does not - // own__, requests the runtime to display a purchase flow so users can - // subscribe to this product. This must be called from a UI thread with the - // underlying store context initialized with the parent GUI window because - // we need to render native dialogs. See + // Assuming this is a Subscription add-on product the current user __does + // not own__, requests the runtime to display a purchase flow so users can + // subscribe to this product. THis function returns early, the result will + // eventually arrive through the supplied callback. This must be called from + // a UI thread with the underlying store context initialized with the parent + // GUI window because we need to render native dialogs. Thus, access to this + // method must be protected so we can ensure it can only happen with GUI + // clients, making API misuse harder to happen. + // See // https://learn.microsoft.com/en-us/uwp/api/windows.services.store.storeproduct.requestpurchaseasync - winrt::Windows::Foundation::IAsyncOperation< - winrt::Windows::Services::Store::StorePurchaseStatus> - PromptUserForPurchase(); + void PromptUserForPurchase(PurchaseCallback callback) const; private: winrt::Windows::Services::Store::StoreProduct self{nullptr}; From 62f5001ebe400ede60bd33f98ba13e19f5a29f57 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 22:21:13 -0300 Subject: [PATCH 16/28] Product as a class instead of struct --- storeapi/base/impl/StoreContext.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storeapi/base/impl/StoreContext.hpp b/storeapi/base/impl/StoreContext.hpp index a1594e8dd..7e32378af 100644 --- a/storeapi/base/impl/StoreContext.hpp +++ b/storeapi/base/impl/StoreContext.hpp @@ -32,7 +32,7 @@ class StoreContext { // Wraps MS StoreProduct type for testability purposes. This is not meant for // direct usage in high level code. The API is loose, the caller services must // tighten it up. - struct Product { + class Product { public: Product(winrt::Windows::Services::Store::StoreProduct self) : self{self} {} // Whether the current user owns this product. From 2f094ded31946bcb1c6cf23c7dac4e96644d99ab Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 16:01:53 -0300 Subject: [PATCH 17/28] StoreContext Const correctness There is no mutation involved with those methods. Thus we can make them const. --- storeapi/base/impl/StoreContext.cpp | 5 +++-- storeapi/base/impl/StoreContext.hpp | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/storeapi/base/impl/StoreContext.cpp b/storeapi/base/impl/StoreContext.cpp index 509a9109b..4b0ad13c6 100644 --- a/storeapi/base/impl/StoreContext.cpp +++ b/storeapi/base/impl/StoreContext.cpp @@ -61,7 +61,8 @@ void StoreContext::Product::PromptUserForPurchase( } std::vector StoreContext::GetProducts( - std::span kinds, std::span ids) { + std::span kinds, + std::span ids) const { debug_assert(!kinds.empty(), "kinds vector cannot be empty"); debug_assert(!ids.empty(), "ids vector cannot be empty"); // Gets Microsoft Store listing info for the specified products that are @@ -71,7 +72,7 @@ std::vector StoreContext::GetProducts( self.GetStoreProductsAsync(to_hstrings(kinds), to_hstrings(ids)).get(); winrt::check_hresult(query.ExtendedError()); - std::vector products; + std::vector products; // Could be empty. for (auto p : query.Products()) { products.emplace_back(p.Value()); diff --git a/storeapi/base/impl/StoreContext.hpp b/storeapi/base/impl/StoreContext.hpp index 7e32378af..18fae21b2 100644 --- a/storeapi/base/impl/StoreContext.hpp +++ b/storeapi/base/impl/StoreContext.hpp @@ -36,7 +36,7 @@ class StoreContext { public: Product(winrt::Windows::Services::Store::StoreProduct self) : self{self} {} // Whether the current user owns this product. - bool IsInUserCollection() { return self.IsInUserCollection(); } + bool IsInUserCollection() const { return self.IsInUserCollection(); } // Assuming this is a Subscription add-on product the current user __owns__, // returns the expiration date of the current billing period. @@ -64,7 +64,7 @@ class StoreContext { // Application; Game; Consumable; UnmanagedConsumable; Durable. See // https://learn.microsoft.com/en-us/uwp/api/windows.services.store.storeproduct.productkind#remarks std::vector GetProducts(std::span kinds, - std::span ids); + std::span ids) const; // Generates the user ID key (a.k.a the JWT) provided the server AAD [hToken] // and the [hUserId] the caller wants to have encoded in the JWT. From 3842d5bd935764bee1a92ba7999352c113dc7064 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 22:25:23 -0300 Subject: [PATCH 18/28] Fix #includes in StoreContext Being pedantic on header inclusion to avoid relying on transitive includes --- storeapi/base/impl/StoreContext.cpp | 5 ++++- storeapi/base/impl/StoreContext.hpp | 5 ++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/storeapi/base/impl/StoreContext.cpp b/storeapi/base/impl/StoreContext.cpp index 4b0ad13c6..90f122831 100644 --- a/storeapi/base/impl/StoreContext.cpp +++ b/storeapi/base/impl/StoreContext.cpp @@ -2,13 +2,16 @@ #include +#include #include +#include +#include +#include #include "../Exception.hpp" namespace StoreApi::impl { -using concurrency::task; using winrt::Windows::Foundation::AsyncStatus; using winrt::Windows::Foundation::IAsyncOperation; using winrt::Windows::Services::Store::StoreProduct; diff --git a/storeapi/base/impl/StoreContext.hpp b/storeapi/base/impl/StoreContext.hpp index 18fae21b2..b979dde37 100644 --- a/storeapi/base/impl/StoreContext.hpp +++ b/storeapi/base/impl/StoreContext.hpp @@ -9,13 +9,12 @@ // To provide the WinRT coroutine types. #include -// To provide coroutines capable of returning more complex non-WinRT types. -#include - // For HWND and GUI-related Windows types. #include +#include #include +#include #include #include "../Purchase.hpp" From 7ae38657453013e911ccdfee951f870e9e7cceb5 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 22:45:19 -0300 Subject: [PATCH 19/28] Clarifies that unit test are about the services classes Context implementations are not tested --- storeapi/test/CMakeLists.txt | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/storeapi/test/CMakeLists.txt b/storeapi/test/CMakeLists.txt index 26a9e181d..6acefe58e 100644 --- a/storeapi/test/CMakeLists.txt +++ b/storeapi/test/CMakeLists.txt @@ -5,22 +5,21 @@ set(CMAKE_SYSTEM_VERSION 10.0) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -project(StoreApiTests LANGUAGES CXX) +project(StoreApiServicesTests LANGUAGES CXX) if(MSVC) add_definitions(-D_UNICODE) endif() set(StoreApi_SRCS - "../base/Context.cpp" - "../base/Context.hpp" "../base/Exception.hpp" + "../base/Purchase.hpp" "../base/StoreService.hpp" "../agent/ServerStoreService.cpp" "../gui/ClientStoreService.hpp" ) -set(StoreApiTests_SRCS +set(StoreApiServicesTests_SRCS "ClientStoreServiceTest.cpp" "ServerStoreServiceTest.cpp" "StoreServiceTest.cpp" @@ -38,11 +37,11 @@ set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) FetchContent_MakeAvailable(googletest) enable_testing() -add_executable(StoreApiTests ${StoreApi_SRCS} ${StoreApiTests_SRCS} ) -target_include_directories(StoreApiTests PUBLIC ${CMAKE_CURRENT_LIST_DIR}/.. ) -target_compile_features(StoreApiTests PRIVATE cxx_std_20) -set_target_properties(StoreApiTests PROPERTIES VS_WINDOWS_TARGET_PLATFORM_MIN_VERSION 10.0.14393.0 COMPILE_WARNING_AS_ERROR ON) -target_link_libraries(StoreApiTests PRIVATE WindowsApp GTest::gtest_main) +add_executable(StoreApiServicesTests ${StoreApi_SRCS} ${StoreApiServicesTests_SRCS} ) +target_include_directories(StoreApiServicesTests PUBLIC ${CMAKE_CURRENT_LIST_DIR}/.. ) +target_compile_features(StoreApiServicesTests PRIVATE cxx_std_20) +set_target_properties(StoreApiServicesTests PROPERTIES VS_WINDOWS_TARGET_PLATFORM_MIN_VERSION 10.0.14393.0 COMPILE_WARNING_AS_ERROR ON) +target_link_libraries(StoreApiServicesTests PRIVATE WindowsApp GTest::gtest_main) if(MSVC) set(MSVC_OPTIONS /W4 # Baseline reasonable warnings @@ -73,8 +72,8 @@ if(MSVC) /INCREMENTAL:NO /await ) - target_compile_options(StoreApiTests INTERFACE ${MSVC_OPTIONS}) + target_compile_options(StoreApiServicesTests INTERFACE ${MSVC_OPTIONS}) endif() include(GoogleTest) -gtest_discover_tests(StoreApiTests) +gtest_discover_tests(StoreApiServicesTests) From 2541bff37d36fae64ce29c2827b1b47e225c8800 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 22:58:30 -0300 Subject: [PATCH 20/28] Updates ClientStoreService tests and related stubs --- storeapi/test/ClientStoreServiceTest.cpp | 22 +- storeapi/test/stubs.hpp | 298 +++++++++++++---------- 2 files changed, 179 insertions(+), 141 deletions(-) diff --git a/storeapi/test/ClientStoreServiceTest.cpp b/storeapi/test/ClientStoreServiceTest.cpp index 692eab29a..996208853 100644 --- a/storeapi/test/ClientStoreServiceTest.cpp +++ b/storeapi/test/ClientStoreServiceTest.cpp @@ -1,5 +1,6 @@ #include +#include #include #include "stubs.hpp" @@ -8,17 +9,26 @@ namespace StoreApi { static constexpr char productId[] = "my-awesome-addon"; +TEST(ClientStoreService, ProductNotFound) { + auto service = ClientStoreService{0}; + EXPECT_THROW({ auto prod = service.FetchAvailableProduct(productId); }, + Exception); +} + TEST(ClientStoreService, CannotRePurchase) { - auto service = ClientStoreService{nullptr}; - auto res = service.PromptUserToSubscribe(productId).get(); - EXPECT_EQ(res, PurchaseStatus::AlreadyPurchased); + auto service = ClientStoreService{0}; + EXPECT_THROW({ auto p = service.FetchAvailableProduct(productId); }, + Exception); } TEST(ClientStoreService, Success) { // or whatever else the underlying purchase operation may return. - auto service = ClientStoreService{nullptr}; - auto res = service.PromptUserToSubscribe(productId).get(); - EXPECT_EQ(res, PurchaseStatus::Succeeded); + auto service = ClientStoreService{0}; + auto p = service.FetchAvailableProduct(productId); + p.PromptUserForPurchase([](PurchaseStatus res, std::int32_t hr) { + EXPECT_EQ(res, PurchaseStatus::Succeeded); + EXPECT_EQ(hr, 0); + }); } } // namespace StoreApi diff --git a/storeapi/test/stubs.hpp b/storeapi/test/stubs.hpp index 7061c76cb..706ce7abd 100644 --- a/storeapi/test/stubs.hpp +++ b/storeapi/test/stubs.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include // For WinRT basic types and coroutines. #include // For non-WinRT coroutines @@ -7,6 +8,11 @@ // Win32 APIs, such as the Timezone #include + +#include +#include +#include + // Test stubs and doubles. // A Store Context that always finds more than one product @@ -21,182 +27,204 @@ struct DoubledContext { // A Store Context that never finds a product. struct EmptyContext { - struct Product {}; + using Window = char; + struct Product { + std::string kind; + std::string id; - concurrency::task> GetProducts( - std::vector kinds, std::vector ids) { - co_return {}; - } -}; + std::chrono::system_clock::time_point CurrentExpirationDate() const { + throw std::logic_error{"This should not be called"}; + } -// A Store Context that always finds exactly one product. -struct FirstContext { - struct Product { - winrt::hstring kind; - winrt::hstring id; + bool IsInUserCollection() const { + throw std::logic_error{"This should not be called"}; + } + + void PromptUserForPurchase(StoreApi::PurchaseCallback) { + throw std::logic_error{"This should not be called"}; + } }; - concurrency::task> GetProducts( - std::vector kinds, std::vector ids) { - co_return {Product{.kind = kinds[0], .id = ids[0]}}; + std::vector GetProducts(std::span kinds, + std::span ids) const { + return {}; } -}; -// A Store Context that always finds exactly one product. -struct EmptyJwtContext { - struct Product { - winrt::hstring kind; - winrt::hstring id; - }; + //winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( + // winrt::hstring hToken, winrt::hstring hUserId) { + // co_return hToken; + //} - concurrency::task> GetProducts( - std::vector kinds, std::vector ids) { - co_return {Product{.kind = kinds[0], .id = ids[0]}}; - } + // noop + void InitDialogs(Window window) {} + }; - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return {}; - } -}; + // A Store Context that always finds exactly one product. + struct FirstContext { + struct Product { + winrt::hstring kind; + winrt::hstring id; + }; -// A Store Context that always finds exactly one product. -struct IdentityJwtContext { - struct Product { - winrt::hstring kind; - winrt::hstring id; + concurrency::task> GetProducts( + std::vector kinds, std::vector ids) { + co_return {Product{.kind = kinds[0], .id = ids[0]}}; + } }; - concurrency::task> GetProducts( - std::vector kinds, std::vector ids) { - co_return {Product{.kind = kinds[0], .id = ids[0]}}; - } + // A Store Context that always finds exactly one product. + struct EmptyJwtContext { + struct Product { + winrt::hstring kind; + winrt::hstring id; + }; - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return hToken; - } -}; + concurrency::task> GetProducts( + std::vector kinds, std::vector ids) { + co_return {Product{.kind = kinds[0], .id = ids[0]}}; + } -// A Store Context that only finds exactly a product user doesn't own. -struct NeverSubscribedContext { - struct Product { - winrt::hstring kind; - winrt::hstring id; + winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( + winrt::hstring hToken, winrt::hstring hUserId) { + co_return {}; + } + }; + + // A Store Context that always finds exactly one product. + struct IdentityJwtContext { + struct Product { + winrt::hstring kind; + winrt::hstring id; + }; - bool IsInUserCollection() { return false; } + concurrency::task> GetProducts( + std::vector kinds, std::vector ids) { + co_return {Product{.kind = kinds[0], .id = ids[0]}}; + } - winrt::Windows::Foundation::DateTime CurrentExpirationDate() { - throw StoreApi::Exception{StoreApi::ErrorCode::Unsubscribed, - std::format("id: {}", winrt::to_string(id))}; + winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( + winrt::hstring hToken, winrt::hstring hUserId) { + co_return hToken; } }; - concurrency::task> GetProducts( - std::vector kinds, std::vector ids) { - co_return {Product{.kind = kinds[0], .id = ids[0]}}; - } + // A Store Context that only finds exactly a product user doesn't own. + struct NeverSubscribedContext { + struct Product { + winrt::hstring kind; + winrt::hstring id; - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return hToken; - } -}; + bool IsInUserCollection() { return false; } -// A Store Context that always finds a subscription that expired in the Unix -// epoch (in the local time zone). -struct UnixEpochContext { - struct Product { - winrt::hstring kind; - winrt::hstring id; - - winrt::Windows::Foundation::DateTime CurrentExpirationDate() { - TIME_ZONE_INFORMATION tz{}; - std::int64_t seconds = 0; - if (GetTimeZoneInformation(&tz) != TIME_ZONE_ID_INVALID) { - // UTC = local time + Bias (in minutes) - seconds = static_cast(tz.Bias) * 60LL; + winrt::Windows::Foundation::DateTime CurrentExpirationDate() { + throw StoreApi::Exception{StoreApi::ErrorCode::Unsubscribed, + std::format("id: {}", winrt::to_string(id))}; } + }; - return winrt::clock::from_time_t(seconds); // should be the UNIX epoch. + concurrency::task> GetProducts( + std::vector kinds, std::vector ids) { + co_return {Product{.kind = kinds[0], .id = ids[0]}}; } - bool IsInUserCollection() { return true; } + winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( + winrt::hstring hToken, winrt::hstring hUserId) { + co_return hToken; + } }; - concurrency::task> GetProducts( - std::vector kinds, std::vector ids) { - co_return {Product{.kind = kinds[0], .id = ids[0]}}; - } - - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return hToken; - } -}; + // A Store Context that always finds a subscription that expired in the Unix + // epoch (in the local time zone). + struct UnixEpochContext { + struct Product { + winrt::hstring kind; + winrt::hstring id; + + winrt::Windows::Foundation::DateTime CurrentExpirationDate() { + TIME_ZONE_INFORMATION tz{}; + std::int64_t seconds = 0; + if (GetTimeZoneInformation(&tz) != TIME_ZONE_ID_INVALID) { + // UTC = local time + Bias (in minutes) + seconds = static_cast(tz.Bias) * 60LL; + } + + return winrt::clock::from_time_t(seconds); // should be the UNIX epoch. + } -// A Store Context that always finds a valid subscription. -struct AlreadyPurchasedContext { - struct Product { - winrt::hstring kind; - winrt::hstring id; + bool IsInUserCollection() { return true; } + }; - winrt::Windows::Foundation::DateTime CurrentExpirationDate() { - return winrt::clock::now() + std::chrono::days{9}; + concurrency::task> GetProducts( + std::vector kinds, std::vector ids) { + co_return {Product{.kind = kinds[0], .id = ids[0]}}; } - bool IsInUserCollection() { return true; } - - winrt::Windows::Foundation::IAsyncOperation< - winrt::Windows::Services::Store::StorePurchaseStatus> - PromptUserForPurchase() { - throw std::logic_error{"This should not be called"}; + winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( + winrt::hstring hToken, winrt::hstring hUserId) { + co_return hToken; } }; - concurrency::task> GetProducts( - std::vector kinds, std::vector ids) { - co_return {Product{.kind = kinds[0], .id = ids[0]}}; - } + // A Store Context that always finds a valid subscription. + struct AlreadyPurchasedContext { + using Window = char; + struct Product { + std::string kind; + std::string id; - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return hToken; - } + std::chrono::system_clock::time_point CurrentExpirationDate() const { + return std::chrono::system_clock::now() + std::chrono::days{9}; + } - void InitDialogs(HWND window) { /*nopp*/ - } -}; + bool IsInUserCollection() const { return true; } -// A Store Context that always finds a valid subscription. -struct PurchaseSuccessContext { - struct Product { - winrt::hstring kind; - winrt::hstring id; + void PromptUserForPurchase(StoreApi::PurchaseCallback) { + throw std::logic_error{"This should not be called"}; + } + }; - winrt::Windows::Foundation::DateTime CurrentExpirationDate() { - throw std::logic_error{"This should not be called"}; + std::vector GetProducts(std::span kinds, + std::span ids) const { + return {Product{.kind = kinds[0], .id = ids[0]}}; } - bool IsInUserCollection() { return false; } - - winrt::Windows::Foundation::IAsyncOperation< - winrt::Windows::Services::Store::StorePurchaseStatus> - PromptUserForPurchase() { - co_return winrt::Windows::Services::Store::StorePurchaseStatus::Succeeded; + winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( + winrt::hstring hToken, winrt::hstring hUserId) { + co_return hToken; } + + // noop + void InitDialogs(Window window) {} }; - concurrency::task> GetProducts( - std::vector kinds, std::vector ids) { - co_return {Product{.kind = kinds[0], .id = ids[0]}}; - } + // A Store Context that always finds a valid subscription. + struct PurchaseSuccessContext { + using Window = char; + struct Product { + std::string kind; + std::string id; - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return hToken; - } + std::chrono::system_clock::time_point CurrentExpirationDate() { + throw std::logic_error{"This should not be called"}; + } - void InitDialogs(HWND window) { /*nopp*/ - } -}; + bool IsInUserCollection() const { return false; } + + void PromptUserForPurchase(StoreApi::PurchaseCallback cb) const { + cb(StoreApi::PurchaseStatus::Succeeded, 0); + } + }; + + std::vector GetProducts(std::span kinds, + std::span ids) const { + return {Product{.kind = kinds[0], .id = ids[0]}}; + } + + winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( + winrt::hstring hToken, winrt::hstring hUserId) { + co_return hToken; + } + + // noop + void InitDialogs(Window window) {} + }; From b134d30be4159b4f8af2e37e3d5a80372376f11a Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 23:20:57 -0300 Subject: [PATCH 21/28] Temporary updates to ServerStoreService Just to enable compiling the existing tests. There is more to come. --- storeapi/agent/ServerStoreService.hpp | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/storeapi/agent/ServerStoreService.hpp b/storeapi/agent/ServerStoreService.hpp index 5e0212b76..050f01894 100644 --- a/storeapi/agent/ServerStoreService.hpp +++ b/storeapi/agent/ServerStoreService.hpp @@ -1,10 +1,14 @@ #pragma once -#include +#include -#include "base/Context.hpp" +#include "base/DefaultContext.hpp" #include "base/Exception.hpp" #include "base/StoreService.hpp" +#include +#include +#include + namespace StoreApi { // Models the interesting user information the application can correlate @@ -19,7 +23,7 @@ struct UserInfo { // Adds functionality on top of the [StoreService] interesting to background // server applications. -template +template class ServerStoreService : public StoreService { public: // Generates the user ID key (a.k.a the JWT) provided the server AAD [token] @@ -45,17 +49,16 @@ class ServerStoreService : public StoreService { // product or the lowest int64_t otherwise (a date too far in the past). This // raw return value suits well for crossing ABI boundaries, such as returning // to a caller in Go. - concurrency::task CurrentExpirationDate(std::string productId) { - auto product = co_await this->GetSubscriptionProduct(productId); + std::int64_t CurrentExpirationDate(std::string productId) { + auto product = this->GetSubscriptionProduct(productId); if (!product.IsInUserCollection()) { - co_return std::numeric_limits::lowest(); + return std::numeric_limits::lowest(); } // C++20 guarantees that std::chrono::system_clock measures UNIX time. - const auto t = winrt::clock::to_sys(product.CurrentExpirationDate()); - const auto dur = t.time_since_epoch(); + const auto dur = product.CurrentExpirationDate().time_since_epoch(); // just need to convert the duration to seconds. - co_return duration_cast(dur).count(); + return duration_cast(dur).count(); } }; From 75e06c37522ca92ce65445e79f1db68726fd8218 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 23:23:43 -0300 Subject: [PATCH 22/28] Updates ServerStoreService tests and more consequential updates to the test stubs. Just enough to keep the tests compiling and behaving as before. --- storeapi/test/ServerStoreServiceTest.cpp | 6 +- storeapi/test/stubs.hpp | 285 +++++++++++------------ 2 files changed, 145 insertions(+), 146 deletions(-) diff --git a/storeapi/test/ServerStoreServiceTest.cpp b/storeapi/test/ServerStoreServiceTest.cpp index a2103e7ee..a2e154405 100644 --- a/storeapi/test/ServerStoreServiceTest.cpp +++ b/storeapi/test/ServerStoreServiceTest.cpp @@ -46,7 +46,7 @@ TEST(ServerStoreService, RealServerFailsUnderTest) { TEST(ServerStoreService, ExpirationDateUnsubscribed) { auto service = ServerStoreService{}; - auto expiration = service.CurrentExpirationDate("my-awesome-addon").get(); + auto expiration = service.CurrentExpirationDate("my-awesome-addon"); EXPECT_EQ(std::numeric_limits::lowest(), expiration); } @@ -65,9 +65,9 @@ TEST(ServerStoreService, ExpirationDateEpoch) { .tm_yday = 0, .tm_isdst = -1, // Use DST value from local time zone }; - auto unix_epoch = static_cast(std::mktime(&tm)); + auto unix_epoch = static_cast(timegm(&tm)); - auto expiration = service.CurrentExpirationDate("my-awesome-addon").get(); + auto expiration = service.CurrentExpirationDate("my-awesome-addon"); EXPECT_EQ(unix_epoch, expiration); } diff --git a/storeapi/test/stubs.hpp b/storeapi/test/stubs.hpp index 706ce7abd..2a101fda4 100644 --- a/storeapi/test/stubs.hpp +++ b/storeapi/test/stubs.hpp @@ -1,4 +1,7 @@ #pragma once + +/// Test stubs and doubles. + #include #include // For WinRT basic types and coroutines. @@ -9,19 +12,26 @@ // Win32 APIs, such as the Timezone #include +#include #include #include +#include +#include #include -// Test stubs and doubles. +#if defined _MSC_VER +#include +#include +#define timegm _mkgmtime +#endif // A Store Context that always finds more than one product struct DoubledContext { struct Product {}; - concurrency::task> GetProducts( - std::vector kinds, std::vector ids) { - co_return {Product{}, Product{}, Product{}}; + std::vector GetProducts(std::span kinds, + std::span ids) const { + return {Product{}, Product{}, Product{}}; } }; @@ -50,181 +60,170 @@ struct EmptyContext { return {}; } - //winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - // winrt::hstring hToken, winrt::hstring hUserId) { - // co_return hToken; - //} - // noop void InitDialogs(Window window) {} - }; - - // A Store Context that always finds exactly one product. - struct FirstContext { - struct Product { - winrt::hstring kind; - winrt::hstring id; - }; +}; - concurrency::task> GetProducts( - std::vector kinds, std::vector ids) { - co_return {Product{.kind = kinds[0], .id = ids[0]}}; - } +// A Store Context that always finds exactly one product. +struct FirstContext { + struct Product { + std::string kind; + std::string id; }; - // A Store Context that always finds exactly one product. - struct EmptyJwtContext { - struct Product { - winrt::hstring kind; - winrt::hstring id; - }; - - concurrency::task> GetProducts( - std::vector kinds, std::vector ids) { - co_return {Product{.kind = kinds[0], .id = ids[0]}}; - } + std::vector GetProducts(std::span kinds, + std::span ids) const { + return {Product{.kind = kinds[0], .id = ids[0]}}; + } +}; - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return {}; - } +// A Store Context that always finds exactly one product. +struct EmptyJwtContext { + struct Product { + std::string kind; + std::string id; }; - // A Store Context that always finds exactly one product. - struct IdentityJwtContext { - struct Product { - winrt::hstring kind; - winrt::hstring id; - }; + std::vector GetProducts(std::span kinds, + std::span ids) const { + return {Product{.kind = kinds[0], .id = ids[0]}}; + } - concurrency::task> GetProducts( - std::vector kinds, std::vector ids) { - co_return {Product{.kind = kinds[0], .id = ids[0]}}; - } + winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( + winrt::hstring hToken, winrt::hstring hUserId) { + co_return {}; + } +}; - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return hToken; - } +// A Store Context that always finds exactly one product. +struct IdentityJwtContext { + struct Product { + std::string kind; + std::string id; }; - // A Store Context that only finds exactly a product user doesn't own. - struct NeverSubscribedContext { - struct Product { - winrt::hstring kind; - winrt::hstring id; + std::vector GetProducts(std::span kinds, + std::span ids) const { + return {Product{.kind = kinds[0], .id = ids[0]}}; + } - bool IsInUserCollection() { return false; } + winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( + winrt::hstring hToken, winrt::hstring hUserId) { + co_return hToken; + } +}; - winrt::Windows::Foundation::DateTime CurrentExpirationDate() { - throw StoreApi::Exception{StoreApi::ErrorCode::Unsubscribed, - std::format("id: {}", winrt::to_string(id))}; - } - }; +// A Store Context that only finds exactly a product user doesn't own. +struct NeverSubscribedContext { + struct Product { + std::string kind; + std::string id; - concurrency::task> GetProducts( - std::vector kinds, std::vector ids) { - co_return {Product{.kind = kinds[0], .id = ids[0]}}; - } + bool IsInUserCollection() { return false; } - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return hToken; + std::chrono::system_clock::time_point CurrentExpirationDate() { + throw StoreApi::Exception{StoreApi::ErrorCode::Unsubscribed, + std::format("id: {}", id)}; } }; - // A Store Context that always finds a subscription that expired in the Unix - // epoch (in the local time zone). - struct UnixEpochContext { - struct Product { - winrt::hstring kind; - winrt::hstring id; - - winrt::Windows::Foundation::DateTime CurrentExpirationDate() { - TIME_ZONE_INFORMATION tz{}; - std::int64_t seconds = 0; - if (GetTimeZoneInformation(&tz) != TIME_ZONE_ID_INVALID) { - // UTC = local time + Bias (in minutes) - seconds = static_cast(tz.Bias) * 60LL; - } - - return winrt::clock::from_time_t(seconds); // should be the UNIX epoch. - } - - bool IsInUserCollection() { return true; } - }; - - concurrency::task> GetProducts( - std::vector kinds, std::vector ids) { - co_return {Product{.kind = kinds[0], .id = ids[0]}}; - } + std::vector GetProducts(std::span kinds, + std::span ids) const { + return {Product{.kind = kinds[0], .id = ids[0]}}; + } - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return hToken; + winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( + winrt::hstring hToken, winrt::hstring hUserId) { + co_return hToken; + } +}; + +// A Store Context that always finds a subscription that expired in the Unix +// epoch (in the local time zone). +struct UnixEpochContext { + struct Product { + std::string kind; + std::string id; + + std::chrono::system_clock::time_point CurrentExpirationDate() { + using namespace std::chrono; + return sys_days{January / 1 / 1970}; } - }; - // A Store Context that always finds a valid subscription. - struct AlreadyPurchasedContext { - using Window = char; - struct Product { - std::string kind; - std::string id; + bool IsInUserCollection() { return true; } + }; - std::chrono::system_clock::time_point CurrentExpirationDate() const { - return std::chrono::system_clock::now() + std::chrono::days{9}; - } + std::vector GetProducts(std::span kinds, + std::span ids) const { + return {Product{.kind = kinds[0], .id = ids[0]}}; + } - bool IsInUserCollection() const { return true; } + winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( + winrt::hstring hToken, winrt::hstring hUserId) { + co_return hToken; + } +}; - void PromptUserForPurchase(StoreApi::PurchaseCallback) { - throw std::logic_error{"This should not be called"}; - } - }; +// A Store Context that always finds a valid subscription. +struct AlreadyPurchasedContext { + using Window = char; + struct Product { + std::string kind; + std::string id; - std::vector GetProducts(std::span kinds, - std::span ids) const { - return {Product{.kind = kinds[0], .id = ids[0]}}; + std::chrono::system_clock::time_point CurrentExpirationDate() const { + return std::chrono::system_clock::now() + std::chrono::days{9}; } - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return hToken; - } + bool IsInUserCollection() const { return true; } - // noop - void InitDialogs(Window window) {} + void PromptUserForPurchase(StoreApi::PurchaseCallback) { + throw std::logic_error{"This should not be called"}; + } }; - // A Store Context that always finds a valid subscription. - struct PurchaseSuccessContext { - using Window = char; - struct Product { - std::string kind; - std::string id; + std::vector GetProducts(std::span kinds, + std::span ids) const { + return {Product{.kind = kinds[0], .id = ids[0]}}; + } - std::chrono::system_clock::time_point CurrentExpirationDate() { - throw std::logic_error{"This should not be called"}; - } + winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( + winrt::hstring hToken, winrt::hstring hUserId) { + co_return hToken; + } - bool IsInUserCollection() const { return false; } + // noop + void InitDialogs(Window window) {} +}; - void PromptUserForPurchase(StoreApi::PurchaseCallback cb) const { - cb(StoreApi::PurchaseStatus::Succeeded, 0); - } - }; +// A Store Context that always finds a valid subscription. +struct PurchaseSuccessContext { + using Window = char; + struct Product { + std::string kind; + std::string id; - std::vector GetProducts(std::span kinds, - std::span ids) const { - return {Product{.kind = kinds[0], .id = ids[0]}}; + std::chrono::system_clock::time_point CurrentExpirationDate() { + throw std::logic_error{"This should not be called"}; } - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return hToken; - } + bool IsInUserCollection() const { return false; } - // noop - void InitDialogs(Window window) {} + void PromptUserForPurchase(StoreApi::PurchaseCallback cb) const { + cb(StoreApi::PurchaseStatus::Succeeded, 0); + } }; + + std::vector GetProducts(std::span kinds, + std::span ids) const { + return {Product{.kind = kinds[0], .id = ids[0]}}; + } + + winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( + winrt::hstring hToken, winrt::hstring hUserId) { + co_return hToken; + } + + // noop + void InitDialogs(Window window) {} +}; From e3525fa6a73b27407bed1bc4364a68d3aa7500aa Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 14 Sep 2023 23:26:25 -0300 Subject: [PATCH 23/28] Updates to the base StoreService tests As before, just to preserve the previous behavior. --- storeapi/test/StoreServiceTest.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/storeapi/test/StoreServiceTest.cpp b/storeapi/test/StoreServiceTest.cpp index 76090b12e..2445f26a4 100644 --- a/storeapi/test/StoreServiceTest.cpp +++ b/storeapi/test/StoreServiceTest.cpp @@ -1,5 +1,6 @@ #include +#include #include #include "stubs.hpp" @@ -15,7 +16,7 @@ class DoubledService : public StoreService { TEST(StoreService, DoubledProductsThrow) { DoubledService service{}; - EXPECT_THROW({ service.GetSubscriptionProduct("never-mind").get(); }, + EXPECT_THROW({ service.GetSubscriptionProduct("never-mind"); }, Exception); } @@ -25,7 +26,7 @@ class EmptyService : public StoreService { }; TEST(StoreService, EmptyProductsThrow) { DoubledService service{}; - EXPECT_THROW({ service.GetSubscriptionProduct("never-mind").get(); }, + EXPECT_THROW({ service.GetSubscriptionProduct("never-mind"); }, Exception); } @@ -35,9 +36,9 @@ class IdentityService : public StoreService { }; TEST(IdentityService, OneProductNoThrow) { IdentityService service{}; - auto product = service.GetSubscriptionProduct("never-mind").get(); - EXPECT_EQ(product.kind, L"Durable"); - EXPECT_EQ(product.id, L"never-mind"); + auto product = service.GetSubscriptionProduct("never-mind"); + EXPECT_EQ(product.kind, "Durable"); + EXPECT_EQ(product.id, "never-mind"); } } // namespace StoreApi From 563c66857c2b60bfcfafb473d1fc02dc802cb7df Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Fri, 15 Sep 2023 00:20:33 -0300 Subject: [PATCH 24/28] Updates the DLL project CurrentExpirationDate no longer a coroutine Conditionally add StoreContext component impl based on the env var. --- msix/storeapi/StoreApi.cpp | 2 +- msix/storeapi/storeapi.vcxproj | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/msix/storeapi/StoreApi.cpp b/msix/storeapi/StoreApi.cpp index 71be65389..4cb550090 100644 --- a/msix/storeapi/StoreApi.cpp +++ b/msix/storeapi/StoreApi.cpp @@ -48,7 +48,7 @@ Int GetSubscriptionExpirationDate(const char* productID, try { StoreApi::ServerStoreService service{}; - *expirationUnix = service.CurrentExpirationDate(productID).get(); + *expirationUnix = service.CurrentExpirationDate(productID); return 0; } catch (const StoreApi::Exception& err) { diff --git a/msix/storeapi/storeapi.vcxproj b/msix/storeapi/storeapi.vcxproj index 448544e8d..0f03b2287 100644 --- a/msix/storeapi/storeapi.vcxproj +++ b/msix/storeapi/storeapi.vcxproj @@ -87,7 +87,6 @@ - @@ -95,10 +94,13 @@ - + + + + From bdbc6d6edf2a1dfa49936e4b35497550661b5d97 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Fri, 15 Sep 2023 12:05:54 -0300 Subject: [PATCH 25/28] Propagates cpp macro UP4W_TEST_WITH_MS_STORE_MOCK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit via the build systems. Instead of selecting which sources header to add to the project, add them all and let them handle that cpp macro. I.e. the production context only presents its contents if MSVC is defined and no UP4W_TEST_WITH_MS_STORE_MOCK is not, rendering itself empty otherwise. The mocked implementations should do the same strategy to render themselves empty if the #defines don't match their requirements. Note that the DefaultContext alias is at the bottom of the impl header. Co-authored-by: Edu Gómez Escandell --- .../p4w_ms_store/windows/CMakeLists.txt | 18 +++++++++--------- msix/storeapi/storeapi.vcxproj | 9 +++++---- storeapi/base/DefaultContext.hpp | 13 ------------- storeapi/base/impl/StoreContext.cpp | 4 ++++ storeapi/base/impl/StoreContext.hpp | 11 +++++++++++ 5 files changed, 29 insertions(+), 26 deletions(-) diff --git a/gui/packages/p4w_ms_store/windows/CMakeLists.txt b/gui/packages/p4w_ms_store/windows/CMakeLists.txt index a4c62df64..5335858dd 100644 --- a/gui/packages/p4w_ms_store/windows/CMakeLists.txt +++ b/gui/packages/p4w_ms_store/windows/CMakeLists.txt @@ -68,17 +68,17 @@ set(STORE_API_SRC "${STORE_API_DIR}/base/Purchase.hpp" "${STORE_API_DIR}/base/Exception.hpp" "${STORE_API_DIR}/base/DefaultContext.hpp" + + # API wrapper implementations + "${STORE_API_DIR}/base/impl/StoreContext.cpp" + "${STORE_API_DIR}/base/impl/StoreContext.hpp" ) -if(DEFINED ENV{TEST_WITH_MS_STORE_MOCK}) - # TODO: Change to warning to be informative and include the appropriate files. - message(FATAL_ERROR "Unsupported build with the MS Store Mock client API due environment variable 'TEST_WITH_MS_STORE_MOCK' set to '$ENV{TEST_WITH_MS_STORE_MOCK}'.") - list(APPEND STORE_API_DEFINES "TEST_WITH_MS_STORE_MOCK") +if(DEFINED ENV{UP4W_TEST_WITH_MS_STORE_MOCK}) + # TODO: Change to informative warning and update the text once mock client API wrappers are avaiable. + message(FATAL_ERROR "Unsupported build with the MS Store Mock client API due environment variable 'UP4W_TEST_WITH_MS_STORE_MOCK' set to '$ENV{UP4W_TEST_WITH_MS_STORE_MOCK}'.") + list(APPEND STORE_API_DEFINES "UP4W_TEST_WITH_MS_STORE_MOCK") else() - message(STATUS "Building with the production version of MS Store client API. Set the environment variable 'TEST_WITH_MS_STORE_MOCK' if you want to build with the mock store API.") - list(APPEND STORE_API_SRC - "${STORE_API_DIR}/base/impl/StoreContext.cpp" - "${STORE_API_DIR}/base/impl/StoreContext.hpp" - ) + message(STATUS "Building with the production version of MS Store client API. Set the environment variable 'UP4W_TEST_WITH_MS_STORE_MOCK' if you want to build with the mock store API.") endif() # Any new source files that you add to the plugin should be added here. diff --git a/msix/storeapi/storeapi.vcxproj b/msix/storeapi/storeapi.vcxproj index 0f03b2287..5e5e17d9e 100644 --- a/msix/storeapi/storeapi.vcxproj +++ b/msix/storeapi/storeapi.vcxproj @@ -43,6 +43,9 @@ + + UP4W_TEST_WITH_MS_STORE_MOCK + Level3 @@ -89,18 +92,16 @@ + + - - - - diff --git a/storeapi/base/DefaultContext.hpp b/storeapi/base/DefaultContext.hpp index d83c10e40..e87991c46 100644 --- a/storeapi/base/DefaultContext.hpp +++ b/storeapi/base/DefaultContext.hpp @@ -1,16 +1,3 @@ #pragma once -#ifdef TEST_WITH_MS_STORE_MOCK -// TODO: Handle the mock case -#ifdef _MSC_VER -// windows specific mocked impl (may use winrt) -#else // _MSC_VER -// non-windows specific mocked impl (Linux friendly) -#endif // _MSC_VER - -#else // TEST_WITH_MS_STORE_MOCK #include "impl/StoreContext.hpp" -namespace StoreApi { -using DefaultContext = impl::StoreContext; -} -#endif // TEST_WITH_MS_STORE_MOCK diff --git a/storeapi/base/impl/StoreContext.cpp b/storeapi/base/impl/StoreContext.cpp index 90f122831..bc06440a8 100644 --- a/storeapi/base/impl/StoreContext.cpp +++ b/storeapi/base/impl/StoreContext.cpp @@ -1,3 +1,5 @@ +#ifndef UP4W_TEST_WITH_MS_STORE_MOCK + #include "StoreContext.hpp" #include @@ -119,3 +121,5 @@ PurchaseStatus translate(StorePurchaseStatus purchaseStatus) noexcept { } // namespace } // namespace StoreApi::impl + +#endif // UP4W_TEST_WITH_MS_STORE_MOCK \ No newline at end of file diff --git a/storeapi/base/impl/StoreContext.hpp b/storeapi/base/impl/StoreContext.hpp index b979dde37..b16b824bf 100644 --- a/storeapi/base/impl/StoreContext.hpp +++ b/storeapi/base/impl/StoreContext.hpp @@ -2,6 +2,11 @@ /// Here lies the classes wrapping the MS API for testability. /// Thus this code is inherently non-testable. +#ifndef UP4W_TEST_WITH_MS_STORE_MOCK + +#ifndef _MSC_VER +#error This is Windows specific Store API Context implementation and cannot compile on other platforms. +#endif // _MSC_VER // For the underlying Store API #include @@ -79,3 +84,9 @@ class StoreContext { }; } // namespace StoreApi::impl + +namespace StoreApi { +using DefaultContext = impl::StoreContext; +} + +#endif // UP4W_TEST_WITH_MS_STORE_MOCK From dbf440d2cc5b45d7b9f4b2c639a689ad001eab87 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Mon, 18 Sep 2023 10:22:21 -0300 Subject: [PATCH 26/28] This test should be with EmptyService --- storeapi/test/StoreServiceTest.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/storeapi/test/StoreServiceTest.cpp b/storeapi/test/StoreServiceTest.cpp index 2445f26a4..18da0bcf9 100644 --- a/storeapi/test/StoreServiceTest.cpp +++ b/storeapi/test/StoreServiceTest.cpp @@ -24,8 +24,9 @@ class EmptyService : public StoreService { public: using StoreService::GetSubscriptionProduct; }; + TEST(StoreService, EmptyProductsThrow) { - DoubledService service{}; + EmptyService service{}; EXPECT_THROW({ service.GetSubscriptionProduct("never-mind"); }, Exception); } From 282220301bfd5e4ba1091cdb8b9e36d43ec7f02d Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Mon, 18 Sep 2023 10:27:52 -0300 Subject: [PATCH 27/28] Assert on the StorePurchaseStatus enum translation Tests in the future would catch if more status are added to that enum, forcing us to review and update. --- storeapi/base/impl/StoreContext.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/storeapi/base/impl/StoreContext.cpp b/storeapi/base/impl/StoreContext.cpp index bc06440a8..b2d5fec09 100644 --- a/storeapi/base/impl/StoreContext.cpp +++ b/storeapi/base/impl/StoreContext.cpp @@ -116,10 +116,11 @@ PurchaseStatus translate(StorePurchaseStatus purchaseStatus) noexcept { case StorePurchaseStatus::ServerError: return PurchaseStatus::ServerError; } + debug_assert(false, "Missing enum elements to translate StorePurchaseStatus."); return StoreApi::PurchaseStatus::Unknown; // To be future proof. } } // namespace } // namespace StoreApi::impl -#endif // UP4W_TEST_WITH_MS_STORE_MOCK \ No newline at end of file +#endif // UP4W_TEST_WITH_MS_STORE_MOCK From f5e972adae785fcd28426fbb1cc4cd056b719846 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Mon, 18 Sep 2023 10:36:58 -0300 Subject: [PATCH 28/28] Removes the custom assertion for consistency It's likely that we will desire to replace CRT Report Handler in the DLL initialization code, for instance. This would not be affected with the custom debug_assert macro. --- storeapi/base/Exception.hpp | 17 ----------------- storeapi/base/impl/StoreContext.cpp | 8 ++++---- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/storeapi/base/Exception.hpp b/storeapi/base/Exception.hpp index ab98673cd..c25c1e00b 100644 --- a/storeapi/base/Exception.hpp +++ b/storeapi/base/Exception.hpp @@ -1,7 +1,6 @@ #pragma once #include #include -#include #include #include #include @@ -78,20 +77,4 @@ class Exception { } }; -#ifdef NDEBUG -#define debug_assert(expr, msg) ((void)0) -#else // NDEBUG -inline void AssertFail( - std::string_view condition, std::string_view msg, - std::source_location loc = std::source_location::current()) { - std::cerr << std::format( - "[ASSERTION FAILURE]: {}:{} {}\n\tunmet condition: {} ({})\n", - loc.file_name(), loc.line(), loc.function_name(), condition, msg); - - std::terminate(); -} -#define debug_assert(expr, msg) \ - (static_cast(expr) ? ((void)0) : AssertFail("'" #expr "'", msg)) -#endif // NDEBUG - } // namespace StoreApi diff --git a/storeapi/base/impl/StoreContext.cpp b/storeapi/base/impl/StoreContext.cpp index b2d5fec09..c942d76f4 100644 --- a/storeapi/base/impl/StoreContext.cpp +++ b/storeapi/base/impl/StoreContext.cpp @@ -50,7 +50,7 @@ StoreContext::Product::CurrentExpirationDate() const { void StoreContext::Product::PromptUserForPurchase( PurchaseCallback callback) const { - debug_assert(callback, "callback must have a target function"); + assert(callback && "callback must have a target function"); self.RequestPurchaseAsync().Completed( // The lambda will be called once the RequestPurchaseAsync completes. [cb = std::move(callback)]( @@ -68,8 +68,8 @@ void StoreContext::Product::PromptUserForPurchase( std::vector StoreContext::GetProducts( std::span kinds, std::span ids) const { - debug_assert(!kinds.empty(), "kinds vector cannot be empty"); - debug_assert(!ids.empty(), "ids vector cannot be empty"); + assert(!kinds.empty() && "kinds vector cannot be empty"); + assert(!ids.empty() && "ids vector cannot be empty"); // Gets Microsoft Store listing info for the specified products that are // associated with the current app. Requires "arrays" of product kinds and // ids. @@ -116,7 +116,7 @@ PurchaseStatus translate(StorePurchaseStatus purchaseStatus) noexcept { case StorePurchaseStatus::ServerError: return PurchaseStatus::ServerError; } - debug_assert(false, "Missing enum elements to translate StorePurchaseStatus."); + assert(false && "Missing enum elements to translate StorePurchaseStatus."); return StoreApi::PurchaseStatus::Unknown; // To be future proof. } } // namespace