diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index 757853bbde..e0aa00db36 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -306,6 +306,7 @@ + @@ -392,6 +393,7 @@ + diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index 0004d827b1..52b6f070e2 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -284,6 +284,9 @@ Commands\Configuration + + Commands\Configuration + @@ -535,6 +538,9 @@ Commands\Configuration + + Commands\Configuration + diff --git a/src/AppInstallerCLICore/Commands/DscCommand.cpp b/src/AppInstallerCLICore/Commands/DscCommand.cpp index d3a57b7b44..36dcba1c72 100644 --- 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::make_unique(FullName()), + std::make_unique(FullName()), #ifndef AICLI_DISABLE_TEST_HOOKS std::make_unique(FullName()), std::make_unique(FullName()), diff --git a/src/AppInstallerCLICore/Commands/DscComposableObject.h b/src/AppInstallerCLICore/Commands/DscComposableObject.h index ec7f13faa0..3b3fa7ddc5 100644 --- 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 ToJson(const Property*); // - // const Type& PROPERTY_NAME() const; - // void PROPERTY_NAME(const Type&); + // const PropertyType& PROPERTY_NAME() const; + // void PROPERTY_NAME(const PropertyType&); // } template 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::SchemaType(), Property::Description(), Property::EnumValues(), Property::Default())); + (FoldHelper{}, ..., details::AddPropertySchema(result, Property::Name(), Property::Flags, GetJsonTypeValue::SchemaType(), Property::Description(), Property::EnumValues(), Property::Default())); return result; } }; - template + template 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 m_value; + std::optional 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 EnumValues() { return std::vector _enum_vals_; } \ static std::optional Default() { return _default_; } \ - std::optional& _property_name_() { return m_value; } \ - const std::optional& _property_name_() const { return m_value; } \ - void _property_name_(std::optional value) { m_value = std::move(value); } \ + std::optional& _property_name_() { return m_value; } \ + const std::optional& _property_name_() const { return m_value; } \ + void _property_name_(std::optional 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 index d3e480d3db..7df2afcff3 100644 --- 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 new file mode 100644 index 0000000000..0623db11b2 --- /dev/null +++ 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 + +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; + + 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& 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, bool ignoreFieldRequirements = false) : + Input(json, ignoreFieldRequirements), + ParentContext(context) + { + Reset(); + } + + const SourceResourceObject Input; + SourceResourceObject Output; + Execution::Context& ParentContext; + std::unique_ptr 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 sources; + sources.emplace_back(source); + SubContext->Add(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 new file mode 100644 index 0000000000..236ef470d9 --- /dev/null +++ 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 index 8b885c45bb..ca93e577d3 100644 --- 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 index 069bd33ad9..e11cf29652 100644 --- a/src/AppInstallerCLICore/pch.h +++ b/src/AppInstallerCLICore/pch.h @@ -10,7 +10,7 @@ #include #pragma warning( push ) -#pragma warning ( disable : 4458 4100 6031 4702 ) +#pragma warning ( disable : 4458 4100 6031 4702 26439 ) #include #include #include diff --git a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs index 0abb6f400f..abb88d3b92 100644 --- 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; /// @@ -25,14 +23,8 @@ public class ConfigureCommand /// 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"); } /// diff --git a/src/AppInstallerCLIE2ETests/DSCv3ResourceCommands.cs b/src/AppInstallerCLIE2ETests/DSCv3PackageResourceCommand.cs similarity index 83% rename from src/AppInstallerCLIE2ETests/DSCv3ResourceCommands.cs rename to src/AppInstallerCLIE2ETests/DSCv3PackageResourceCommand.cs index b8831c1709..b01ecc6352 100644 --- a/src/AppInstallerCLIE2ETests/DSCv3ResourceCommands.cs +++ b/src/AppInstallerCLIE2ETests/DSCv3PackageResourceCommand.cs @@ -1,15 +1,12 @@ // ----------------------------------------------------------------------------- -// +// // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // // ----------------------------------------------------------------------------- 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; @@ -19,33 +16,16 @@ namespace AppInstallerCLIE2ETests /// [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 + 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 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"; - /// - /// Ensures that the test resources manifests are present. - /// - 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); - } - /// /// Setup done once before all the tests here. /// @@ -55,7 +35,7 @@ public void OneTimeSetup() TestCommon.SetupTestSource(); WinGetSettingsHelper.ConfigureFeature("dsc3", true); WinGetSettingsHelper.ConfigureLoggingLevel("verbose"); - EnsureTestResourcePresence(); + EnsureTestResourcePresence(PackageResource); } /// @@ -172,9 +152,7 @@ public void Package_Test_NotPresent() Assert.AreEqual(packageResourceData.Identifier, output.Identifier); Assert.False(output.InDesiredState); - Assert.IsNotNull(diff); - Assert.AreEqual(1, diff.Count); - Assert.AreEqual(ExistPropertyName, diff[0]); + AssertDiffState(diff, [ ExistPropertyName ]); } /// @@ -552,73 +530,6 @@ private static void RemoveTestPackage() 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(string output) - { - string[] lines = GetOutputLines(output); - Assert.AreEqual(1, lines.Length); - - return JsonSerializer.Deserialize(lines[0], GetDefaultJsonOptions()); - } - - private static (T, List) GetSingleOutputLineAndDiffAs(string output) - { - string[] lines = GetOutputLines(output); - Assert.AreEqual(2, lines.Length); - - var options = GetDefaultJsonOptions(); - return (JsonSerializer.Deserialize(lines[0], options), JsonSerializer.Deserialize>(lines[1], options)); - } - - private static List GetOutputLinesAs(string output) - { - List result = new List(); - string[] lines = GetOutputLines(output); - var options = GetDefaultJsonOptions(); - - foreach (string line in lines) - { - result.Add(JsonSerializer.Deserialize(line, options)); - } - - return result; - } - private static void AssertExistingPackageResourceData(PackageResourceData output, string version, bool ignoreLatest = false) { Assert.IsNotNull(output); @@ -639,23 +550,12 @@ private static void AssertExistingPackageResourceData(PackageResourceData output } } - private static void AssertDiffState(List diff, IList 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")] + [JsonPropertyName(InDesiredStatePropertyName)] public bool? InDesiredState { get; set; } [JsonPropertyName("id")] diff --git a/src/AppInstallerCLIE2ETests/DSCv3ResourceTestBase.cs b/src/AppInstallerCLIE2ETests/DSCv3ResourceTestBase.cs new file mode 100644 index 0000000000..838eb17375 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/DSCv3ResourceTestBase.cs @@ -0,0 +1,184 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +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; + + /// + /// Provides common functionality for DSC v3 resource tests. + /// + public class DSCv3ResourceTestBase + { + /// + /// The string for the `get` function. + /// + public const string GetFunction = "get"; + + /// + /// The string for the `test` function. + /// + public const string TestFunction = "test"; + + /// + /// The string for the `set` function. + /// + public const string SetFunction = "set"; + + /// + /// The string for the `export` function. + /// + public const string ExportFunction = "export"; + + /// + /// The string for the `_exist` property name. + /// + public const string ExistPropertyName = "_exist"; + + /// + /// The string for the `_inDesiredState` property name. + /// + public const string InDesiredStatePropertyName = "_inDesiredState"; + + /// + /// Write the resource manifest out to the WindowsApps alias directory. + /// + /// The resource manifest to write. + 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); + } + + /// + /// Runs a DSC v3 resource command. + /// + /// The resource to target. + /// The resource function to run. + /// Input for the function; supports null, direct string, or JSON serialization of complex objects. + /// The maximum time to wait in milliseconds. + /// Whether to throw on a timeout or simply return the incomplete result. + /// A RunCommandResult containing the process exit code and output and error streams. + 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); + } + + /// + /// Asserts that a RunCommandResult contains a success for a DSC v3 resource command run. + /// + /// The result of a DSC v3 resource command run. + protected static void AssertSuccessfulResourceRun(ref TestCommon.RunCommandResult result) + { + Assert.AreEqual(0, result.ExitCode); + Assert.IsNotEmpty(result.StdOut); + } + + /// + /// Gets the output as lines. + /// + /// The output stream from a DSC v3 resource command. + /// The lines of the output. + protected static string[] GetOutputLines(string output) + { + return output.TrimEnd().Split(Environment.NewLine); + } + + /// + /// Asserts that the output is a single line and deserializes that line as JSON. + /// + /// The type to deserialize from JSON. + /// The output stream from a DSC v3 resource command. + /// The object as deserialized. + protected static T GetSingleOutputLineAs(string output) + { + string[] lines = GetOutputLines(output); + Assert.AreEqual(1, lines.Length); + + return JsonSerializer.Deserialize(lines[0], GetDefaultJsonOptions()); + } + + /// + /// Asserts that the output is two lines and deserializes them as a JSON object and JSON string array. + /// + /// The type to deserialize from JSON. + /// The output stream from a DSC v3 resource command. + /// The object as deserialized and the contents of the string array. + protected static (T, List) GetSingleOutputLineAndDiffAs(string output) + { + string[] lines = GetOutputLines(output); + Assert.AreEqual(2, lines.Length); + + var options = GetDefaultJsonOptions(); + return (JsonSerializer.Deserialize(lines[0], options), JsonSerializer.Deserialize>(lines[1], options)); + } + + /// + /// Deserializes all lines as JSON objects. + /// + /// The type to deserialize from JSON. + /// The output stream from a DSC v3 resource command. + /// A List of objects as deserialized. + protected static List GetOutputLinesAs(string output) + { + List result = new List(); + string[] lines = GetOutputLines(output); + var options = GetDefaultJsonOptions(); + + foreach (string line in lines) + { + result.Add(JsonSerializer.Deserialize(line, options)); + } + + return result; + } + + /// + /// Requires that the diff from a resource command contain the same set of strings as expected. + /// + /// The diff from a resource command. + /// The expected strings. + protected static void AssertDiffState(List diff, IList 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 new file mode 100644 index 0000000000..9a27c7ad42 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs @@ -0,0 +1,485 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System.Collections.Generic; + using System.Text.Json.Serialization; + using AppInstallerCLIE2ETests.Helpers; + using NUnit.Framework; + using WinRT; + + /// + /// `Configure` command tests. + /// + [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); } + } + + /// + /// Setup done once before all the tests here. + /// + [OneTimeSetUp] + public void OneTimeSetup() + { + TestCommon.SetupTestSource(); + WinGetSettingsHelper.ConfigureFeature("dsc3", true); + EnsureTestResourcePresence(SourceResource); + } + + /// + /// Teardown done once after all the tests here. + /// + [OneTimeTearDown] + public void OneTimeTeardown() + { + RemoveTestSource(); + WinGetSettingsHelper.ConfigureFeature("dsc3", false); + TestCommon.TearDownTestSource(); + } + + /// + /// Set up. + /// + [SetUp] + public void Setup() + { + RemoveTestSource(); + } + + /// + /// Calls `get` on the `source` resource with the value not present. + /// + [Test] + public void Source_Get_NotPresent() + { + SourceResourceData resourceData = new SourceResourceData() { Name = DefaultSourceName }; + + var result = RunDSCv3Command(SourceResource, GetFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + SourceResourceData output = GetSingleOutputLineAs(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(resourceData.Name, output.Name); + } + + /// + /// Calls `get` on the `source` resource with the value present. + /// + [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(result.StdOut); + AssertExistingSourceResourceData(output, DefaultSourceArgDirect, DefaultTrustLevel, true); + } + + /// + /// Calls `test` on the `source` resource with the value not present. + /// + [Test] + public void Source_Test_NotPresent() + { + SourceResourceData resourceData = new SourceResourceData() { Name = DefaultSourceName }; + + var result = RunDSCv3Command(SourceResource, TestFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + (SourceResourceData output, List diff) = GetSingleOutputLineAndDiffAs(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(resourceData.Name, output.Name); + Assert.False(output.InDesiredState); + + AssertDiffState(diff, [ ExistPropertyName ]); + } + + /// + /// Calls `test` on the `source` resource with the value present. + /// + [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 diff) = GetSingleOutputLineAndDiffAs(result.StdOut); + AssertExistingSourceResourceData(output, DefaultSourceArgDirect, DefaultTrustLevel, DefaultExplicitState); + Assert.True(output.InDesiredState); + + AssertDiffState(diff, []); + } + + /// + /// Calls `test` on the `source` resource with a argument that matches. + /// + /// The argument to use when adding the existing source. + /// The trust level to use when adding the existing source. + /// The explicit state to use when adding the existing source. + /// The property to target for the test. + [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 diff) = GetSingleOutputLineAndDiffAs(result.StdOut); + AssertExistingSourceResourceData(output, useDefaultArgument ? DefaultSourceArgDirect : NonDefaultSourceArgDirect, trustLevel, isExplicit); + Assert.True(output.InDesiredState); + + AssertDiffState(diff, []); + } + + /// + /// Calls `test` on the `source` resource with a argument that does not match. + /// + /// The argument to use when adding the existing source. + /// The trust level to use when adding the existing source. + /// The explicit state to use when adding the existing source. + /// The property to target for the test. + /// The value to test against. + [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 diff) = GetSingleOutputLineAndDiffAs(result.StdOut); + AssertExistingSourceResourceData(output, useDefaultArgument ? DefaultSourceArgDirect : NonDefaultSourceArgDirect, trustLevel, isExplicit); + Assert.False(output.InDesiredState); + + AssertDiffState(diff, [ targetProperty ]); + } + + /// + /// Calls `test` on the `source` resource with all properties matching. + /// + [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 diff) = GetSingleOutputLineAndDiffAs(result.StdOut); + AssertExistingSourceResourceData(output, resourceData); + Assert.True(output.InDesiredState); + + AssertDiffState(diff, []); + } + + /// + /// Calls `set` on the `source` resource when the source is not present, and again afterward. + /// + [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 diff) = GetSingleOutputLineAndDiffAs(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(result.StdOut); + AssertExistingSourceResourceData(output, resourceData); + + AssertDiffState(diff, []); + } + + /// + /// Calls `set` on the `source` resource to ensure that it is not present. + /// + [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 diff) = GetSingleOutputLineAndDiffAs(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(result.StdOut); + Assert.IsNotNull(output); + Assert.False(output.Exist); + Assert.AreEqual(resourceDataForGet.Name, output.Name); + } + + /// + /// Calls `set` on the `source` resource with an existing item, replacing it. + /// + [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 diff) = GetSingleOutputLineAndDiffAs(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(result.StdOut); + AssertExistingSourceResourceData(output, resourceData); + } + + /// + /// Calls `export` on the `source` resource without providing any input. + /// + [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 output = GetOutputLinesAs(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 index 65c787d8c3..1f7f69c2c9 100644 --- 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. The target scope of the package. - - Indicates whether to accept agreements for package installs. + + Indicates whether to accept agreements for sources and packages. Value is logged for correlation + + Manage source configuration + + + Allows management of source configuration via the DSC v3 command line interface protocol. See the help link for details. + + + The name to use for the source. + + + The argument for the source. + + + The type of the source. + + + The trust level of the source. + + + Whether the source is included when calls don't specify a source. + \ No newline at end of file diff --git a/src/AppInstallerCommonCore/pch.h b/src/AppInstallerCommonCore/pch.h index 2825b5ff5c..bf28a8c0c6 100644 --- a/src/AppInstallerCommonCore/pch.h +++ b/src/AppInstallerCommonCore/pch.h @@ -28,7 +28,7 @@ #include #pragma warning( push ) -#pragma warning ( disable : 4458 4100 4702 6031 ) +#pragma warning ( disable : 4458 4100 4702 6031 26439 ) #include #include #include diff --git a/src/AppInstallerSharedLib/pch.h b/src/AppInstallerSharedLib/pch.h index d9bf1dd1ec..57f5054501 100644 --- a/src/AppInstallerSharedLib/pch.h +++ b/src/AppInstallerSharedLib/pch.h @@ -19,7 +19,7 @@ #include #pragma warning( push ) -#pragma warning ( disable : 4458 4100 4702 6031 ) +#pragma warning ( disable : 4458 4100 4702 6031 26439 ) #include #include #include