commit 6540534476dde41d2fa77dfaeab0761e5c45761b parent 3556a3ed5992a5b4e6a9f9dbb0816dc33c2154a7 Author: JohnMcPMS <johnmcp@microsoft.com> Date: Tue, 29 Apr 2025 16:52:16 -0700 Dscv3 source resource (#5418) ## Change Add a new DSC v3 resource: `source`. This largely follows the design of the v2 resource, but uses the v3 paradigm for existence. Other minor changes: - Refactor the existing package resource tests into a test base. - Fix an issue building valijson. Diffstat:
17 files changed, 1757 insertions(+), 704 deletions(-)
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -306,6 +306,7 @@ <ClInclude Include="Commands\DscCommandBase.h" /> <ClInclude Include="Commands\DscComposableObject.h" /> <ClInclude Include="Commands\DscPackageResource.h" /> + <ClInclude Include="Commands\DscSourceResource.h" /> <ClInclude Include="Commands\DscTestFileResource.h" /> <ClInclude Include="Commands\DscTestJsonResource.h" /> <ClInclude Include="Commands\ErrorCommand.h" /> @@ -392,6 +393,7 @@ <ClCompile Include="Commands\DscCommandBase.cpp" /> <ClCompile Include="Commands\DscComposableObject.cpp" /> <ClCompile Include="Commands\DscPackageResource.cpp" /> + <ClCompile Include="Commands\DscSourceResource.cpp" /> <ClCompile Include="Commands\DscTestFileResource.cpp" /> <ClCompile Include="Commands\DscTestJsonResource.cpp" /> <ClCompile Include="Commands\ErrorCommand.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -284,6 +284,9 @@ <ClInclude Include="Commands\DscPackageResource.h"> <Filter>Commands\Configuration</Filter> </ClInclude> + <ClInclude Include="Commands\DscSourceResource.h"> + <Filter>Commands\Configuration</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -535,6 +538,9 @@ <ClCompile Include="Commands\DscPackageResource.cpp"> <Filter>Commands\Configuration</Filter> </ClCompile> + <ClCompile Include="Commands\DscSourceResource.cpp"> + <Filter>Commands\Configuration</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Commands/DscCommand.cpp b/src/AppInstallerCLICore/Commands/DscCommand.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "DscCommand.h" #include "DscPackageResource.h" +#include "DscSourceResource.h" #ifndef AICLI_DISABLE_TEST_HOOKS #include "DscTestFileResource.h" @@ -15,6 +16,7 @@ namespace AppInstaller::CLI { return InitializeFromMoveOnly<std::vector<std::unique_ptr<Command>>>({ std::make_unique<DscPackageResource>(FullName()), + std::make_unique<DscSourceResource>(FullName()), #ifndef AICLI_DISABLE_TEST_HOOKS std::make_unique<DscTestFileResource>(FullName()), std::make_unique<DscTestJsonResource>(FullName()), diff --git a/src/AppInstallerCLICore/Commands/DscComposableObject.h b/src/AppInstallerCLICore/Commands/DscComposableObject.h @@ -101,13 +101,13 @@ namespace AppInstaller::CLI // // struct Property // { - // using Type = { bool, std::string }; + // using PropertyType = { bool, std::string }; // static std::string_view Name(); // static void FromJson(Property*, const Json::Value*); // static std::optional<Json::Value> ToJson(const Property*); // - // const Type& PROPERTY_NAME() const; - // void PROPERTY_NAME(const Type&); + // const PropertyType& PROPERTY_NAME() const; + // void PROPERTY_NAME(const PropertyType&); // } template <typename... Property> struct DscComposableObject : public Property... @@ -149,15 +149,15 @@ namespace AppInstaller::CLI static Json::Value Schema(const std::string& title) { Json::Value result = details::GetBaseSchema(title); - (FoldHelper{}, ..., details::AddPropertySchema(result, Property::Name(), Property::Flags, GetJsonTypeValue<typename Property::Type>::SchemaType(), Property::Description(), Property::EnumValues(), Property::Default())); + (FoldHelper{}, ..., details::AddPropertySchema(result, Property::Name(), Property::Flags, GetJsonTypeValue<typename Property::PropertyType>::SchemaType(), Property::Description(), Property::EnumValues(), Property::Default())); return result; } }; - template <typename Derived, typename PropertyType, DscComposablePropertyFlag PropertyFlags> + template <typename Derived, typename PropertyTypeT, DscComposablePropertyFlag PropertyFlags> struct DscComposableProperty { - using Type = PropertyType; + using PropertyType = PropertyTypeT; static constexpr DscComposablePropertyFlag Flags = PropertyFlags; static void FromJson(Derived* self, const Json::Value* value, bool ignoreFieldRequirements) @@ -201,7 +201,7 @@ namespace AppInstaller::CLI } protected: - std::optional<Type> m_value; + std::optional<PropertyType> m_value; }; #define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL_START(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_, _enum_vals_, _default_) \ @@ -211,9 +211,9 @@ namespace AppInstaller::CLI static Resource::LocString Description() { return _description_; } \ static std::vector<std::string> EnumValues() { return std::vector<std::string> _enum_vals_; } \ static std::optional<std::string> Default() { return _default_; } \ - std::optional<Type>& _property_name_() { return m_value; } \ - const std::optional<Type>& _property_name_() const { return m_value; } \ - void _property_name_(std::optional<Type> value) { m_value = std::move(value); } \ + std::optional<PropertyType>& _property_name_() { return m_value; } \ + const std::optional<PropertyType>& _property_name_() const { return m_value; } \ + void _property_name_(std::optional<PropertyType> value) { m_value = std::move(value); } \ #define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_, _enum_vals_, _default_) \ WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL_START(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_, _enum_vals_, _default_) \ diff --git a/src/AppInstallerCLICore/Commands/DscPackageResource.cpp b/src/AppInstallerCLICore/Commands/DscPackageResource.cpp @@ -24,7 +24,7 @@ namespace AppInstaller::CLI WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_ENUM(MatchOptionProperty, std::string, MatchOption, "matchOption", Resource::String::DscResourcePropertyDescriptionPackageMatchOption, ({ "equals", "equalsCaseInsensitive", "startsWithCaseInsensitive", "containsCaseInsensitive" }), "equalsCaseInsensitive"); WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_DEFAULT(UseLatestProperty, bool, UseLatest, "useLatest", Resource::String::DscResourcePropertyDescriptionPackageUseLatest, "false"); WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_ENUM(InstallModeProperty, std::string, InstallMode, "installMode", Resource::String::DscResourcePropertyDescriptionPackageInstallMode, ({ "default", "silent", "interactive" }), "silent"); - WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(AcceptAgreementsProperty, bool, AcceptAgreements, "acceptAgreements", Resource::String::DscResourcePropertyDescriptionPackageAcceptAgreements); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(AcceptAgreementsProperty, bool, AcceptAgreements, "acceptAgreements", Resource::String::DscResourcePropertyDescriptionAcceptAgreements); // TODO: To support Scope on this resource: // 1. Change the installed source to pull in all package info for both scopes by default diff --git a/src/AppInstallerCLICore/Commands/DscSourceResource.cpp b/src/AppInstallerCLICore/Commands/DscSourceResource.cpp @@ -0,0 +1,429 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "DscSourceResource.h" +#include "DscComposableObject.h" +#include "Resources.h" +#include "Workflows/SourceFlow.h" +#include <winget/RepositorySource.h> + +using namespace AppInstaller::Utility::literals; +using namespace AppInstaller::Repository; + +namespace AppInstaller::CLI +{ + namespace + { + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(NameProperty, std::string, SourceName, "name", DscComposablePropertyFlag::Required | DscComposablePropertyFlag::CopyToOutput, Resource::String::DscResourcePropertyDescriptionSourceName); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(ArgumentProperty, std::string, Argument, "argument", Resource::String::DscResourcePropertyDescriptionSourceArgument); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(TypeProperty, std::string, Type, "type", Resource::String::DscResourcePropertyDescriptionSourceType); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_ENUM(TrustLevelProperty, std::string, TrustLevel, "trustLevel", Resource::String::DscResourcePropertyDescriptionSourceTrustLevel, ({ "undefined", "none", "trusted" }), "undefined"); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(ExplicitProperty, bool, Explicit, "explicit", Resource::String::DscResourcePropertyDescriptionSourceExplicit); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(AcceptAgreementsProperty, bool, AcceptAgreements, "acceptAgreements", Resource::String::DscResourcePropertyDescriptionAcceptAgreements); + + using SourceResourceObject = DscComposableObject<StandardExistProperty, StandardInDesiredStateProperty, NameProperty, ArgumentProperty, TypeProperty, TrustLevelProperty, ExplicitProperty, AcceptAgreementsProperty>; + + std::string TrustLevelStringFromFlags(SourceTrustLevel trustLevel) + { + return WI_IsFlagSet(trustLevel, SourceTrustLevel::Trusted) ? "trusted" : "none"; + } + + // The values as the resource uses them. + enum class ResourceTrustLevel + { + Undefined, + Invalid, + None, + Trusted + }; + + ResourceTrustLevel EffectiveTrustLevel(const std::optional<std::string>& input) + { + if (!input) + { + return ResourceTrustLevel::Undefined; + } + + std::string inputValue = Utility::ToLower(input.value()); + if (inputValue == "undefined") + { + return ResourceTrustLevel::Undefined; + } + else if (inputValue == "none") + { + return ResourceTrustLevel::None; + } + else if (inputValue == "trusted") + { + return ResourceTrustLevel::Trusted; + } + else + { + return ResourceTrustLevel::Invalid; + } + } + + struct SourceFunctionData + { + SourceFunctionData(Execution::Context& context, const std::optional<Json::Value>& json, bool ignoreFieldRequirements = false) : + Input(json, ignoreFieldRequirements), + ParentContext(context) + { + Reset(); + } + + const SourceResourceObject Input; + SourceResourceObject Output; + Execution::Context& ParentContext; + std::unique_ptr<Execution::Context> SubContext; + + // Reset the state that is modified by Get + void Reset() + { + Output = Input.CopyForOutput(); + + SubContext = ParentContext.CreateSubContext(); + SubContext->SetFlags(Execution::ContextFlag::DisableInteractivity); + + if (Input.AcceptAgreements().value_or(false)) + { + SubContext->Args.AddArg(Execution::Args::Type::AcceptSourceAgreements); + } + } + + // Fills the Output object with the current state + void Get() + { + auto currentSources = Repository::Source::GetCurrentSources(); + const std::string& name = Input.SourceName().value(); + + Output.Exist(false); + + for (auto const& source : currentSources) + { + if (Utility::ICUCaseInsensitiveEquals(source.Name, name)) + { + Output.Exist(true); + Output.Argument(source.Arg); + Output.Type(source.Type); + Output.TrustLevel(TrustLevelStringFromFlags(source.TrustLevel)); + Output.Explicit(source.Explicit); + + std::vector<Repository::SourceDetails> sources; + sources.emplace_back(source); + SubContext->Add<Execution::Data::SourceList>(std::move(sources)); + break; + } + } + + AICLI_LOG(CLI, Verbose, << "Source::Get found:\n" << Json::writeString(Json::StreamWriterBuilder{}, Output.ToJson())); + } + + void Add() + { + AICLI_LOG(CLI, Verbose, << "Source::Add invoked"); + + if (!SubContext->Args.Contains(Execution::Args::Type::SourceName)) + { + SubContext->Args.AddArg(Execution::Args::Type::SourceName, Input.SourceName().value()); + } + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS, !Input.Argument().has_value()); + SubContext->Args.AddArg(Execution::Args::Type::SourceArg, Input.Argument().value()); + + if (Input.Type()) + { + SubContext->Args.AddArg(Execution::Args::Type::SourceType, Input.Type().value()); + } + + ResourceTrustLevel effectiveTrustLevel = EffectiveTrustLevel(Input.TrustLevel()); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS, effectiveTrustLevel == ResourceTrustLevel::Invalid); + if (effectiveTrustLevel == ResourceTrustLevel::Trusted) + { + SubContext->Args.AddArg(Execution::Args::Type::SourceTrustLevel, TrustLevelStringFromFlags(SourceTrustLevel::Trusted)); + } + + if (Input.Explicit().value_or(false)) + { + SubContext->Args.AddArg(Execution::Args::Type::SourceExplicit); + } + + *SubContext << + Workflow::EnsureRunningAsAdmin << + Workflow::CreateSourceForSourceAdd << + Workflow::AddSource; + } + + void Remove() + { + AICLI_LOG(CLI, Verbose, << "Source::Remove invoked"); + + if (!SubContext->Args.Contains(Execution::Args::Type::SourceName)) + { + SubContext->Args.AddArg(Execution::Args::Type::SourceName, Input.SourceName().value()); + } + + *SubContext << + Workflow::EnsureRunningAsAdmin << + Workflow::RemoveSources; + } + + void Replace() + { + AICLI_LOG(CLI, Verbose, << "Source::Replace invoked"); + Remove(); + Add(); + } + + // Determines if the current Output values match the Input values state. + bool Test() + { + // Need to populate Output before calling + THROW_HR_IF(E_UNEXPECTED, !Output.Exist().has_value()); + + if (Input.ShouldExist()) + { + if (Output.Exist().value()) + { + AICLI_LOG(CLI, Verbose, << "Source::Test needed to inspect these properties: Argument(" << TestArgument() << "), Type(" << TestType() << "), TrustLevel(" << TestTrustLevel() << "), Explicit(" << TestExplicit() << ")"); + return TestArgument() && TestType() && TestTrustLevel() && TestExplicit(); + } + else + { + AICLI_LOG(CLI, Verbose, << "Source::Test was false because the source is not present"); + return false; + } + } + else + { + AICLI_LOG(CLI, Verbose, << "Source::Test desired the source to not exist, and it " << (Output.Exist().value() ? "did" : "did not")); + return !Output.Exist().value(); + } + } + + Json::Value DiffJson() + { + // Need to populate Output before calling + THROW_HR_IF(E_UNEXPECTED, !Output.Exist().has_value()); + + Json::Value result{ Json::ValueType::arrayValue }; + + if (Input.ShouldExist() != Output.Exist().value()) + { + result.append(std::string{ StandardExistProperty::Name() }); + } + else + { + if (!TestArgument()) + { + result.append(std::string{ ArgumentProperty::Name() }); + } + + if (!TestType()) + { + result.append(std::string{ TypeProperty::Name() }); + } + + if (!TestTrustLevel()) + { + result.append(std::string{ TrustLevelProperty::Name() }); + } + + if (!TestExplicit()) + { + result.append(std::string{ ExplicitProperty::Name() }); + } + } + + return result; + } + + bool TestArgument() + { + if (Input.Argument()) + { + if (Output.Argument()) + { + return Input.Argument().value() == Output.Argument().value(); + } + else + { + return false; + } + } + else + { + return true; + } + } + + bool TestType() + { + if (Input.Type()) + { + if (Output.Type()) + { + return Utility::CaseInsensitiveEquals(Input.Type().value(), Output.Type().value()); + } + else + { + return false; + } + } + else + { + return true; + } + } + + bool TestTrustLevel() + { + auto inputTrustLevel = EffectiveTrustLevel(Input.TrustLevel()); + + if (inputTrustLevel != ResourceTrustLevel::Undefined) + { + return inputTrustLevel == EffectiveTrustLevel(Output.TrustLevel()); + } + else + { + return true; + } + } + + bool TestExplicit() + { + if (Input.Explicit()) + { + if (Output.Explicit()) + { + return Input.Explicit().value() == Output.Explicit().value(); + } + else + { + return false; + } + } + else + { + return true; + } + } + }; + } + + DscSourceResource::DscSourceResource(std::string_view parent) : + DscCommandBase(parent, "source", DscResourceKind::Resource, + DscFunctions::Get | DscFunctions::Set | DscFunctions::Test | DscFunctions::Export | DscFunctions::Schema, + DscFunctionModifiers::ImplementsPretest | DscFunctionModifiers::HandlesExist | DscFunctionModifiers::ReturnsStateAndDiff) + { + } + + Resource::LocString DscSourceResource::ShortDescription() const + { + return Resource::String::DscSourceResourceShortDescription; + } + + Resource::LocString DscSourceResource::LongDescription() const + { + return Resource::String::DscSourceResourceLongDescription; + } + + std::string DscSourceResource::ResourceType() const + { + return "Source"; + } + + void DscSourceResource::ResourceFunctionGet(Execution::Context& context) const + { + if (auto json = GetJsonFromInput(context)) + { + SourceFunctionData data{ context, json }; + + data.Get(); + + WriteJsonOutputLine(context, data.Output.ToJson()); + } + } + + void DscSourceResource::ResourceFunctionSet(Execution::Context& context) const + { + if (auto json = GetJsonFromInput(context)) + { + SourceFunctionData data{ context, json }; + + data.Get(); + + // Capture the diff before updating the output + auto diff = data.DiffJson(); + + if (!data.Test()) + { + if (data.Input.ShouldExist()) + { + if (data.Output.Exist().value()) + { + AICLI_LOG(CLI, Info, << "Replacing source with new information"); + data.Replace(); + } + else + { + AICLI_LOG(CLI, Info, << "Adding source as it was not found"); + data.Add(); + } + } + else + { + AICLI_LOG(CLI, Info, << "Removing source as desired"); + data.Remove(); + } + + if (data.SubContext->IsTerminated()) + { + data.ParentContext.Terminate(data.SubContext->GetTerminationHR()); + return; + } + + data.Reset(); + data.Get(); + } + + WriteJsonOutputLine(context, data.Output.ToJson()); + WriteJsonOutputLine(context, diff); + } + } + + void DscSourceResource::ResourceFunctionTest(Execution::Context& context) const + { + if (auto json = GetJsonFromInput(context)) + { + SourceFunctionData data{ context, json }; + + data.Get(); + data.Output.InDesiredState(data.Test()); + + WriteJsonOutputLine(context, data.Output.ToJson()); + WriteJsonOutputLine(context, data.DiffJson()); + } + } + + void DscSourceResource::ResourceFunctionExport(Execution::Context& context) const + { + auto currentSources = Repository::Source::GetCurrentSources(); + + for (auto const& source : currentSources) + { + SourceResourceObject output; + output.SourceName(source.Name); + output.Argument(source.Arg); + output.Type(source.Type); + output.TrustLevel(TrustLevelStringFromFlags(source.TrustLevel)); + output.Explicit(source.Explicit); + WriteJsonOutputLine(context, output.ToJson()); + } + } + + void DscSourceResource::ResourceFunctionSchema(Execution::Context& context) const + { + WriteJsonOutputLine(context, SourceResourceObject::Schema(ResourceType())); + } +} diff --git a/src/AppInstallerCLICore/Commands/DscSourceResource.h b/src/AppInstallerCLICore/Commands/DscSourceResource.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "DscCommandBase.h" + +namespace AppInstaller::CLI +{ + // A resource for managing source configuration. + struct DscSourceResource : public DscCommandBase + { + DscSourceResource(std::string_view parent); + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + protected: + std::string ResourceType() const override; + + void ResourceFunctionGet(Execution::Context& context) const override; + void ResourceFunctionSet(Execution::Context& context) const override; + void ResourceFunctionTest(Execution::Context& context) const override; + void ResourceFunctionExport(Execution::Context& context) const override; + void ResourceFunctionSchema(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -228,6 +228,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionManifest); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionExist); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionInDesiredState); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionAcceptAgreements); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageId); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageSource); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageVersion); @@ -235,7 +236,13 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageMatchOption); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageUseLatest); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageInstallMode); - WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageAcceptAgreements); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionSourceName); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionSourceArgument); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionSourceType); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionSourceTrustLevel); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionSourceExplicit); + WINGET_DEFINE_RESOURCE_STRINGID(DscSourceResourceShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DscSourceResourceLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(EnableAdminSettingFailed); WINGET_DEFINE_RESOURCE_STRINGID(EnableWindowsFeaturesSuccess); WINGET_DEFINE_RESOURCE_STRINGID(EnablingWindowsFeature); diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h @@ -10,7 +10,7 @@ #include <wincodec.h> #pragma warning( push ) -#pragma warning ( disable : 4458 4100 6031 4702 ) +#pragma warning ( disable : 4458 4100 6031 4702 26439 ) #include <valijson/schema.hpp> #include <valijson/schema_parser.hpp> #include <valijson/validator.hpp> diff --git a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs @@ -8,9 +8,7 @@ namespace AppInstallerCLIE2ETests { using System; using System.IO; - using System.Linq; using AppInstallerCLIE2ETests.Helpers; - using Microsoft.Win32; using NUnit.Framework; /// <summary> @@ -25,14 +23,8 @@ namespace AppInstallerCLIE2ETests /// </summary> public static void EnsureTestResourcePresence() { - string outputDirectory = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\WindowsApps"); - Assert.IsNotEmpty(outputDirectory); - - var result = TestCommon.RunAICLICommand("dscv3 test-file", $"--manifest -o {outputDirectory}\\test-file.dsc.resource.json"); - Assert.AreEqual(0, result.ExitCode); - - result = TestCommon.RunAICLICommand("dscv3 test-json", $"--manifest -o {outputDirectory}\\test-json.dsc.resource.json"); - Assert.AreEqual(0, result.ExitCode); + DSCv3ResourceTestBase.EnsureTestResourcePresence("test-file"); + DSCv3ResourceTestBase.EnsureTestResourcePresence("test-json"); } /// <summary> diff --git a/src/AppInstallerCLIE2ETests/DSCv3PackageResourceCommand.cs b/src/AppInstallerCLIE2ETests/DSCv3PackageResourceCommand.cs @@ -0,0 +1,577 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DSCv3PackageResourceCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System.Collections.Generic; + using System.Text.Json.Serialization; + using AppInstallerCLIE2ETests.Helpers; + using NUnit.Framework; + + /// <summary> + /// `Configure` command tests. + /// </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1010:Opening square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1011:Closing square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] + public class DSCv3PackageResourceCommand : DSCv3ResourceTestBase + { + private const string DefaultPackageIdentifier = Constants.ExeInstallerPackageId; + private const string DefaultPackageLowVersion = "1.0.0.0"; + private const string DefaultPackageMidVersion = "1.1.0.0"; + private const string DefaultPackageHighVersion = "2.0.0.0"; + private const string PackageResource = "package"; + private const string VersionPropertyName = "version"; + private const string UseLatestPropertyName = "useLatest"; + + /// <summary> + /// Setup done once before all the tests here. + /// </summary> + [OneTimeSetUp] + public void OneTimeSetup() + { + TestCommon.SetupTestSource(); + WinGetSettingsHelper.ConfigureFeature("dsc3", true); + WinGetSettingsHelper.ConfigureLoggingLevel("verbose"); + EnsureTestResourcePresence(PackageResource); + } + + /// <summary> + /// Teardown done once after all the tests here. + /// </summary> + [OneTimeTearDown] + public void OneTimeTeardown() + { + RemoveTestPackage(); + WinGetSettingsHelper.ConfigureLoggingLevel(null); + WinGetSettingsHelper.ConfigureFeature("dsc3", false); + TestCommon.TearDownTestSource(); + } + + /// <summary> + /// Set up. + /// </summary> + [SetUp] + public void Setup() + { + // Try clean up TestExeInstaller for failure cases where cleanup is not successful + RemoveTestPackage(); + } + + /// <summary> + /// Calls `get` on the `package` resource with the value not present. + /// </summary> + [Test] + public void Package_Get_NotPresent() + { + PackageResourceData packageResourceData = new PackageResourceData() { Identifier = DefaultPackageIdentifier }; + + var result = RunDSCv3Command(PackageResource, GetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + PackageResourceData output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(packageResourceData.Identifier, output.Identifier); + } + + /// <summary> + /// Calls `get` on the `package` resource with the package not existing. + /// </summary> + [Test] + public void Package_Get_UnknownIdentifier() + { + PackageResourceData packageResourceData = new PackageResourceData() { Identifier = "Not.An.Existing.Identifier.123456789.ABCDEFG" }; + + var result = RunDSCv3Command(PackageResource, GetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + PackageResourceData output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(packageResourceData.Identifier, output.Identifier); + } + + /// <summary> + /// Calls `get` on the `package` resource with the value present. + /// </summary> + [Test] + public void Package_Get_Present() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() { Identifier = DefaultPackageIdentifier }; + + var result = RunDSCv3Command(PackageResource, GetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + PackageResourceData output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageLowVersion); + } + + /// <summary> + /// Calls `get` on the `package` resource with the value present and supplying most inputs. + /// </summary> + [Test] + public void Package_Get_MuchInput() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + Source = Constants.TestSourceName, + MatchOption = "equals", + }; + + var result = RunDSCv3Command(PackageResource, GetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + PackageResourceData output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageLowVersion); + } + + /// <summary> + /// Calls `test` on the `package` resource with the value not present. + /// </summary> + [Test] + public void Package_Test_NotPresent() + { + PackageResourceData packageResourceData = new PackageResourceData() { Identifier = DefaultPackageIdentifier }; + + var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(packageResourceData.Identifier, output.Identifier); + Assert.False(output.InDesiredState); + + AssertDiffState(diff, [ ExistPropertyName ]); + } + + /// <summary> + /// Calls `test` on the `package` resource with the value present. + /// </summary> + [Test] + public void Package_Test_SimplePresent() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() { Identifier = DefaultPackageIdentifier }; + + var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageLowVersion); + Assert.True(output.InDesiredState); + + AssertDiffState(diff, []); + } + + /// <summary> + /// Calls `test` on the `package` resource with a version that matches. + /// </summary> + [Test] + public void Package_Test_VersionMatch() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + Version = DefaultPackageLowVersion, + }; + + var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageLowVersion); + Assert.True(output.InDesiredState); + + AssertDiffState(diff, []); + } + + /// <summary> + /// Calls `test` on the `package` resource with a version that does not match. + /// </summary> + [Test] + public void Package_Test_VersionMismatch() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + Version = DefaultPackageMidVersion, + }; + + var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageLowVersion); + Assert.False(output.InDesiredState); + + AssertDiffState(diff, [ VersionPropertyName ]); + } + + /// <summary> + /// Calls `test` on the `package` resource with a version that is the latest. + /// </summary> + [Test] + public void Package_Test_Latest() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + UseLatest = true, + }; + + var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageHighVersion); + Assert.True(output.InDesiredState); + + AssertDiffState(diff, []); + } + + /// <summary> + /// Calls `test` on the `package` resource with a version that is not the latest. + /// </summary> + [Test] + public void Package_Test_NotLatest() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageMidVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + UseLatest = true, + }; + + var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageMidVersion); + Assert.False(output.InDesiredState); + + AssertDiffState(diff, [ UseLatestPropertyName ]); + } + + /// <summary> + /// Calls `set` on the `package` resource when the package is not present, and again afterward. + /// </summary> + [Test] + public void Package_Set_SimpleRepeated() + { + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + }; + + var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageHighVersion, ignoreLatest: true); + + AssertDiffState(diff, [ ExistPropertyName ]); + + // Set again should be a no-op + result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (output, diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageHighVersion); + + AssertDiffState(diff, []); + } + + /// <summary> + /// Calls `set` on the `package` resource to ensure that it is not present. + /// </summary> + [Test] + public void Package_Set_Remove() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + Exist = false, + }; + + var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(packageResourceData.Identifier, output.Identifier); + + AssertDiffState(diff, [ ExistPropertyName ]); + + // Call `get` to ensure the result + PackageResourceData packageResourceDataForGet = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + }; + + result = RunDSCv3Command(PackageResource, GetFunction, packageResourceDataForGet); + AssertSuccessfulResourceRun(ref result); + + output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(packageResourceDataForGet.Identifier, output.Identifier); + } + + /// <summary> + /// Calls `set` on the `package` resource to request the latest version when a lower version is installed. + /// </summary> + [Test] + public void Package_Set_Latest() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageMidVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + UseLatest = true, + }; + + var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageHighVersion, ignoreLatest: true); + + AssertDiffState(diff, [ UseLatestPropertyName ]); + + // Call `get` to ensure the result + PackageResourceData packageResourceDataForGet = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + }; + + result = RunDSCv3Command(PackageResource, GetFunction, packageResourceDataForGet); + AssertSuccessfulResourceRun(ref result); + + output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageHighVersion); + } + + /// <summary> + /// Calls `set` on the `package` resource to request a specific version when a lower version is installed. + /// </summary> + [Test] + public void Package_Set_SpecificVersionUpgrade() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + Version = DefaultPackageMidVersion, + }; + + var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageMidVersion, ignoreLatest: true); + + AssertDiffState(diff, [ VersionPropertyName ]); + + // Call `get` to ensure the result + PackageResourceData packageResourceDataForGet = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + }; + + result = RunDSCv3Command(PackageResource, GetFunction, packageResourceDataForGet); + AssertSuccessfulResourceRun(ref result); + + output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageMidVersion); + } + + /// <summary> + /// Calls `set` on the `package` resource to request a specific version when a higher version is installed. + /// </summary> + [Test] + public void Package_Set_SpecificVersionDowngrade() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + Version = DefaultPackageMidVersion, + }; + + var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + + (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageMidVersion, ignoreLatest: true); + + AssertDiffState(diff, [ VersionPropertyName ]); + + // Call `get` to ensure the result + PackageResourceData packageResourceDataForGet = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + }; + + result = RunDSCv3Command(PackageResource, GetFunction, packageResourceDataForGet); + AssertSuccessfulResourceRun(ref result); + + output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); + AssertExistingPackageResourceData(output, DefaultPackageMidVersion); + } + + /// <summary> + /// Calls `export` on the `package` resource without providing any input. + /// </summary> + [Test] + public void Package_Export_NoInput() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + var result = RunDSCv3Command(PackageResource, ExportFunction, " "); + AssertSuccessfulResourceRun(ref result); + + List<PackageResourceData> output = GetOutputLinesAs<PackageResourceData>(result.StdOut); + + bool foundDefaultPackage = false; + foreach (PackageResourceData item in output) + { + if (item.Identifier == DefaultPackageIdentifier) + { + foundDefaultPackage = true; + Assert.IsNull(item.Version); + break; + } + } + + Assert.IsTrue(foundDefaultPackage); + } + + /// <summary> + /// Calls `export` on the `package` resource providing input to request that versions be included. + /// </summary> + [Test] + public void Package_Export_RequestVersions() + { + var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); + Assert.AreEqual(0, setupInstall.ExitCode); + + PackageResourceData packageResourceData = new PackageResourceData() + { + UseLatest = false, + }; + + var result = RunDSCv3Command(PackageResource, ExportFunction, packageResourceData, 300000); + AssertSuccessfulResourceRun(ref result); + + List<PackageResourceData> output = GetOutputLinesAs<PackageResourceData>(result.StdOut); + + bool foundDefaultPackage = false; + foreach (PackageResourceData item in output) + { + if (item.Identifier == DefaultPackageIdentifier) + { + foundDefaultPackage = true; + Assert.AreEqual(DefaultPackageLowVersion, item.Version); + } + else + { + Assert.IsNotNull(item.Version); + Assert.IsNotEmpty(item.Version); + } + } + + Assert.IsTrue(foundDefaultPackage); + } + + private static void RemoveTestPackage() + { + PackageResourceData packageResourceData = new PackageResourceData() + { + Identifier = DefaultPackageIdentifier, + Exist = false, + }; + + var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); + AssertSuccessfulResourceRun(ref result); + } + + private static void AssertExistingPackageResourceData(PackageResourceData output, string version, bool ignoreLatest = false) + { + Assert.IsNotNull(output); + Assert.True(output.Exist); + Assert.AreEqual(DefaultPackageIdentifier, output.Identifier); + Assert.AreEqual(version, output.Version); + + if (!ignoreLatest) + { + if (version == DefaultPackageHighVersion) + { + Assert.True(output.UseLatest); + } + else + { + Assert.False(output.UseLatest); + } + } + } + + private class PackageResourceData + { + [JsonPropertyName(ExistPropertyName)] + public bool? Exist { get; set; } + + [JsonPropertyName(InDesiredStatePropertyName)] + public bool? InDesiredState { get; set; } + + [JsonPropertyName("id")] + public string Identifier { get; set; } + + public string Source { get; set; } + + public string Version { get; set; } + + public string MatchOption { get; set; } + + public bool? UseLatest { get; set; } + + public string InstallMode { get; set; } + + public bool? AcceptAgreements { get; set; } + } + } +} diff --git a/src/AppInstallerCLIE2ETests/DSCv3ResourceCommands.cs b/src/AppInstallerCLIE2ETests/DSCv3ResourceCommands.cs @@ -1,677 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="DSCv3ResourceCommands.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace AppInstallerCLIE2ETests -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Text.Json; - using System.Text.Json.Serialization; - using AppInstallerCLIE2ETests.Helpers; - using NUnit.Framework; - - /// <summary> - /// `Configure` command tests. - /// </summary> - [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1010:Opening square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1011:Closing square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] - public class DSCv3ResourceCommands - { - private const string DefaultPackageIdentifier = Constants.ExeInstallerPackageId; - private const string DefaultPackageLowVersion = "1.0.0.0"; - private const string DefaultPackageMidVersion = "1.1.0.0"; - private const string DefaultPackageHighVersion = "2.0.0.0"; - private const string PackageResource = "package"; - private const string GetFunction = "get"; - private const string TestFunction = "test"; - private const string SetFunction = "set"; - private const string ExportFunction = "export"; - private const string ExistPropertyName = "_exist"; - private const string VersionPropertyName = "version"; - private const string UseLatestPropertyName = "useLatest"; - - /// <summary> - /// Ensures that the test resources manifests are present. - /// </summary> - public static void EnsureTestResourcePresence() - { - string outputDirectory = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\WindowsApps"); - Assert.IsNotEmpty(outputDirectory); - - var result = TestCommon.RunAICLICommand("dscv3 package", $"--manifest -o {outputDirectory}\\microsoft.winget.package.dsc.resource.json"); - Assert.AreEqual(0, result.ExitCode); - } - - /// <summary> - /// Setup done once before all the tests here. - /// </summary> - [OneTimeSetUp] - public void OneTimeSetup() - { - TestCommon.SetupTestSource(); - WinGetSettingsHelper.ConfigureFeature("dsc3", true); - WinGetSettingsHelper.ConfigureLoggingLevel("verbose"); - EnsureTestResourcePresence(); - } - - /// <summary> - /// Teardown done once after all the tests here. - /// </summary> - [OneTimeTearDown] - public void OneTimeTeardown() - { - RemoveTestPackage(); - WinGetSettingsHelper.ConfigureLoggingLevel(null); - WinGetSettingsHelper.ConfigureFeature("dsc3", false); - TestCommon.TearDownTestSource(); - } - - /// <summary> - /// Set up. - /// </summary> - [SetUp] - public void Setup() - { - // Try clean up TestExeInstaller for failure cases where cleanup is not successful - RemoveTestPackage(); - } - - /// <summary> - /// Calls `get` on the `package` resource with the value not present. - /// </summary> - [Test] - public void Package_Get_NotPresent() - { - PackageResourceData packageResourceData = new PackageResourceData() { Identifier = DefaultPackageIdentifier }; - - var result = RunDSCv3Command(PackageResource, GetFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - PackageResourceData output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); - Assert.IsNotNull(output); - Assert.False(output.Exist); - Assert.AreEqual(packageResourceData.Identifier, output.Identifier); - } - - /// <summary> - /// Calls `get` on the `package` resource with the package not existing. - /// </summary> - [Test] - public void Package_Get_UnknownIdentifier() - { - PackageResourceData packageResourceData = new PackageResourceData() { Identifier = "Not.An.Existing.Identifier.123456789.ABCDEFG" }; - - var result = RunDSCv3Command(PackageResource, GetFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - PackageResourceData output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); - Assert.IsNotNull(output); - Assert.False(output.Exist); - Assert.AreEqual(packageResourceData.Identifier, output.Identifier); - } - - /// <summary> - /// Calls `get` on the `package` resource with the value present. - /// </summary> - [Test] - public void Package_Get_Present() - { - var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); - Assert.AreEqual(0, setupInstall.ExitCode); - - PackageResourceData packageResourceData = new PackageResourceData() { Identifier = DefaultPackageIdentifier }; - - var result = RunDSCv3Command(PackageResource, GetFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - PackageResourceData output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); - AssertExistingPackageResourceData(output, DefaultPackageLowVersion); - } - - /// <summary> - /// Calls `get` on the `package` resource with the value present and supplying most inputs. - /// </summary> - [Test] - public void Package_Get_MuchInput() - { - var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); - Assert.AreEqual(0, setupInstall.ExitCode); - - PackageResourceData packageResourceData = new PackageResourceData() - { - Identifier = DefaultPackageIdentifier, - Source = Constants.TestSourceName, - MatchOption = "equals", - }; - - var result = RunDSCv3Command(PackageResource, GetFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - PackageResourceData output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); - AssertExistingPackageResourceData(output, DefaultPackageLowVersion); - } - - /// <summary> - /// Calls `test` on the `package` resource with the value not present. - /// </summary> - [Test] - public void Package_Test_NotPresent() - { - PackageResourceData packageResourceData = new PackageResourceData() { Identifier = DefaultPackageIdentifier }; - - var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); - Assert.IsNotNull(output); - Assert.False(output.Exist); - Assert.AreEqual(packageResourceData.Identifier, output.Identifier); - Assert.False(output.InDesiredState); - - Assert.IsNotNull(diff); - Assert.AreEqual(1, diff.Count); - Assert.AreEqual(ExistPropertyName, diff[0]); - } - - /// <summary> - /// Calls `test` on the `package` resource with the value present. - /// </summary> - [Test] - public void Package_Test_SimplePresent() - { - var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); - Assert.AreEqual(0, setupInstall.ExitCode); - - PackageResourceData packageResourceData = new PackageResourceData() { Identifier = DefaultPackageIdentifier }; - - var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); - AssertExistingPackageResourceData(output, DefaultPackageLowVersion); - Assert.True(output.InDesiredState); - - AssertDiffState(diff, []); - } - - /// <summary> - /// Calls `test` on the `package` resource with a version that matches. - /// </summary> - [Test] - public void Package_Test_VersionMatch() - { - var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); - Assert.AreEqual(0, setupInstall.ExitCode); - - PackageResourceData packageResourceData = new PackageResourceData() - { - Identifier = DefaultPackageIdentifier, - Version = DefaultPackageLowVersion, - }; - - var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); - AssertExistingPackageResourceData(output, DefaultPackageLowVersion); - Assert.True(output.InDesiredState); - - AssertDiffState(diff, []); - } - - /// <summary> - /// Calls `test` on the `package` resource with a version that does not match. - /// </summary> - [Test] - public void Package_Test_VersionMismatch() - { - var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); - Assert.AreEqual(0, setupInstall.ExitCode); - - PackageResourceData packageResourceData = new PackageResourceData() - { - Identifier = DefaultPackageIdentifier, - Version = DefaultPackageMidVersion, - }; - - var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); - AssertExistingPackageResourceData(output, DefaultPackageLowVersion); - Assert.False(output.InDesiredState); - - AssertDiffState(diff, [ VersionPropertyName ]); - } - - /// <summary> - /// Calls `test` on the `package` resource with a version that is the latest. - /// </summary> - [Test] - public void Package_Test_Latest() - { - var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier}"); - Assert.AreEqual(0, setupInstall.ExitCode); - - PackageResourceData packageResourceData = new PackageResourceData() - { - Identifier = DefaultPackageIdentifier, - UseLatest = true, - }; - - var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); - AssertExistingPackageResourceData(output, DefaultPackageHighVersion); - Assert.True(output.InDesiredState); - - AssertDiffState(diff, []); - } - - /// <summary> - /// Calls `test` on the `package` resource with a version that is not the latest. - /// </summary> - [Test] - public void Package_Test_NotLatest() - { - var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageMidVersion}"); - Assert.AreEqual(0, setupInstall.ExitCode); - - PackageResourceData packageResourceData = new PackageResourceData() - { - Identifier = DefaultPackageIdentifier, - UseLatest = true, - }; - - var result = RunDSCv3Command(PackageResource, TestFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); - AssertExistingPackageResourceData(output, DefaultPackageMidVersion); - Assert.False(output.InDesiredState); - - AssertDiffState(diff, [ UseLatestPropertyName ]); - } - - /// <summary> - /// Calls `set` on the `package` resource when the package is not present, and again afterward. - /// </summary> - [Test] - public void Package_Set_SimpleRepeated() - { - PackageResourceData packageResourceData = new PackageResourceData() - { - Identifier = DefaultPackageIdentifier, - }; - - var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); - AssertExistingPackageResourceData(output, DefaultPackageHighVersion, ignoreLatest: true); - - AssertDiffState(diff, [ ExistPropertyName ]); - - // Set again should be a no-op - result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - (output, diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); - AssertExistingPackageResourceData(output, DefaultPackageHighVersion); - - AssertDiffState(diff, []); - } - - /// <summary> - /// Calls `set` on the `package` resource to ensure that it is not present. - /// </summary> - [Test] - public void Package_Set_Remove() - { - var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier}"); - Assert.AreEqual(0, setupInstall.ExitCode); - - PackageResourceData packageResourceData = new PackageResourceData() - { - Identifier = DefaultPackageIdentifier, - Exist = false, - }; - - var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); - Assert.IsNotNull(output); - Assert.False(output.Exist); - Assert.AreEqual(packageResourceData.Identifier, output.Identifier); - - AssertDiffState(diff, [ ExistPropertyName ]); - - // Call `get` to ensure the result - PackageResourceData packageResourceDataForGet = new PackageResourceData() - { - Identifier = DefaultPackageIdentifier, - }; - - result = RunDSCv3Command(PackageResource, GetFunction, packageResourceDataForGet); - AssertSuccessfulResourceRun(ref result); - - output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); - Assert.IsNotNull(output); - Assert.False(output.Exist); - Assert.AreEqual(packageResourceDataForGet.Identifier, output.Identifier); - } - - /// <summary> - /// Calls `set` on the `package` resource to request the latest version when a lower version is installed. - /// </summary> - [Test] - public void Package_Set_Latest() - { - var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageMidVersion}"); - Assert.AreEqual(0, setupInstall.ExitCode); - - PackageResourceData packageResourceData = new PackageResourceData() - { - Identifier = DefaultPackageIdentifier, - UseLatest = true, - }; - - var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); - AssertExistingPackageResourceData(output, DefaultPackageHighVersion, ignoreLatest: true); - - AssertDiffState(diff, [ UseLatestPropertyName ]); - - // Call `get` to ensure the result - PackageResourceData packageResourceDataForGet = new PackageResourceData() - { - Identifier = DefaultPackageIdentifier, - }; - - result = RunDSCv3Command(PackageResource, GetFunction, packageResourceDataForGet); - AssertSuccessfulResourceRun(ref result); - - output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); - AssertExistingPackageResourceData(output, DefaultPackageHighVersion); - } - - /// <summary> - /// Calls `set` on the `package` resource to request a specific version when a lower version is installed. - /// </summary> - [Test] - public void Package_Set_SpecificVersionUpgrade() - { - var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); - Assert.AreEqual(0, setupInstall.ExitCode); - - PackageResourceData packageResourceData = new PackageResourceData() - { - Identifier = DefaultPackageIdentifier, - Version = DefaultPackageMidVersion, - }; - - var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); - AssertExistingPackageResourceData(output, DefaultPackageMidVersion, ignoreLatest: true); - - AssertDiffState(diff, [ VersionPropertyName ]); - - // Call `get` to ensure the result - PackageResourceData packageResourceDataForGet = new PackageResourceData() - { - Identifier = DefaultPackageIdentifier, - }; - - result = RunDSCv3Command(PackageResource, GetFunction, packageResourceDataForGet); - AssertSuccessfulResourceRun(ref result); - - output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); - AssertExistingPackageResourceData(output, DefaultPackageMidVersion); - } - - /// <summary> - /// Calls `set` on the `package` resource to request a specific version when a higher version is installed. - /// </summary> - [Test] - public void Package_Set_SpecificVersionDowngrade() - { - var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier}"); - Assert.AreEqual(0, setupInstall.ExitCode); - - PackageResourceData packageResourceData = new PackageResourceData() - { - Identifier = DefaultPackageIdentifier, - Version = DefaultPackageMidVersion, - }; - - var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - - (PackageResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<PackageResourceData>(result.StdOut); - AssertExistingPackageResourceData(output, DefaultPackageMidVersion, ignoreLatest: true); - - AssertDiffState(diff, [ VersionPropertyName ]); - - // Call `get` to ensure the result - PackageResourceData packageResourceDataForGet = new PackageResourceData() - { - Identifier = DefaultPackageIdentifier, - }; - - result = RunDSCv3Command(PackageResource, GetFunction, packageResourceDataForGet); - AssertSuccessfulResourceRun(ref result); - - output = GetSingleOutputLineAs<PackageResourceData>(result.StdOut); - AssertExistingPackageResourceData(output, DefaultPackageMidVersion); - } - - /// <summary> - /// Calls `export` on the `package` resource without providing any input. - /// </summary> - [Test] - public void Package_Export_NoInput() - { - var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier}"); - Assert.AreEqual(0, setupInstall.ExitCode); - - var result = RunDSCv3Command(PackageResource, ExportFunction, " "); - AssertSuccessfulResourceRun(ref result); - - List<PackageResourceData> output = GetOutputLinesAs<PackageResourceData>(result.StdOut); - - bool foundDefaultPackage = false; - foreach (PackageResourceData item in output) - { - if (item.Identifier == DefaultPackageIdentifier) - { - foundDefaultPackage = true; - Assert.IsNull(item.Version); - break; - } - } - - Assert.IsTrue(foundDefaultPackage); - } - - /// <summary> - /// Calls `export` on the `package` resource providing input to request that versions be included. - /// </summary> - [Test] - public void Package_Export_RequestVersions() - { - var setupInstall = TestCommon.RunAICLICommand("install", $"--id {DefaultPackageIdentifier} --version {DefaultPackageLowVersion}"); - Assert.AreEqual(0, setupInstall.ExitCode); - - PackageResourceData packageResourceData = new PackageResourceData() - { - UseLatest = false, - }; - - var result = RunDSCv3Command(PackageResource, ExportFunction, packageResourceData, 300000); - AssertSuccessfulResourceRun(ref result); - - List<PackageResourceData> output = GetOutputLinesAs<PackageResourceData>(result.StdOut); - - bool foundDefaultPackage = false; - foreach (PackageResourceData item in output) - { - if (item.Identifier == DefaultPackageIdentifier) - { - foundDefaultPackage = true; - Assert.AreEqual(DefaultPackageLowVersion, item.Version); - } - else - { - Assert.IsNotNull(item.Version); - Assert.IsNotEmpty(item.Version); - } - } - - Assert.IsTrue(foundDefaultPackage); - } - - private static void RemoveTestPackage() - { - PackageResourceData packageResourceData = new PackageResourceData() - { - Identifier = DefaultPackageIdentifier, - Exist = false, - }; - - var result = RunDSCv3Command(PackageResource, SetFunction, packageResourceData); - AssertSuccessfulResourceRun(ref result); - } - - private static TestCommon.RunCommandResult RunDSCv3Command(string resource, string function, object input, int timeOut = 60000, bool throwOnTimeout = true) - { - return TestCommon.RunAICLICommand($"dscv3 {resource}", $"--{function}", ConvertToJSON(input), timeOut, throwOnTimeout); - } - - private static void AssertSuccessfulResourceRun(ref TestCommon.RunCommandResult result) - { - Assert.AreEqual(0, result.ExitCode); - Assert.IsNotEmpty(result.StdOut); - } - - private static JsonSerializerOptions GetDefaultJsonOptions() - { - return new JsonSerializerOptions() - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Converters = - { - new JsonStringEnumConverter(), - }, - }; - } - - private static string ConvertToJSON(object value) => value switch - { - string s => s, - null => null, - _ => JsonSerializer.Serialize(value, GetDefaultJsonOptions()), - }; - - private static string[] GetOutputLines(string output) - { - return output.TrimEnd().Split(Environment.NewLine); - } - - private static T GetSingleOutputLineAs<T>(string output) - { - string[] lines = GetOutputLines(output); - Assert.AreEqual(1, lines.Length); - - return JsonSerializer.Deserialize<T>(lines[0], GetDefaultJsonOptions()); - } - - private static (T, List<string>) GetSingleOutputLineAndDiffAs<T>(string output) - { - string[] lines = GetOutputLines(output); - Assert.AreEqual(2, lines.Length); - - var options = GetDefaultJsonOptions(); - return (JsonSerializer.Deserialize<T>(lines[0], options), JsonSerializer.Deserialize<List<string>>(lines[1], options)); - } - - private static List<T> GetOutputLinesAs<T>(string output) - { - List<T> result = new List<T>(); - string[] lines = GetOutputLines(output); - var options = GetDefaultJsonOptions(); - - foreach (string line in lines) - { - result.Add(JsonSerializer.Deserialize<T>(line, options)); - } - - return result; - } - - private static void AssertExistingPackageResourceData(PackageResourceData output, string version, bool ignoreLatest = false) - { - Assert.IsNotNull(output); - Assert.True(output.Exist); - Assert.AreEqual(DefaultPackageIdentifier, output.Identifier); - Assert.AreEqual(version, output.Version); - - if (!ignoreLatest) - { - if (version == DefaultPackageHighVersion) - { - Assert.True(output.UseLatest); - } - else - { - Assert.False(output.UseLatest); - } - } - } - - private static void AssertDiffState(List<string> diff, IList<string> expected) - { - Assert.IsNotNull(diff); - Assert.AreEqual(expected.Count, diff.Count); - - foreach (string item in expected) - { - Assert.Contains(item, diff); - } - } - - private class PackageResourceData - { - [JsonPropertyName(ExistPropertyName)] - public bool? Exist { get; set; } - - [JsonPropertyName("_inDesiredState")] - public bool? InDesiredState { get; set; } - - [JsonPropertyName("id")] - public string Identifier { get; set; } - - public string Source { get; set; } - - public string Version { get; set; } - - public string MatchOption { get; set; } - - public bool? UseLatest { get; set; } - - public string InstallMode { get; set; } - - public bool? AcceptAgreements { get; set; } - } - } -} diff --git a/src/AppInstallerCLIE2ETests/DSCv3ResourceTestBase.cs b/src/AppInstallerCLIE2ETests/DSCv3ResourceTestBase.cs @@ -0,0 +1,184 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DSCv3ResourceTestBase.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Text.Json; + using System.Text.Json.Serialization; + using AppInstallerCLIE2ETests.Helpers; + using NUnit.Framework; + + /// <summary> + /// Provides common functionality for DSC v3 resource tests. + /// </summary> + public class DSCv3ResourceTestBase + { + /// <summary> + /// The string for the `get` function. + /// </summary> + public const string GetFunction = "get"; + + /// <summary> + /// The string for the `test` function. + /// </summary> + public const string TestFunction = "test"; + + /// <summary> + /// The string for the `set` function. + /// </summary> + public const string SetFunction = "set"; + + /// <summary> + /// The string for the `export` function. + /// </summary> + public const string ExportFunction = "export"; + + /// <summary> + /// The string for the `_exist` property name. + /// </summary> + public const string ExistPropertyName = "_exist"; + + /// <summary> + /// The string for the `_inDesiredState` property name. + /// </summary> + public const string InDesiredStatePropertyName = "_inDesiredState"; + + /// <summary> + /// Write the resource manifest out to the WindowsApps alias directory. + /// </summary> + /// <param name="resource">The resource manifest to write.</param> + public static void EnsureTestResourcePresence(string resource) + { + string outputDirectory = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\WindowsApps"); + Assert.IsNotEmpty(outputDirectory); + + var result = TestCommon.RunAICLICommand($"dscv3 {resource}", $"--manifest -o {outputDirectory}\\microsoft.winget.{resource}.dsc.resource.json"); + Assert.AreEqual(0, result.ExitCode); + } + + /// <summary> + /// Runs a DSC v3 resource command. + /// </summary> + /// <param name="resource">The resource to target.</param> + /// <param name="function">The resource function to run.</param> + /// <param name="input">Input for the function; supports null, direct string, or JSON serialization of complex objects.</param> + /// <param name="timeOut">The maximum time to wait in milliseconds.</param> + /// <param name="throwOnTimeout">Whether to throw on a timeout or simply return the incomplete result.</param> + /// <returns>A RunCommandResult containing the process exit code and output and error streams.</returns> + protected static TestCommon.RunCommandResult RunDSCv3Command(string resource, string function, object input, int timeOut = 60000, bool throwOnTimeout = true) + { + return TestCommon.RunAICLICommand($"dscv3 {resource}", $"--{function}", ConvertToJSON(input), timeOut, throwOnTimeout); + } + + /// <summary> + /// Asserts that a RunCommandResult contains a success for a DSC v3 resource command run. + /// </summary> + /// <param name="result">The result of a DSC v3 resource command run.</param> + protected static void AssertSuccessfulResourceRun(ref TestCommon.RunCommandResult result) + { + Assert.AreEqual(0, result.ExitCode); + Assert.IsNotEmpty(result.StdOut); + } + + /// <summary> + /// Gets the output as lines. + /// </summary> + /// <param name="output">The output stream from a DSC v3 resource command.</param> + /// <returns>The lines of the output.</returns> + protected static string[] GetOutputLines(string output) + { + return output.TrimEnd().Split(Environment.NewLine); + } + + /// <summary> + /// Asserts that the output is a single line and deserializes that line as JSON. + /// </summary> + /// <typeparam name="T">The type to deserialize from JSON.</typeparam> + /// <param name="output">The output stream from a DSC v3 resource command.</param> + /// <returns>The object as deserialized.</returns> + protected static T GetSingleOutputLineAs<T>(string output) + { + string[] lines = GetOutputLines(output); + Assert.AreEqual(1, lines.Length); + + return JsonSerializer.Deserialize<T>(lines[0], GetDefaultJsonOptions()); + } + + /// <summary> + /// Asserts that the output is two lines and deserializes them as a JSON object and JSON string array. + /// </summary> + /// <typeparam name="T">The type to deserialize from JSON.</typeparam> + /// <param name="output">The output stream from a DSC v3 resource command.</param> + /// <returns>The object as deserialized and the contents of the string array.</returns> + protected static (T, List<string>) GetSingleOutputLineAndDiffAs<T>(string output) + { + string[] lines = GetOutputLines(output); + Assert.AreEqual(2, lines.Length); + + var options = GetDefaultJsonOptions(); + return (JsonSerializer.Deserialize<T>(lines[0], options), JsonSerializer.Deserialize<List<string>>(lines[1], options)); + } + + /// <summary> + /// Deserializes all lines as JSON objects. + /// </summary> + /// <typeparam name="T">The type to deserialize from JSON.</typeparam> + /// <param name="output">The output stream from a DSC v3 resource command.</param> + /// <returns>A List of objects as deserialized.</returns> + protected static List<T> GetOutputLinesAs<T>(string output) + { + List<T> result = new List<T>(); + string[] lines = GetOutputLines(output); + var options = GetDefaultJsonOptions(); + + foreach (string line in lines) + { + result.Add(JsonSerializer.Deserialize<T>(line, options)); + } + + return result; + } + + /// <summary> + /// Requires that the diff from a resource command contain the same set of strings as expected. + /// </summary> + /// <param name="diff">The diff from a resource command.</param> + /// <param name="expected">The expected strings.</param> + protected static void AssertDiffState(List<string> diff, IList<string> expected) + { + Assert.IsNotNull(diff); + Assert.AreEqual(expected.Count, diff.Count); + + foreach (string item in expected) + { + Assert.Contains(item, diff); + } + } + + private static JsonSerializerOptions GetDefaultJsonOptions() + { + return new JsonSerializerOptions() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = + { + new JsonStringEnumConverter(), + }, + }; + } + + private static string ConvertToJSON(object value) => value switch + { + string s => s, + null => null, + _ => JsonSerializer.Serialize(value, GetDefaultJsonOptions()), + }; + } +} diff --git a/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs b/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs @@ -0,0 +1,485 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DSCv3SourceResourceCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System.Collections.Generic; + using System.Text.Json.Serialization; + using AppInstallerCLIE2ETests.Helpers; + using NUnit.Framework; + using WinRT; + + /// <summary> + /// `Configure` command tests. + /// </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1010:Opening square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1011:Closing square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] + public class DSCv3SourceResourceCommand : DSCv3ResourceTestBase + { + private const string DefaultSourceName = "SourceResourceTestSource"; + private const string DefaultSourceType = "Microsoft.Test.Configurable"; + private const string DefaultTrustLevel = "none"; + private const string TrustedTrustLevel = "trusted"; + private const bool DefaultExplicitState = false; + private const string SourceResource = "source"; + private const string ArgumentPropertyName = "argument"; + private const string TypePropertyName = "type"; + private const string TrustLevelPropertyName = "trustLevel"; + private const string ExplicitPropertyName = "explicit"; + + private static string DefaultSourceArgForCmdLine + { + get { return CreateSourceArgument(true); } + } + + private static string NonDefaultSourceArgForCmdLine + { + get { return CreateSourceArgument(true, 1, 1); } + } + + private static string DefaultSourceArgDirect + { + get { return CreateSourceArgument(false); } + } + + private static string NonDefaultSourceArgDirect + { + get { return CreateSourceArgument(false, 1, 1); } + } + + /// <summary> + /// Setup done once before all the tests here. + /// </summary> + [OneTimeSetUp] + public void OneTimeSetup() + { + TestCommon.SetupTestSource(); + WinGetSettingsHelper.ConfigureFeature("dsc3", true); + EnsureTestResourcePresence(SourceResource); + } + + /// <summary> + /// Teardown done once after all the tests here. + /// </summary> + [OneTimeTearDown] + public void OneTimeTeardown() + { + RemoveTestSource(); + WinGetSettingsHelper.ConfigureFeature("dsc3", false); + TestCommon.TearDownTestSource(); + } + + /// <summary> + /// Set up. + /// </summary> + [SetUp] + public void Setup() + { + RemoveTestSource(); + } + + /// <summary> + /// Calls `get` on the `source` resource with the value not present. + /// </summary> + [Test] + public void Source_Get_NotPresent() + { + SourceResourceData resourceData = new SourceResourceData() { Name = DefaultSourceName }; + + var result = RunDSCv3Command(SourceResource, GetFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + SourceResourceData output = GetSingleOutputLineAs<SourceResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(resourceData.Name, output.Name); + } + + /// <summary> + /// Calls `get` on the `source` resource with the value present. + /// </summary> + [Test] + public void Source_Get_Present() + { + var setup = TestCommon.RunAICLICommand("source add", $"--name {DefaultSourceName} --arg {DefaultSourceArgForCmdLine} --type {DefaultSourceType} --explicit"); + Assert.AreEqual(0, setup.ExitCode); + + SourceResourceData resourceData = new SourceResourceData() { Name = DefaultSourceName }; + + var result = RunDSCv3Command(SourceResource, GetFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + SourceResourceData output = GetSingleOutputLineAs<SourceResourceData>(result.StdOut); + AssertExistingSourceResourceData(output, DefaultSourceArgDirect, DefaultTrustLevel, true); + } + + /// <summary> + /// Calls `test` on the `source` resource with the value not present. + /// </summary> + [Test] + public void Source_Test_NotPresent() + { + SourceResourceData resourceData = new SourceResourceData() { Name = DefaultSourceName }; + + var result = RunDSCv3Command(SourceResource, TestFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + (SourceResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<SourceResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(resourceData.Name, output.Name); + Assert.False(output.InDesiredState); + + AssertDiffState(diff, [ ExistPropertyName ]); + } + + /// <summary> + /// Calls `test` on the `source` resource with the value present. + /// </summary> + [Test] + public void Source_Test_SimplePresent() + { + var setup = TestCommon.RunAICLICommand("source add", $"--name {DefaultSourceName} --arg {DefaultSourceArgForCmdLine} --type {DefaultSourceType}"); + Assert.AreEqual(0, setup.ExitCode); + + SourceResourceData resourceData = new SourceResourceData() { Name = DefaultSourceName }; + + var result = RunDSCv3Command(SourceResource, TestFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + (SourceResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<SourceResourceData>(result.StdOut); + AssertExistingSourceResourceData(output, DefaultSourceArgDirect, DefaultTrustLevel, DefaultExplicitState); + Assert.True(output.InDesiredState); + + AssertDiffState(diff, []); + } + + /// <summary> + /// Calls `test` on the `source` resource with a argument that matches. + /// </summary> + /// <param name="useDefaultArgument">The argument to use when adding the existing source.</param> + /// <param name="trustLevel">The trust level to use when adding the existing source.</param> + /// <param name="isExplicit">The explicit state to use when adding the existing source.</param> + /// <param name="targetProperty">The property to target for the test.</param> + [TestCase(false, DefaultTrustLevel, true, ArgumentPropertyName)] + [TestCase(true, DefaultTrustLevel, false, TypePropertyName)] + [TestCase(false, TrustedTrustLevel, false, TrustLevelPropertyName)] + [TestCase(true, DefaultTrustLevel, true, ExplicitPropertyName)] + public void Source_Test_PropertyMatch(bool useDefaultArgument, string trustLevel, bool isExplicit, string targetProperty) + { + var setup = TestCommon.RunAICLICommand("source add", $"--name {DefaultSourceName} --arg {(useDefaultArgument ? DefaultSourceArgForCmdLine : NonDefaultSourceArgForCmdLine)} --type {DefaultSourceType} --trust-level {trustLevel} {(isExplicit ? "--explicit" : string.Empty)}"); + Assert.AreEqual(0, setup.ExitCode); + + SourceResourceData resourceData = new SourceResourceData() { Name = DefaultSourceName }; + + switch (targetProperty) + { + case ArgumentPropertyName: + resourceData.Argument = useDefaultArgument ? DefaultSourceArgDirect : NonDefaultSourceArgDirect; + break; + case TypePropertyName: + resourceData.Type = DefaultSourceType; + break; + case TrustLevelPropertyName: + resourceData.TrustLevel = trustLevel; + break; + case ExplicitPropertyName: + resourceData.Explicit = isExplicit; + break; + default: + Assert.Fail($"{targetProperty} is not a handled case."); + break; + } + + var result = RunDSCv3Command(SourceResource, TestFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + (SourceResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<SourceResourceData>(result.StdOut); + AssertExistingSourceResourceData(output, useDefaultArgument ? DefaultSourceArgDirect : NonDefaultSourceArgDirect, trustLevel, isExplicit); + Assert.True(output.InDesiredState); + + AssertDiffState(diff, []); + } + + /// <summary> + /// Calls `test` on the `source` resource with a argument that does not match. + /// </summary> + /// <param name="useDefaultArgument">The argument to use when adding the existing source.</param> + /// <param name="trustLevel">The trust level to use when adding the existing source.</param> + /// <param name="isExplicit">The explicit state to use when adding the existing source.</param> + /// <param name="targetProperty">The property to target for the test.</param> + /// <param name="testValue">The value to test against.</param> + [TestCase(false, DefaultTrustLevel, true, ArgumentPropertyName, true)] + [TestCase(false, DefaultTrustLevel, false, TrustLevelPropertyName, TrustedTrustLevel)] + [TestCase(true, DefaultTrustLevel, true, ExplicitPropertyName, false)] + public void Source_Test_PropertyMismatch(bool useDefaultArgument, string trustLevel, bool isExplicit, string targetProperty, object testValue) + { + var setup = TestCommon.RunAICLICommand("source add", $"--name {DefaultSourceName} --arg {(useDefaultArgument ? DefaultSourceArgForCmdLine : NonDefaultSourceArgForCmdLine)} --type {DefaultSourceType} --trust-level {trustLevel} {(isExplicit ? "--explicit" : string.Empty)}"); + Assert.AreEqual(0, setup.ExitCode); + + SourceResourceData resourceData = new SourceResourceData() { Name = DefaultSourceName }; + + switch (targetProperty) + { + case ArgumentPropertyName: + resourceData.Argument = (bool)testValue ? DefaultSourceArgDirect : NonDefaultSourceArgDirect; + break; + case TrustLevelPropertyName: + resourceData.TrustLevel = (string)testValue; + break; + case ExplicitPropertyName: + resourceData.Explicit = (bool)testValue; + break; + default: + Assert.Fail($"{targetProperty} is not a handled case."); + break; + } + + var result = RunDSCv3Command(SourceResource, TestFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + (SourceResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<SourceResourceData>(result.StdOut); + AssertExistingSourceResourceData(output, useDefaultArgument ? DefaultSourceArgDirect : NonDefaultSourceArgDirect, trustLevel, isExplicit); + Assert.False(output.InDesiredState); + + AssertDiffState(diff, [ targetProperty ]); + } + + /// <summary> + /// Calls `test` on the `source` resource with all properties matching. + /// </summary> + [Test] + public void Source_Test_AllMatch() + { + var setup = TestCommon.RunAICLICommand("source add", $"--name {DefaultSourceName} --arg {NonDefaultSourceArgForCmdLine} --type {DefaultSourceType} --trust-level {TrustedTrustLevel} --explicit"); + Assert.AreEqual(0, setup.ExitCode); + + SourceResourceData resourceData = new SourceResourceData() + { + Name = DefaultSourceName, + Argument = NonDefaultSourceArgDirect, + Type = DefaultSourceType, + TrustLevel = TrustedTrustLevel, + Explicit = true, + }; + + var result = RunDSCv3Command(SourceResource, TestFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + (SourceResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<SourceResourceData>(result.StdOut); + AssertExistingSourceResourceData(output, resourceData); + Assert.True(output.InDesiredState); + + AssertDiffState(diff, []); + } + + /// <summary> + /// Calls `set` on the `source` resource when the source is not present, and again afterward. + /// </summary> + [Test] + public void Source_Set_SimpleRepeated() + { + SourceResourceData resourceData = new SourceResourceData() + { + Name = DefaultSourceName, + Argument = NonDefaultSourceArgDirect, + Type = DefaultSourceType, + }; + + var result = RunDSCv3Command(SourceResource, SetFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + (SourceResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<SourceResourceData>(result.StdOut); + AssertExistingSourceResourceData(output, resourceData); + + AssertDiffState(diff, [ ExistPropertyName ]); + + // Set again should be a no-op + result = RunDSCv3Command(SourceResource, SetFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + (output, diff) = GetSingleOutputLineAndDiffAs<SourceResourceData>(result.StdOut); + AssertExistingSourceResourceData(output, resourceData); + + AssertDiffState(diff, []); + } + + /// <summary> + /// Calls `set` on the `source` resource to ensure that it is not present. + /// </summary> + [Test] + public void Source_Set_Remove() + { + var setup = TestCommon.RunAICLICommand("source add", $"--name {DefaultSourceName} --arg {DefaultSourceArgForCmdLine} --type {DefaultSourceType} --explicit"); + Assert.AreEqual(0, setup.ExitCode); + + SourceResourceData resourceData = new SourceResourceData() + { + Name = DefaultSourceName, + Exist = false, + }; + + var result = RunDSCv3Command(SourceResource, SetFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + (SourceResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<SourceResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(resourceData.Name, output.Name); + + AssertDiffState(diff, [ ExistPropertyName ]); + + // Call `get` to ensure the result + SourceResourceData resourceDataForGet = new SourceResourceData() + { + Name = DefaultSourceName, + }; + + result = RunDSCv3Command(SourceResource, GetFunction, resourceDataForGet); + AssertSuccessfulResourceRun(ref result); + + output = GetSingleOutputLineAs<SourceResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(resourceDataForGet.Name, output.Name); + } + + /// <summary> + /// Calls `set` on the `source` resource with an existing item, replacing it. + /// </summary> + [Test] + public void Source_Set_Replace() + { + var setup = TestCommon.RunAICLICommand("source add", $"--name {DefaultSourceName} --arg {DefaultSourceArgForCmdLine} --type {DefaultSourceType}"); + Assert.AreEqual(0, setup.ExitCode); + + SourceResourceData resourceData = new SourceResourceData() + { + Name = DefaultSourceName, + Argument = DefaultSourceArgDirect, + Type = DefaultSourceType, + TrustLevel = TrustedTrustLevel, + Explicit = true, + }; + + var result = RunDSCv3Command(SourceResource, SetFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + (SourceResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<SourceResourceData>(result.StdOut); + AssertExistingSourceResourceData(output, resourceData); + + AssertDiffState(diff, [ TrustLevelPropertyName, ExplicitPropertyName ]); + + // Call `get` to ensure the result + SourceResourceData resourceDataForGet = new SourceResourceData() + { + Name = DefaultSourceName, + }; + + result = RunDSCv3Command(SourceResource, GetFunction, resourceDataForGet); + AssertSuccessfulResourceRun(ref result); + + output = GetSingleOutputLineAs<SourceResourceData>(result.StdOut); + AssertExistingSourceResourceData(output, resourceData); + } + + /// <summary> + /// Calls `export` on the `source` resource without providing any input. + /// </summary> + [Test] + public void Source_Export_NoInput() + { + var setup = TestCommon.RunAICLICommand("source add", $"--name {DefaultSourceName} --arg {DefaultSourceArgForCmdLine} --type {DefaultSourceType}"); + Assert.AreEqual(0, setup.ExitCode); + + var result = RunDSCv3Command(SourceResource, ExportFunction, " "); + AssertSuccessfulResourceRun(ref result); + + List<SourceResourceData> output = GetOutputLinesAs<SourceResourceData>(result.StdOut); + + bool foundDefaultSource = false; + foreach (SourceResourceData item in output) + { + if (item.Name == DefaultSourceName) + { + foundDefaultSource = true; + Assert.AreEqual(DefaultSourceName, item.Name); + Assert.AreEqual(DefaultSourceArgDirect, item.Argument); + Assert.AreEqual(DefaultSourceType, item.Type); + Assert.AreEqual(DefaultTrustLevel, item.TrustLevel); + Assert.AreEqual(DefaultExplicitState, item.Explicit); + break; + } + } + + Assert.IsTrue(foundDefaultSource); + } + + private static void RemoveTestSource() + { + SourceResourceData resourceData = new SourceResourceData() + { + Name = DefaultSourceName, + Exist = false, + }; + + var result = RunDSCv3Command(SourceResource, SetFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + } + + private static void AssertExistingSourceResourceData(SourceResourceData output, SourceResourceData input) + { + AssertExistingSourceResourceData(output, input.Argument, input.TrustLevel, input.Explicit); + } + + private static void AssertExistingSourceResourceData(SourceResourceData output, string argument, string trustLevel = null, bool? isExplicit = null) + { + Assert.IsNotNull(output); + Assert.True(output.Exist); + Assert.AreEqual(DefaultSourceName, output.Name); + Assert.AreEqual(argument, output.Argument); + Assert.AreEqual(DefaultSourceType, output.Type); + + if (trustLevel != null) + { + Assert.AreEqual(trustLevel, output.TrustLevel); + } + + if (isExplicit != null) + { + Assert.AreEqual(isExplicit, output.Explicit); + } + } + + private static string CreateSourceArgument(bool forCommandLine = false, int openHR = 0, int searchHR = 0) + { + const string CommandLineFormat = @"""{{""""OpenHR"""": {0}, """"SearchHR"""": {1} }}"""; + const string DirectFormat = @"{{""OpenHR"": {0}, ""SearchHR"": {1} }}"; + return string.Format(forCommandLine ? CommandLineFormat : DirectFormat, openHR, searchHR); + } + + private class SourceResourceData + { + [JsonPropertyName(ExistPropertyName)] + public bool? Exist { get; set; } + + [JsonPropertyName(InDesiredStatePropertyName)] + public bool? InDesiredState { get; set; } + + public string Name { get; set; } + + public string Argument { get; set; } + + public string Type { get; set; } + + public string TrustLevel { get; set; } + + public bool? Explicit { get; set; } + + public bool? AcceptAgreements { get; set; } + } + } +} diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3280,10 +3280,31 @@ Please specify one of them using the --source option to proceed.</value> <data name="DscResourcePropertyDescriptionPackageScope" xml:space="preserve"> <value>The target scope of the package.</value> </data> - <data name="DscResourcePropertyDescriptionPackageAcceptAgreements" xml:space="preserve"> - <value>Indicates whether to accept agreements for package installs.</value> + <data name="DscResourcePropertyDescriptionAcceptAgreements" xml:space="preserve"> + <value>Indicates whether to accept agreements for sources and packages.</value> </data> <data name="CorrelationArgumentDescription" xml:space="preserve"> <value>Value is logged for correlation</value> </data> + <data name="DscSourceResourceShortDescription" xml:space="preserve"> + <value>Manage source configuration</value> + </data> + <data name="DscSourceResourceLongDescription" xml:space="preserve"> + <value>Allows management of source configuration via the DSC v3 command line interface protocol. See the help link for details.</value> + </data> + <data name="DscResourcePropertyDescriptionSourceName" xml:space="preserve"> + <value>The name to use for the source.</value> + </data> + <data name="DscResourcePropertyDescriptionSourceArgument" xml:space="preserve"> + <value>The argument for the source.</value> + </data> + <data name="DscResourcePropertyDescriptionSourceType" xml:space="preserve"> + <value>The type of the source.</value> + </data> + <data name="DscResourcePropertyDescriptionSourceTrustLevel" xml:space="preserve"> + <value>The trust level of the source.</value> + </data> + <data name="DscResourcePropertyDescriptionSourceExplicit" xml:space="preserve"> + <value>Whether the source is included when calls don't specify a source.</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCommonCore/pch.h b/src/AppInstallerCommonCore/pch.h @@ -28,7 +28,7 @@ #include <json/json.h> #pragma warning( push ) -#pragma warning ( disable : 4458 4100 4702 6031 ) +#pragma warning ( disable : 4458 4100 4702 6031 26439 ) #include <valijson/schema.hpp> #include <valijson/schema_parser.hpp> #include <valijson/validator.hpp> diff --git a/src/AppInstallerSharedLib/pch.h b/src/AppInstallerSharedLib/pch.h @@ -19,7 +19,7 @@ #include <json/json.h> #pragma warning( push ) -#pragma warning ( disable : 4458 4100 4702 6031 ) +#pragma warning ( disable : 4458 4100 4702 6031 26439 ) #include <valijson/schema.hpp> #include <valijson/schema_parser.hpp> #include <valijson/validator.hpp>