Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<PackageReference Include="Microsoft.Msix.Utils" />
<PackageReference Include="Microsoft.Win32.Registry" />
<PackageReference Include="Microsoft.Windows.CsWinRT" />
<PackageReference Include="ModelContextProtocol.Core" />
<PackageReference Include="nunit" />
<PackageReference Include="NUnit3TestAdapter" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
Expand Down
1 change: 1 addition & 0 deletions src/AppInstallerCLIE2ETests/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class Constants
public const string InprocTestbedUseTestPackageParameter = "InprocTestbedUseTestPackage";
public const string WinGetServerPathParameter = "WinGetServerPath";
public const string RpcTestHelperPathParameter = "RpcTestHelperPath";
public const string MCPServerPathParameter = "MCPServerPath";

// Test Sources
public const string DefaultWingetSourceName = @"winget";
Expand Down
124 changes: 124 additions & 0 deletions src/AppInstallerCLIE2ETests/Helpers/AgreementsRestSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// -----------------------------------------------------------------------------
// <copyright file="AgreementsRestSource.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------------

namespace AppInstallerCLIE2ETests.Helpers
{
using System.IO;
using NUnit.Framework;

/// <summary>
/// A REST source with source agreements, served from a generated information file under the static file root.
/// Only the information endpoint is served, so the source cannot be searched.
/// </summary>
public class AgreementsRestSource
{
/// <summary>
/// The label of the source agreement.
/// </summary>
public const string AgreementLabel = "Test Agreement";

/// <summary>
/// The text of the source agreement.
/// </summary>
public const string AgreementText = "Test agreement text";

/// <summary>
/// The URL of the source agreement.
/// </summary>
public const string AgreementUrl = "https://localhost:5001/TestKit/agreement";

/// <summary>
/// Initializes a new instance of the <see cref="AgreementsRestSource"/> class.
/// </summary>
/// <param name="name">The name of the source, also used as its directory under the static file root.</param>
public AgreementsRestSource(string name)
{
this.Name = name;
}

/// <summary>
/// Gets the name of the source.
/// </summary>
public string Name { get; }

/// <summary>
/// Gets the URL of the source.
/// </summary>
public string Url => "https://localhost:5001/TestKit/" + this.Name;

private string InformationFilePath => Path.Combine(TestSetup.Parameters.StaticFileRootPath, this.Name, "information");

/// <summary>
/// Writes the information served by the source.
/// </summary>
/// <param name="agreementsIdentifier">The agreements identifier, or null for a source without agreements.</param>
/// <param name="requiresMarket">Whether the source requires the market, which is an implicit agreement.</param>
/// <param name="includeAgreements">Whether the agreements identifier is accompanied by an agreement.</param>
public void WriteInformation(string agreementsIdentifier, bool requiresMarket, bool includeAgreements = true)
{
string agreementEntries = includeAgreements ? $@"{{
""AgreementLabel"": ""{AgreementLabel}"",
""Agreement"": ""{AgreementText}"",
""AgreementUrl"": ""{AgreementUrl}""
}}" : string.Empty;

string agreements = agreementsIdentifier == null ? string.Empty : $@",
""SourceAgreements"": {{
""AgreementsIdentifier"": ""{agreementsIdentifier}"",
""Agreements"": [{agreementEntries}]
}}";

string market = requiresMarket ? @",
""RequiredPackageMatchFields"": [ ""Market"" ]" : string.Empty;

string information = $@"{{
""Data"": {{
""SourceIdentifier"": ""{this.Name}"",
""ServerSupportedVersions"": [ ""1.0.0"" ]{agreements}{market}
}}
}}";

Directory.CreateDirectory(Path.GetDirectoryName(this.InformationFilePath));
File.WriteAllText(this.InformationFilePath, information);
}

/// <summary>
/// Adds the source with the CLI and then changes its agreements, so that they are not accepted.
/// Adding a source accepts its current agreements.
/// </summary>
/// <param name="requiresMarket">Whether the source requires the market, which is an implicit agreement.</param>
public void AddWithUnacceptedAgreements(bool requiresMarket = false)
{
this.WriteInformation("AgreementsV1", requiresMarket);
var result = TestCommon.RunAICLICommand("source add", $"{this.Name} {this.Url} --type {Constants.RestTestSourceType} --trust-level trusted --accept-source-agreements");
Assert.That(result.ExitCode, Is.EqualTo(Constants.ErrorCode.S_OK), result.StdOut);

this.WriteInformation("AgreementsV2", requiresMarket);
}

/// <summary>
/// Deletes the information served by the source, so that requests for it fail.
/// </summary>
public void DeleteInformation()
{
File.Delete(this.InformationFilePath);
}

/// <summary>
/// Removes the source and its information file.
/// </summary>
public void Remove()
{
TestCommon.RunAICLICommand("source remove", this.Name);

var directory = Path.GetDirectoryName(this.InformationFilePath);
if (Directory.Exists(directory))
{
Directory.Delete(directory, true);
}
}
}
}
6 changes: 6 additions & 0 deletions src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ private TestSetup()
this.InprocTestbedPath = this.InitializeFileParam(Constants.InprocTestbedPathParameter);
this.WinGetServerPath = this.InitializeStringParam(Constants.WinGetServerPathParameter);
this.RpcTestHelperPath = this.InitializeStringParam(Constants.RpcTestHelperPathParameter);
this.MCPServerPath = this.InitializeStringParam(Constants.MCPServerPathParameter, "WindowsPackageManagerMCPServerDev.exe");

this.ForcedExperimentalFeatures = this.InitializeStringArrayParam(Constants.ForcedExperimentalFeaturesParameter);
}
Expand Down Expand Up @@ -149,6 +150,11 @@ public static TestSetup Parameters
/// </summary>
public string RpcTestHelperPath { get; }

/// <summary>
/// Gets the command used to start the MCP server for the MCP tests.
/// </summary>
public string MCPServerPath { get; }

/// <summary>
/// Gets a value indicating whether to use the test package or not.
/// </summary>
Expand Down
153 changes: 153 additions & 0 deletions src/AppInstallerCLIE2ETests/Interop/SourceAgreementsInterop.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// -----------------------------------------------------------------------------
// <copyright file="SourceAgreementsInterop.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------------
namespace AppInstallerCLIE2ETests.Interop
{
using System;
using System.Threading.Tasks;
using AppInstallerCLIE2ETests.Helpers;
using Microsoft.Management.Deployment;
using Microsoft.Management.Deployment.Projection;
using NUnit.Framework;

/// <summary>
/// Tests for the source agreement acceptance state on PackageCatalogReference.
/// </summary>
[TestFixtureSource(typeof(InstanceInitializersSource), nameof(InstanceInitializersSource.InProcess), Category = nameof(InstanceInitializersSource.InProcess))]
[TestFixtureSource(typeof(InstanceInitializersSource), nameof(InstanceInitializersSource.OutOfProcess), Category = nameof(InstanceInitializersSource.OutOfProcess))]
public class SourceAgreementsInterop : BaseInterop
{
private const int ErrorIllegalStateChange = unchecked((int)0x8000000D);

private readonly AgreementsRestSource source = new AgreementsRestSource("TestAgreementsRestSource");

private PackageManager packageManager;

/// <summary>
/// Initializes a new instance of the <see cref="SourceAgreementsInterop"/> class.
/// </summary>
/// <param name="initializer">initializer.</param>
public SourceAgreementsInterop(IInstanceInitializer initializer)
: base(initializer)
{
}

/// <summary>
/// Set up.
/// </summary>
[SetUp]
public void Setup()
{
this.source.Remove();
this.packageManager = this.TestFactory.CreatePackageManager();
}

/// <summary>
/// Tear down.
/// </summary>
[TearDown]
public void TearDown()
{
this.source.Remove();
}

/// <summary>
/// A source without agreements or implicit agreement fields is satisfied.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Test]
public async Task NoAgreements_Satisfied()
{
this.source.WriteInformation(agreementsIdentifier: null, requiresMarket: false);
var catalogRef = await this.AddCatalogAsync();

Assert.That(catalogRef.SourceAgreements.Count, Is.EqualTo(0));
Assert.That(catalogRef.ImplicitAgreementFields, Is.EqualTo(ImplicitAgreementFields.None));
Assert.That(catalogRef.SourceAgreementsSatisfied, Is.True);
}

/// <summary>
/// Adding a source accepts its agreements; changing them requires acceptance again, which is persisted by SaveAcceptedSourceAgreements.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Test]
public async Task ChangedAgreements_NotSatisfiedUntilSaved()
{
this.source.WriteInformation("AgreementsV1", requiresMarket: true);
var catalogRef = await this.AddCatalogAsync();

Assert.That(catalogRef.SourceAgreements.Count, Is.EqualTo(1));
Assert.That(catalogRef.ImplicitAgreementFields, Is.EqualTo(ImplicitAgreementFields.Market));
Assert.That(catalogRef.SourceAgreementsSatisfied, Is.True, "Adding a source records acceptance of its agreements.");

// A new reference is needed, as the source information is retrieved once per reference.
this.source.WriteInformation("AgreementsV2", requiresMarket: true);
catalogRef = this.packageManager.GetPackageCatalogByName(this.source.Name);
Assert.That(catalogRef.SourceAgreementsSatisfied, Is.False);

catalogRef.SaveAcceptedSourceAgreements();
Assert.That(catalogRef.SourceAgreementsSatisfied, Is.True);

// Verify that the acceptance was persisted.
catalogRef = this.packageManager.GetPackageCatalogByName(this.source.Name);
Assert.That(catalogRef.SourceAgreementsSatisfied, Is.True);
}

/// <summary>
/// A source that changes its agreements identifier without declaring any agreements has nothing to accept.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Test]
public async Task EmptyAgreements_Satisfied()
{
this.source.WriteInformation("AgreementsV1", requiresMarket: false);
await this.AddCatalogAsync();

this.source.WriteInformation("AgreementsV2", requiresMarket: false, includeAgreements: false);
var catalogRef = this.packageManager.GetPackageCatalogByName(this.source.Name);

Assert.That(catalogRef.SourceAgreements.Count, Is.EqualTo(0));
Assert.That(catalogRef.SourceAgreementsSatisfied, Is.True);
}

/// <summary>
/// The agreement members are not available on a composite catalog reference.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Test]
public async Task Composite_Throws()
{
this.source.WriteInformation("AgreementsV1", requiresMarket: true);
var catalogRef = await this.AddCatalogAsync();

var options = this.TestFactory.CreateCreateCompositePackageCatalogOptions();
options.Catalogs.Add(catalogRef);
options.CompositeSearchBehavior = CompositeSearchBehavior.AllCatalogs;
var compositeRef = this.packageManager.CreateCompositePackageCatalog(options);
Assert.That(compositeRef.IsComposite, Is.True);

Assert.That(() => compositeRef.SourceAgreementsSatisfied, Throws.InstanceOf<Exception>().With.Property("HResult").EqualTo(ErrorIllegalStateChange));
Assert.That(() => compositeRef.ImplicitAgreementFields, Throws.InstanceOf<Exception>().With.Property("HResult").EqualTo(ErrorIllegalStateChange));
Assert.That(() => compositeRef.SaveAcceptedSourceAgreements(), Throws.InstanceOf<Exception>().With.Property("HResult").EqualTo(ErrorIllegalStateChange));
}

private async Task<PackageCatalogReference> AddCatalogAsync()
{
var options = this.TestFactory.CreateAddPackageCatalogOptions();
options.Name = this.source.Name;
options.SourceUri = this.source.Url;
options.Type = Constants.RestTestSourceType;
options.TrustLevel = PackageCatalogTrustLevel.Trusted;

var addResult = await this.packageManager.AddPackageCatalogAsync(options);
Assert.That(addResult, Is.Not.Null);
Assert.That(addResult.Status, Is.EqualTo(AddPackageCatalogStatus.Ok), $"Failed to add REST test source. Error: {addResult.ExtendedErrorCode?.HResult:X}");

var catalogRef = this.packageManager.GetPackageCatalogByName(this.source.Name);
Assert.That(catalogRef, Is.Not.Null);
return catalogRef;
}
}
}
Loading
Loading