winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit b2f860f643abbc9c5e7db8672e16050672959744
parent 7fe67eec639f08988056b82b92a7913cdee66b06
Author: Ashwini Patil <47225815+ashpatil-msft@users.noreply.github.com>
Date:   Mon, 19 Apr 2021 12:44:58 -0700

Rest source support tests (#861)

Added rest source support tests
Diffstat:
M.github/actions/spelling/expect.txt | 3+++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 5+++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 15+++++++++++++++
Asrc/AppInstallerCLITests/HttpClientHelper.cpp | 21+++++++++++++++++++++
Msrc/AppInstallerCLITests/RestClient.cpp | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Asrc/AppInstallerCLITests/RestInterface_1_0.cpp | 414+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLITests/SearchRequestSerializer.cpp | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLITests/TestRestRequestHandler.cpp | 25+++++++++++++++++++++++++
Asrc/AppInstallerCLITests/TestRestRequestHandler.h | 21+++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp | 48++++++++++++++++++++++++++++++------------------
Msrc/AppInstallerRepositoryCore/Rest/HttpClientHelper.h | 23+++++++++++------------
Msrc/AppInstallerRepositoryCore/Rest/RestClient.cpp | 9++++-----
Msrc/AppInstallerRepositoryCore/Rest/RestClient.h | 6++++--
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp | 12++++++------
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h | 5++++-
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/InformationResponseDeserializer.cpp | 6++++++
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.cpp | 7+++++++
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.cpp | 5++---
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer.cpp | 1+
19 files changed, 701 insertions(+), 49 deletions(-)

diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -23,6 +23,7 @@ AType AUrl Aysnc azurewebsites +Baz Beigi bfd bght @@ -30,6 +31,7 @@ bitmask bkup blargle blogs +Bluetooth bomgar BOMs brk @@ -276,5 +278,6 @@ wsv wto Wunused WZDNCRFJ +XPLATSTR xsi zy diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -176,6 +176,7 @@ <ItemGroup> <ClInclude Include="pch.h" /> <ClInclude Include="TestCommon.h" /> + <ClInclude Include="TestRestRequestHandler.h" /> <ClInclude Include="TestHooks.h" /> <ClInclude Include="TestSettings.h" /> <ClInclude Include="TestSource.h" /> @@ -189,6 +190,7 @@ <ClCompile Include="ExperimentalFeature.cpp" /> <ClCompile Include="GroupPolicy.cpp" /> <ClCompile Include="HashCommand.cpp" /> + <ClCompile Include="HttpClientHelper.cpp" /> <ClCompile Include="ManifestComparator.cpp" /> <ClCompile Include="JsonHelper.cpp" /> <ClCompile Include="MsixInfo.cpp" /> @@ -200,8 +202,11 @@ <ClCompile Include="Registry.cpp" /> <ClCompile Include="RestClient.cpp" /> <ClCompile Include="RestHelper.cpp" /> + <ClCompile Include="RestInterface_1_0.cpp" /> + <ClCompile Include="SearchRequestSerializer.cpp" /> <ClCompile Include="SQLiteIndexSource.cpp" /> <ClCompile Include="Strings.cpp" /> + <ClCompile Include="TestRestRequestHandler.cpp" /> <ClCompile Include="TestSettings.cpp" /> <ClCompile Include="TestSource.cpp" /> <ClCompile Include="UserSettings.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -36,6 +36,9 @@ <ClInclude Include="TestSettings.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="TestRestRequestHandler.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -149,6 +152,18 @@ <ClCompile Include="RestClient.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="RestInterface_1_0.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="TestRestRequestHandler.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="HttpClientHelper.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="SearchRequestSerializer.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLITests/HttpClientHelper.cpp b/src/AppInstallerCLITests/HttpClientHelper.cpp @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "TestRestRequestHandler.h" +#include <AppInstallerErrors.h> +#include <Rest/HttpClientHelper.h> + +using namespace AppInstaller::Repository::Rest; + +TEST_CASE("ExtractJsonResponse_UnsupportedMimeType", "[RestSource][RestSearch]") +{ + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, L"", web::http::details::mime_types::text_plain) }; + REQUIRE_THROWS_HR(helper.HandleGet(L"https://testUri"), APPINSTALLER_CLI_ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE); +} + +TEST_CASE("ValidateAndExtractResponse_ServiceUnavailable", "[RestSource]") +{ + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::ServiceUnavailable) }; + REQUIRE_THROWS_HR(helper.HandleGet(L"https://testUri"), MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, web::http::status_codes::ServiceUnavailable)); +} diff --git a/src/AppInstallerCLITests/RestClient.cpp b/src/AppInstallerCLITests/RestClient.cpp @@ -2,16 +2,20 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" +#include "TestRestRequestHandler.h" #include <Rest/RestClient.h> #include <Rest/Schema/IRestClient.h> #include <AppInstallerVersions.h> #include <set> +#include <AppInstallerErrors.h> using namespace AppInstaller; using namespace AppInstaller::Utility; using namespace AppInstaller::Repository::Rest; using namespace AppInstaller::Repository::Rest::Schema; +const utility::string_t TestRestUri = L"http://restsource.net"; + TEST_CASE("GetLatestCommonVersion", "[RestSource]") { std::set<AppInstaller::Utility::Version> wingetSupportedContracts = { Version {"1.0.0"}, Version {"1.2.0"} }; @@ -34,8 +38,56 @@ TEST_CASE("GetLatestCommonVersion_UnsupportedVersion", "[RestSource]") TEST_CASE("GetSupportedInterface", "[RestSource]") { Version version{ "1.0.0" }; - REQUIRE(RestClient::GetSupportedInterface("https://restsource.net", version)->GetVersion() == version); + REQUIRE(RestClient::GetSupportedInterface(utility::conversions::to_utf8string(TestRestUri), version)->GetVersion() == version); Version invalid{ "1.2.0" }; - REQUIRE_THROWS(RestClient::GetSupportedInterface("https://restsource.net", invalid)); + REQUIRE_THROWS(RestClient::GetSupportedInterface(utility::conversions::to_utf8string(TestRestUri), invalid)); +} + +TEST_CASE("GetSupportedVersion_Success", "[RestSource]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : { + "SourceIdentifier": "Source123", + "ServerSupportedVersions": [ + "0.2.0", + "1.0.0"] + }})delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; + std::set<AppInstaller::Utility::Version> wingetSupportedContracts = { Version {"1.3.0"}, Version {"0.2.0"}, Version {"1.10.0"} }; + REQUIRE(RestClient::GetSupportedVersion(TestRestUri, wingetSupportedContracts, std::move(helper)) == Version{ "0.2.0" }); +} + +TEST_CASE("GetSupportedVersion_UnexpectedVersion", "[RestSource]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : { + "SourceIdentifier": "Source123", + "ServerSupportedVersions": [ + "1.2.0", + "2.0.0"] + }})delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; + std::set<AppInstaller::Utility::Version> wingetSupportedContracts = { Version {"1.0.0"}, Version {"0.2.0"} }; + REQUIRE_THROWS_HR(RestClient::GetSupportedVersion(TestRestUri, wingetSupportedContracts, std::move(helper)), + APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE); +} + +TEST_CASE("RestClientCreate", "[RestSource]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : { + "SourceIdentifier": "Source123", + "ServerSupportedVersions": [ + "1.0.0", + "2.0.0"] + }})delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; + REQUIRE_NOTHROW(RestClient::Create(utility::conversions::to_utf8string(TestRestUri), std::move(helper))); } diff --git a/src/AppInstallerCLITests/RestInterface_1_0.cpp b/src/AppInstallerCLITests/RestInterface_1_0.cpp @@ -0,0 +1,414 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "TestRestRequestHandler.h" +#include <set> +#include <Rest/Schema/1_0/Interface.h> +#include <Rest/Schema/IRestClient.h> +#include <AppInstallerVersions.h> +#include <AppInstallerErrors.h> +#include <winget/ManifestValidation.h> +#include <Public/AppInstallerSHA256.h> + +using namespace TestCommon; +using namespace AppInstaller::Utility; +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Repository::Rest; +using namespace AppInstaller::Repository::Rest::Schema; +using namespace AppInstaller::Repository::Rest::Schema::V1_0; + +namespace +{ + const std::string TestRestUriString = "http://restsource.com/api"; + + utility::string_t GetGoodManifest_RequiredFields() + { + return _XPLATSTR( + R"delimiter({ + "Data": { + "PackageIdentifier": "Foo.Bar", + "Versions": [ + { + "PackageVersion": "5.0.0", + "DefaultLocale": { + "PackageLocale": "en-us", + "Publisher": "Foo", + "PackageName": "Bar", + "License": "Foo bar license", + "ShortDescription": "Foo bar description" + }, + "Installers": [ + { + "Architecture": "x64", + "InstallerSha256": "011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6", + "InstallerType": "exe", + "InstallerUrl": "https://installer.example.com/foobar.exe" + } + ] + } + ] + } + })delimiter"); + } + + struct GoodManifest_AllFields + { + public: + utility::string_t GetSampleManifest_AllFields() + { + utility::string_t id = L"Foo.Bar"; + return _XPLATSTR( + R"delimiter( + { + "Data": { + "PackageIdentifier": "Foo.Bar", + "Versions": [ + { + "PackageVersion": "3.0.0abc", + "DefaultLocale": { + "PackageLocale": "en-US", + "Publisher": "Foo", + "PublisherUrl": "http://publisher.net", + "PublisherSupportUrl": "http://publisherSupport.net", + "PrivacyUrl": "http://packagePrivacyUrl.net", + "Author": "FooBar", + "PackageName": "Bar", + "PackageUrl": "http://packageUrl.net", + "License": "Foo Bar License", + "LicenseUrl": "http://licenseUrl.net", + "Copyright": "Foo Bar Copyright", + "CopyrightUrl": "http://copyrightUrl.net", + "ShortDescription": "Foo bar is a foo bar.", + "Description": "Foo bar is a placeholder.", + "Tags": [ + "FooBar", + "Foo", + "Bar" + ], + "Moniker": "FooBarMoniker" + }, + "Channel": "", + "Locales": [ + { + "PackageLocale": "fr-Fr", + "Publisher": "Foo French", + "PublisherUrl": "http://publisher-fr.net", + "PublisherSupportUrl": "http://publisherSupport-fr.net", + "PrivacyUrl": "http://packagePrivacyUrl-fr.net", + "Author": "FooBar French", + "PackageName": "Bar", + "PackageUrl": "http://packageUrl-fr.net", + "License": "Foo Bar License", + "LicenseUrl": "http://licenseUrl-fr.net", + "Copyright": "Foo Bar Copyright", + "CopyrightUrl": "http://copyrightUrl-fr.net", + "ShortDescription": "Foo bar is a foo bar French.", + "Description": "Foo bar is a placeholder French.", + "Tags": [ + "FooBarFr", + "FooFr", + "BarFr" + ] + } + ], + "Installers": [ + { + "InstallerSha256": "011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6", + "InstallerUrl": "http://foobar.exe", + "Architecture": "x86", + "InstallerLocale": "en-US", + "Platform": [ + "Windows.Desktop" + ], + "MinimumOSVersion": "1078", + "InstallerType": "msix", + "Scope": "user", + "SignatureSha256": "011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6", + "InstallModes": [ + "interactive" + ], + "InstallerSwitches": { + "Silent": "/s", + "SilentWithProgress": "/s", + "Interactive": "/i", + "InstallLocation": "C:\\Users\\User1", + "Log": "/l", + "Upgrade": "/u", + "Custom": "/custom" + }, + "InstallerSuccessCodes": [ + 0 + ], + "UpgradeBehavior": "install", + "Commands": [ + "command1" + ], + "Protocols": [ + "protocol1" + ], + "FileExtensions": [ + ".file-extension" + ], + "Dependencies": { + "WindowsFeatures": [ + "feature1" + ], + "WindowsLibraries": [ + "library1" + ], + "PackageDependencies": [ + { + "PackageIdentifier": "Foo.Baz", + "MinimumVersion": "2.0.0" + } + ], + "ExternalDependencies": [ + "FooBarBaz" + ] + }, + "PackageFamilyName": "FooBar.PackageFamilyName", + "ProductCode": "", + "Capabilities": [ + "Bluetooth" + ], + "RestrictedCapabilities": [ + "restrictedCapability" + ] + } + ] + } + ] + }, + "ContinuationToken": "abcd" + })delimiter"); + } + + void VerifyLocalizations_AllFields(Manifest manifest) + { + REQUIRE(manifest.DefaultLocalization.Locale == "en-US"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Publisher>() == "Foo"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PublisherUrl>() == "http://publisher.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PublisherSupportUrl>() == "http://publisherSupport.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PrivacyUrl>() == "http://packagePrivacyUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Author>() == "FooBar"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageName>() == "Bar"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageUrl>() == "http://packageUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::License>() == "Foo Bar License"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::LicenseUrl>() == "http://licenseUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Copyright>() == "Foo Bar Copyright"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::CopyrightUrl>() == "http://copyrightUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::ShortDescription>() == "Foo bar is a foo bar."); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Description>() == "Foo bar is a placeholder."); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Tags>().size() == 3); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Tags>().at(0) == "FooBar"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Tags>().at(1) == "Foo"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Tags>().at(2) == "Bar"); + + REQUIRE(manifest.Localizations.size() == 1); + ManifestLocalization frenchLocalization = manifest.Localizations.at(0); + REQUIRE(frenchLocalization.Locale == "fr-Fr"); + REQUIRE(frenchLocalization.Get<Localization::Publisher>() == "Foo French"); + REQUIRE(frenchLocalization.Get<Localization::PublisherUrl>() == "http://publisher-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::PublisherSupportUrl>() == "http://publisherSupport-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::PrivacyUrl>() == "http://packagePrivacyUrl-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::Author>() == "FooBar French"); + REQUIRE(frenchLocalization.Get<Localization::PackageName>() == "Bar"); + REQUIRE(frenchLocalization.Get<Localization::PackageUrl>() == "http://packageUrl-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::License>() == "Foo Bar License"); + REQUIRE(frenchLocalization.Get<Localization::LicenseUrl>() == "http://licenseUrl-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::Copyright>() == "Foo Bar Copyright"); + REQUIRE(frenchLocalization.Get<Localization::CopyrightUrl>() == "http://copyrightUrl-fr.net"); + REQUIRE(frenchLocalization.Get<Localization::ShortDescription>() == "Foo bar is a foo bar French."); + REQUIRE(frenchLocalization.Get<Localization::Description>() == "Foo bar is a placeholder French."); + REQUIRE(frenchLocalization.Get<Localization::Tags>().size() == 3); + REQUIRE(frenchLocalization.Get<Localization::Tags>().at(0) == "FooBarFr"); + REQUIRE(frenchLocalization.Get<Localization::Tags>().at(1) == "FooFr"); + REQUIRE(frenchLocalization.Get<Localization::Tags>().at(2) == "BarFr"); + } + + void VerifyInstallers_AllFields(Manifest manifest) + { + REQUIRE(manifest.Installers.size() == 1); + + ManifestInstaller actualInstaller = manifest.Installers.at(0); + REQUIRE(actualInstaller.Sha256 == AppInstaller::Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6")); + REQUIRE(actualInstaller.Url == "http://foobar.exe"); + REQUIRE(actualInstaller.Arch == Architecture::X86); + REQUIRE(actualInstaller.Locale == "en-US"); + REQUIRE(actualInstaller.Platform.size() == 1); + REQUIRE(actualInstaller.Platform[0] == PlatformEnum::Desktop); + REQUIRE(actualInstaller.MinOSVersion == "1078"); + REQUIRE(actualInstaller.InstallerType == InstallerTypeEnum::Msix); + REQUIRE(actualInstaller.Scope == ScopeEnum::User); + REQUIRE(actualInstaller.SignatureSha256 == AppInstaller::Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6")); + REQUIRE(actualInstaller.InstallModes.size() == 1); + REQUIRE(actualInstaller.InstallModes.at(0) == InstallModeEnum::Interactive); + REQUIRE(actualInstaller.Switches.size() == 7); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Silent) == "/s"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::SilentWithProgress) == "/s"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Interactive) == "/i"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::InstallLocation) == "C:\\Users\\User1"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Log) == "/l"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Update) == "/u"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Custom) == "/custom"); + REQUIRE(actualInstaller.InstallerSuccessCodes.size() == 1); + REQUIRE(actualInstaller.InstallerSuccessCodes.at(0) == 0); + REQUIRE(actualInstaller.UpdateBehavior == UpdateBehaviorEnum::Install); + REQUIRE(actualInstaller.Commands.at(0) == "command1"); + REQUIRE(actualInstaller.Protocols.at(0) == "protocol1"); + REQUIRE(actualInstaller.FileExtensions.at(0) == ".file-extension"); + REQUIRE(actualInstaller.Dependencies.WindowsFeatures.at(0) == "feature1"); + REQUIRE(actualInstaller.Dependencies.WindowsLibraries.at(0) == "library1"); + REQUIRE(actualInstaller.Dependencies.PackageDependencies.at(0).Id == "Foo.Baz"); + REQUIRE(actualInstaller.Dependencies.PackageDependencies.at(0).MinVersion == "2.0.0"); + REQUIRE(actualInstaller.Dependencies.ExternalDependencies.at(0) == "FooBarBaz"); + REQUIRE(actualInstaller.PackageFamilyName == "FooBar.PackageFamilyName"); + REQUIRE(actualInstaller.ProductCode == ""); + REQUIRE(actualInstaller.Capabilities.at(0) == "Bluetooth"); + REQUIRE(actualInstaller.RestrictedCapabilities.at(0) == "restrictedCapability"); + } + }; +} + +TEST_CASE("Search_GoodResponse", "[RestSource]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : [ + { + "PackageIdentifier": "git.package", + "PackageName": "package", + "Publisher": "git", + "Versions": [ + { "PackageVersion": "1.0.0" }, + { "PackageVersion": "2.0.0"}] + }] + })delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1{ TestRestUriString, std::move(helper) }; + Schema::IRestClient::SearchResult searchResponse = v1.Search({}); + REQUIRE(searchResponse.Matches.size() == 1); + Schema::IRestClient::Package package = searchResponse.Matches.at(0); + REQUIRE(package.PackageInformation.PackageIdentifier.compare("git.package") == 0); + REQUIRE(package.PackageInformation.Publisher.compare("git") == 0); + REQUIRE(package.PackageInformation.PackageName.compare("package") == 0); + REQUIRE(package.Versions.size() == 2); + REQUIRE(package.Versions.at(0).VersionAndChannel.GetVersion().ToString().compare("1.0.0") == 0); + REQUIRE(package.Versions.at(1).VersionAndChannel.GetVersion().ToString().compare("2.0.0") == 0); +} + +TEST_CASE("Search_BadResponse_NoVersions", "[RestSource]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : [ + { + "PackageIdentifier": "git.package", + "PackageName": "package", + "Publisher": "git", + "Versions": null }] + })delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1{ TestRestUriString, std::move(helper) }; + REQUIRE_THROWS_HR(v1.Search({}), APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA); +} + +TEST_CASE("Search_BadResponse_NotFoundCode", "[RestSource]") +{ + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::NotFound) }; + Interface v1{ TestRestUriString, std::move(helper) }; + Schema::IRestClient::SearchResult result = v1.Search({}); + REQUIRE(result.Matches.empty()); +} + +TEST_CASE("Search_Optimized_ManifestResponse", "[RestSource]") +{ + utility::string_t sample = GetGoodManifest_RequiredFields(); + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + AppInstaller::Repository::SearchRequest request; + PackageMatchFilter filter{ PackageMatchField::Id, MatchType::Exact, "Foo" }; + request.Filters.emplace_back(std::move(filter)); + Interface v1{ TestRestUriString, std::move(helper) }; + Schema::IRestClient::SearchResult result = v1.Search(request); + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].Versions.size() == 1); + REQUIRE(result.Matches[0].Versions[0].VersionAndChannel.GetVersion().ToString() == "5.0.0"); + REQUIRE(result.Matches[0].Versions[0].VersionAndChannel.GetChannel().ToString() == ""); + REQUIRE(result.Matches[0].Versions[0].Manifest); + + // Verify manifest is populated + Manifest manifest = result.Matches[0].Versions[0].Manifest.value(); + REQUIRE(manifest.Id == "Foo.Bar"); + REQUIRE(manifest.Version == "5.0.0"); + REQUIRE(manifest.DefaultLocalization.Locale == "en-us"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Publisher>() == "Foo"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageName>() == "Bar"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::License>() == "Foo bar license"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::ShortDescription>() == "Foo bar description"); + REQUIRE(manifest.Installers.size() == 1); + REQUIRE(manifest.Installers[0].Arch == Architecture::X64); + REQUIRE(manifest.Installers[0].Sha256 == AppInstaller::Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6")); + REQUIRE(manifest.Installers[0].InstallerType == InstallerTypeEnum::Exe); + REQUIRE(manifest.Installers[0].Url == "https://installer.example.com/foobar.exe"); +} + +TEST_CASE("Search_Optimized_NoResponse_NotFoundCode", "[RestSource]") +{ + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::NotFound) }; + AppInstaller::Repository::SearchRequest request; + PackageMatchFilter filter{ PackageMatchField::Id, MatchType::Exact, "Foo" }; + request.Filters.emplace_back(std::move(filter)); + Interface v1{ TestRestUriString, std::move(helper) }; + Schema::IRestClient::SearchResult result = v1.Search(request); + REQUIRE(result.Matches.empty()); +} + +TEST_CASE("GetManifests_GoodResponse", "[RestSource]") +{ + GoodManifest_AllFields sampleManifest; + utility::string_t sample = sampleManifest.GetSampleManifest_AllFields(); + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1{ TestRestUriString, std::move(helper) }; + std::vector<Manifest> manifests = v1.GetManifests("Foo.Bar"); + REQUIRE(manifests.size() == 1); + + // Verify manifest is populated + Manifest manifest = manifests[0]; + REQUIRE(manifest.Id == "Foo.Bar"); + REQUIRE(manifest.Version == "3.0.0abc"); + REQUIRE(manifest.Moniker == "FooBarMoniker"); + REQUIRE(manifest.Channel == ""); + sampleManifest.VerifyLocalizations_AllFields(manifest); + sampleManifest.VerifyInstallers_AllFields(manifest); +} + +TEST_CASE("GetManifests_BadResponse_SuccessCode", "[RestSource]") +{ + utility::string_t badManifest = _XPLATSTR( + R"delimiter({ + "Data": { + "PackageIdentifier": "Foo.Bar", + "Versions": [ + { + "PackageVersion": "5.0.0" + } + ] + } + })delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(badManifest)) }; + Interface v1{ TestRestUriString, std::move(helper) }; + REQUIRE_THROWS_HR(v1.GetManifests("Foo.Bar"), APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA); +} + +TEST_CASE("GetManifests_NotFoundCode", "[RestSource]") +{ + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::NotFound) }; + Interface v1{ TestRestUriString, std::move(helper) }; + std::vector<Manifest> manifests = v1.GetManifests("Foo.Bar"); + REQUIRE(manifests.empty()); +} diff --git a/src/AppInstallerCLITests/SearchRequestSerializer.cpp b/src/AppInstallerCLITests/SearchRequestSerializer.cpp @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "TestRestRequestHandler.h" +#include <AppInstallerErrors.h> +#include <Rest/Schema/1_0/Json/SearchRequestSerializer.h> + +using namespace TestCommon; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Repository::Rest::Schema::V1_0; +using namespace AppInstaller::Repository::Rest::Schema::V1_0::Json; + +TEST_CASE("SearchRequestSerializer_InclusionsFilters", "[RestSource]") +{ + SearchRequest searchRequest; + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::Substring, "Foo.Bar")); + searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Moniker, MatchType::Exact, "FooBar")); + searchRequest.MaximumResults = 10; + + SearchRequestSerializer serializer; + web::json::value actual = serializer.Serialize(searchRequest); + + REQUIRE(!actual.is_null()); + REQUIRE(!actual.has_field(L"FetchAllManifests")); + REQUIRE(actual.at(L"MaximumResults").as_integer() == static_cast<int>(searchRequest.MaximumResults)); + + // Inclusions + web::json::array inclusions = actual.at(L"Inclusions").as_array(); + REQUIRE(inclusions.size() == 1); + REQUIRE(inclusions.at(0).at(L"PackageMatchField").as_string() == L"Id"); + web::json::value requestMatch = inclusions.at(0).at(L"RequestMatch"); + REQUIRE(!requestMatch.is_null()); + REQUIRE(requestMatch.at(L"KeyWord").as_string() == L"Foo.Bar"); + REQUIRE(requestMatch.at(L"MatchType").as_string() == L"Substring"); + + // Filters + web::json::array filters = actual.at(L"Filters").as_array(); + REQUIRE(filters.size() == 1); + REQUIRE(filters.at(0).at(L"PackageMatchField").as_string() == L"Moniker"); + web::json::value requestMatchFilter = filters.at(0).at(L"RequestMatch"); + REQUIRE(!requestMatchFilter.is_null()); + REQUIRE(requestMatchFilter.at(L"KeyWord").as_string() == L"FooBar"); + REQUIRE(requestMatchFilter.at(L"MatchType").as_string() == L"Exact"); +} + +TEST_CASE("SearchRequestSerializer_Query", "[RestSource]") +{ + SearchRequest searchRequest; + searchRequest.Query = RequestMatch(MatchType::Substring, "Foo.Bar"); + + SearchRequestSerializer serializer; + web::json::value actual = serializer.Serialize(std::move(searchRequest)); + + REQUIRE(!actual.is_null()); + web::json::value query = actual.at(L"Query"); + REQUIRE(query.at(L"KeyWord").as_string() == L"Foo.Bar"); + REQUIRE(query.at(L"MatchType").as_string() == L"Substring"); +} + +TEST_CASE("SearchRequestSerializer_FetchAllManifests", "[RestSource]") +{ + SearchRequestSerializer serializer; + web::json::value actual = serializer.Serialize({}); + + REQUIRE(!actual.is_null()); + REQUIRE(actual.at(L"FetchAllManifests").as_bool()); +} diff --git a/src/AppInstallerCLITests/TestRestRequestHandler.cpp b/src/AppInstallerCLITests/TestRestRequestHandler.cpp @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "TestRestRequestHandler.h" +#include <Rest/Schema/1_0/Interface.h> +#include <Rest/Schema/IRestClient.h> + +std::shared_ptr<TestRestRequestHandler> GetTestRestRequestHandler( + const web::http::status_code statusCode, const utility::string_t& sampleResponseString, const utility::string_t& mimeType) +{ + return std::make_shared<TestRestRequestHandler>([statusCode, sampleResponseString, mimeType](web::http::http_request request) -> + pplx::task<web::http::http_response> + { + web::http::http_response response; + if (!sampleResponseString.empty()) + { + response.set_body(web::json::value::parse(sampleResponseString)); + } + + response.headers().set_content_type(mimeType); + response.set_status_code(statusCode); + return pplx::task_from_result(response); + }); +} diff --git a/src/AppInstallerCLITests/TestRestRequestHandler.h b/src/AppInstallerCLITests/TestRestRequestHandler.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <cpprest/http_client.h> + +class TestRestRequestHandler : public web::http::http_pipeline_stage +{ +public: + TestRestRequestHandler(const std::function<pplx::task<web::http::http_response>(web::http::http_request request)>& handler) : m_handler(handler) {} + + virtual pplx::task<web::http::http_response> propagate(web::http::http_request request) + { + return m_handler(request); + } + +private: + std::function<pplx::task<web::http::http_response>(web::http::http_request request)> m_handler; +}; + +std::shared_ptr<TestRestRequestHandler> GetTestRestRequestHandler( + const web::http::status_code statusCode, const utility::string_t& sampleResponseString = {}, const utility::string_t& mimeType = web::http::details::mime_types::application_json); diff --git a/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp b/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp @@ -5,12 +5,13 @@ namespace AppInstaller::Repository::Rest { - HttpClientHelper::HttpClientHelper(const utility::string_t& url) : m_client(url), m_url(url) {} + HttpClientHelper::HttpClientHelper(std::optional<std::shared_ptr<web::http::http_pipeline_stage>> stage) : m_defaultRequestHandlerStage(stage) {} pplx::task<web::http::http_response> HttpClientHelper::Post( - const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) + const utility::string_t& uri, const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) const { - AICLI_LOG(Repo, Verbose, << "Sending http POST request to: " << utility::conversions::to_utf8string(m_url)); + AICLI_LOG(Repo, Verbose, << "Sending http POST request to: " << utility::conversions::to_utf8string(uri)); + web::http::client::http_client client = GetClient(uri); web::http::http_request request{ web::http::methods::POST }; request.headers().set_content_type(web::http::details::mime_types::application_json); request.set_body(body.serialize()); @@ -21,14 +22,14 @@ namespace AppInstaller::Repository::Rest request.headers().add(pair.first, pair.second); } - return MakeRequest(request); + return client.request(request); } std::optional<web::json::value> HttpClientHelper::HandlePost( - const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) + const utility::string_t& uri, const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) const { web::http::http_response httpResponse; - HttpClientHelper::Post(body, headers).then([&httpResponse](const web::http::http_response& response) + HttpClientHelper::Post(uri, body, headers).then([&httpResponse](const web::http::http_response& response) { AICLI_LOG(Repo, Verbose, << "Response status: " << response.status_code()); httpResponse = response; @@ -37,9 +38,11 @@ namespace AppInstaller::Repository::Rest return ValidateAndExtractResponse(httpResponse); } - pplx::task<web::http::http_response> HttpClientHelper::Get(const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) + pplx::task<web::http::http_response> HttpClientHelper::Get( + const utility::string_t& uri, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) const { - AICLI_LOG(Repo, Verbose, << "Sending http GET request to: " << utility::conversions::to_utf8string(m_url)); + AICLI_LOG(Repo, Verbose, << "Sending http GET request to: " << utility::conversions::to_utf8string(uri)); + web::http::client::http_client client = GetClient(uri); web::http::http_request request{ web::http::methods::GET }; request.headers().set_content_type(web::http::details::mime_types::application_json); @@ -49,13 +52,14 @@ namespace AppInstaller::Repository::Rest request.headers().add(pair.first, pair.second); } - return MakeRequest(request); + return client.request(request); } - std::optional<web::json::value> HttpClientHelper::HandleGet(const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) + std::optional<web::json::value> HttpClientHelper::HandleGet( + const utility::string_t& uri, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers) const { web::http::http_response httpResponse; - Get(headers).then([&httpResponse](const web::http::http_response& response) + Get(uri, headers).then([&httpResponse](const web::http::http_response& response) { AICLI_LOG(Repo, Verbose, << "Response status: " << response.status_code()); httpResponse = response; @@ -64,7 +68,20 @@ namespace AppInstaller::Repository::Rest return ValidateAndExtractResponse(httpResponse); } - std::optional<web::json::value> HttpClientHelper::ValidateAndExtractResponse(const web::http::http_response& response) + web::http::client::http_client HttpClientHelper::GetClient(const utility::string_t& uri) const + { + web::http::client::http_client client{ uri }; + + // Add default custom handlers if any. + if (m_defaultRequestHandlerStage) + { + client.add_handler(m_defaultRequestHandlerStage.value()); + } + + return client; + } + + std::optional<web::json::value> HttpClientHelper::ValidateAndExtractResponse(const web::http::http_response& response) const { std::optional<web::json::value> result; switch (response.status_code()) @@ -89,7 +106,7 @@ namespace AppInstaller::Repository::Rest return result; } - std::optional<web::json::value> HttpClientHelper::ExtractJsonResponse(const web::http::http_response& response) + std::optional<web::json::value> HttpClientHelper::ExtractJsonResponse(const web::http::http_response& response) const { utility::string_t contentType = response.headers().content_type(); @@ -98,9 +115,4 @@ namespace AppInstaller::Repository::Rest return response.extract_json().get(); } - - pplx::task<web::http::http_response> HttpClientHelper::MakeRequest(web::http::http_request req) - { - return m_client.request(req); - } } diff --git a/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.h b/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.h @@ -11,25 +11,24 @@ namespace AppInstaller::Repository::Rest { struct HttpClientHelper { - HttpClientHelper(const utility::string_t& url); + HttpClientHelper(std::optional<std::shared_ptr<web::http::http_pipeline_stage>> = {}); - pplx::task<web::http::http_response> Post(const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}); + pplx::task<web::http::http_response> Post(const utility::string_t& uri, const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}) const; - std::optional<web::json::value> HandlePost(const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}); + std::optional<web::json::value> HandlePost(const utility::string_t& uri, const web::json::value& body, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}) const; - pplx::task<web::http::http_response> Get(const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}); - - std::optional<web::json::value> HandleGet(const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}); + pplx::task<web::http::http_response> Get(const utility::string_t& uri, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}) const; + std::optional<web::json::value> HandleGet(const utility::string_t& uri, const std::vector<std::pair<utility::string_t, utility::string_t>>& headers = {}) const; + protected: - pplx::task<web::http::http_response> MakeRequest(web::http::http_request req); - - std::optional<web::json::value> ValidateAndExtractResponse(const web::http::http_response& response); + std::optional<web::json::value> ValidateAndExtractResponse(const web::http::http_response& response) const; - std::optional<web::json::value> ExtractJsonResponse(const web::http::http_response& response); + std::optional<web::json::value> ExtractJsonResponse(const web::http::http_response& response) const; private: - web::http::client::http_client m_client; - utility::string_t m_url; + web::http::client::http_client GetClient(const utility::string_t& uri) const; + + std::optional<std::shared_ptr<web::http::http_pipeline_stage>> m_defaultRequestHandlerStage; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.cpp b/src/AppInstallerRepositoryCore/Rest/RestClient.cpp @@ -41,11 +41,10 @@ namespace AppInstaller::Repository::Rest return endpoint; } - Version RestClient::GetSupportedVersion(const utility::string_t& restApi, const std::set<Version>& wingetSupportedVersions) + Version RestClient::GetSupportedVersion(const utility::string_t& restApi, const std::set<Version>& wingetSupportedVersions, const HttpClientHelper& clientHelper) { // Call information endpoint - HttpClientHelper httpClientHelper{ GetInformationEndpoint(restApi) }; - std::optional<web::json::value> response = httpClientHelper.HandleGet(); + std::optional<web::json::value> response = clientHelper.HandleGet(GetInformationEndpoint(restApi)); THROW_HR_IF(APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE, !response); @@ -89,12 +88,12 @@ namespace AppInstaller::Repository::Rest THROW_HR(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION); } - RestClient RestClient::Create(const std::string& restApi) + RestClient RestClient::Create(const std::string& restApi, const HttpClientHelper& helper) { utility::string_t restEndpoint = RestHelper::GetRestAPIBaseUri(restApi); THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL, !RestHelper::IsValidUri(restEndpoint)); - Version version = GetSupportedVersion(restEndpoint, WingetSupportedContracts); + Version version = GetSupportedVersion(restEndpoint, WingetSupportedContracts, helper); std::unique_ptr<Schema::IRestClient> supportedInterface = GetSupportedInterface(utility::conversions::to_utf8string(restEndpoint), version); return RestClient{ std::move(supportedInterface) }; } diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.h b/src/AppInstallerRepositoryCore/Rest/RestClient.h @@ -4,6 +4,8 @@ #include <set> #include <cpprest/json.h> #include "Rest/Schema/IRestClient.h" +#include "Rest/HttpClientHelper.h" +#include "cpprest/json.h" namespace AppInstaller::Repository::Rest { @@ -29,11 +31,11 @@ namespace AppInstaller::Repository::Rest static utility::string_t GetInformationEndpoint(const utility::string_t& restApiUri); - static AppInstaller::Utility::Version GetSupportedVersion(const utility::string_t& restApi, const std::set<AppInstaller::Utility::Version>& wingetSupportedVersions); + static AppInstaller::Utility::Version GetSupportedVersion(const utility::string_t& restApi, const std::set<AppInstaller::Utility::Version>& wingetSupportedVersions, const HttpClientHelper& httpClientHelper); static std::unique_ptr<Schema::IRestClient> GetSupportedInterface(const std::string& restApi, const AppInstaller::Utility::Version& version); - static RestClient Create(const std::string& restApi); + static RestClient Create(const std::string& restApi, const HttpClientHelper& helper = {}); private: std::unique_ptr<Schema::IRestClient> m_interface; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp @@ -52,11 +52,10 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 } } - Interface::Interface(const std::string& restApi) + Interface::Interface(const std::string& restApi, const HttpClientHelper& httpClientHelper) : m_restApiUri(restApi), m_httpClientHelper(httpClientHelper) { THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL, !RestHelper::IsValidUri(JsonHelper::GetUtilityString(restApi))); - m_restApiUri = restApi; m_searchEndpoint = GetSearchEndpoint(m_restApiUri); m_requiredRestApiHeaders.emplace_back( std::pair(JsonHelper::GetUtilityString(ContractVersion), JsonHelper::GetUtilityString(GetVersion().ToString()))); @@ -76,8 +75,8 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 } // TODO: Handle continuation token - HttpClientHelper clientHelper{ m_searchEndpoint }; - std::optional<web::json::value> jsonObject = clientHelper.HandlePost(GetSearchBody(request), m_requiredRestApiHeaders); + std::optional<web::json::value> jsonObject = m_httpClientHelper.HandlePost( + m_searchEndpoint, GetSearchBody(request), m_requiredRestApiHeaders); if (!jsonObject) { @@ -115,6 +114,8 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 bool Interface::MeetsOptimizedSearchCriteria(const SearchRequest& request) const { + // Optimization: If the user wants to install a certain package with an exact match on package id and a particular rest source, we will + // call the package manifest endpoint to get the manifest directly instead of running a search for it. if (!request.Query && request.Inclusions.size() == 0 && request.Filters.size() == 1 && request.Filters[0].Field == PackageMatchField::Id && request.Filters[0].Type == MatchType::Exact) @@ -157,8 +158,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 std::vector<Manifest::Manifest> Interface::GetManifests(const std::string& packageId, const std::map<std::string_view, std::string>& params) const { std::vector<Manifest::Manifest> results; - HttpClientHelper clientHelper{ GetManifestByVersionEndpoint(m_restApiUri, packageId, params) }; - std::optional<web::json::value> jsonObject = clientHelper.HandleGet(m_requiredRestApiHeaders); + std::optional<web::json::value> jsonObject = m_httpClientHelper.HandleGet(GetManifestByVersionEndpoint(m_restApiUri, packageId, params), m_requiredRestApiHeaders); if (!jsonObject) { diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h @@ -3,6 +3,8 @@ #pragma once #include "Rest/Schema/IRestClient.h" #include <cpprest/json.h> +#include "cpprest/json.h" +#include "Rest/HttpClientHelper.h" #include <vector> namespace AppInstaller::Repository::Rest::Schema::V1_0 @@ -10,7 +12,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 // Interface to this schema version exposed through IRestClient. struct Interface : public IRestClient { - Interface(const std::string& restApi); + Interface(const std::string& restApi, const HttpClientHelper& httpClientHelper = {}); Interface(const Interface&) = delete; Interface& operator=(const Interface&) = delete; @@ -31,5 +33,6 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 std::string m_restApiUri; utility::string_t m_searchEndpoint; std::vector<std::pair<utility::string_t, utility::string_t>> m_requiredRestApiHeaders; + HttpClientHelper m_httpClientHelper; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/InformationResponseDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/InformationResponseDeserializer.cpp @@ -29,6 +29,12 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json { try { + if (dataObject.is_null()) + { + AICLI_LOG(Repo, Error, << "Missing json object."); + return {}; + } + std::optional<std::reference_wrapper<const web::json::value>> data = JsonHelper::GetJsonValueFromNode(dataObject, JsonHelper::GetUtilityString(Data)); if (!data) { diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.cpp @@ -96,6 +96,12 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json std::optional<std::vector<Manifest::Manifest>> ManifestDeserializer::DeserializeVersion(const web::json::value& dataJsonObject) const { + if (dataJsonObject.is_null()) + { + AICLI_LOG(Repo, Error, << "Missing json object."); + return {}; + } + std::vector<Manifest::Manifest> manifests; try { @@ -250,6 +256,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json } locale.Add<AppInstaller::Manifest::Localization::ShortDescription>(std::move(shortDescription.value())); + locale.Add<AppInstaller::Manifest::Localization::PublisherUrl>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PublisherUrl)).value_or("")); locale.Add<AppInstaller::Manifest::Localization::PublisherSupportUrl>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PublisherSupportUrl)).value_or("")); locale.Add<AppInstaller::Manifest::Localization::PrivacyUrl>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PrivacyUrl)).value_or("")); locale.Add<AppInstaller::Manifest::Localization::Author>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Author)).value_or("")); diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.cpp @@ -43,7 +43,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json if (searchRequest.IsForEverything()) { - json_body[JsonHelper::GetUtilityString(FetchAllManifests)] = web::json::value::string(L"true"); + json_body[JsonHelper::GetUtilityString(FetchAllManifests)] = web::json::value::boolean(true); return json_body; } @@ -51,8 +51,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json { auto& requestMatch = searchRequest.Query.value(); web::json::value requestMatchObject = web::json::value::object(); - requestMatchObject[JsonHelper::GetUtilityString(RequestMatch)] = GetRequestMatchJsonObject(requestMatch); - json_body[JsonHelper::GetUtilityString(Query)] = requestMatchObject; + json_body[JsonHelper::GetUtilityString(Query)] = GetRequestMatchJsonObject(requestMatch);; } if (!searchRequest.Filters.empty()) diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer.cpp @@ -35,6 +35,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json // Make search result from json output. if (searchResponseObject.is_null()) { + AICLI_LOG(Repo, Error, << "Missing json object."); return {}; }