Skip to content
Merged
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
198 changes: 196 additions & 2 deletions src/WebExpress.WebCore.Test/Manager/UnitTestPackageManager.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.IO.Compression;
using System.IO.Compression;
using System.Reflection;
using System.Security.Cryptography;
using WebExpress.WebCore.Test.Fixture;
using WebExpress.WebCore.WebComponent;
using WebExpress.WebCore.WebPackage;
Expand Down Expand Up @@ -246,5 +247,198 @@ public void LoadPackageReadsSpec()
Directory.Delete(packagePath, true);
}
}

/// <summary>
/// Tests package validation with extension/type checks.
/// </summary>
[Fact]
public void ValidatePackageRejectsInvalidExtension()
{
// arrange
var httpServerContext = UnitTestFixture.CreateHttpServerContextMock();
var componentHub = UnitTestFixture.CreateComponentHubMock(httpServerContext);
var packageManager = componentHub.PackageManager as PackageManager;
var packagePath = httpServerContext.PackagePath;
var dummyFile = Path.Combine(packagePath, "dummy.zip");

try
{
Directory.CreateDirectory(packagePath);
File.WriteAllText(dummyFile, "not-a-package");

// act
var validation = packageManager.ValidatePackage(dummyFile);

// validation
Assert.False(validation.IsValid);
Assert.Contains(validation.Messages, x => x.Contains("extension", StringComparison.OrdinalIgnoreCase));
}
finally
{
if (File.Exists(dummyFile))
{
File.Delete(dummyFile);
}

if (Directory.Exists(packagePath))
{
Directory.Delete(packagePath, true);
}
}
}

/// <summary>
/// Tests a complete package lifecycle using explicit package manager operations.
/// </summary>
[Fact]
public void PackageLifecycleInstallDeactivateActivateUninstall()
{
// arrange
var httpServerContext = UnitTestFixture.CreateHttpServerContextMock();
var componentHub = UnitTestFixture.CreateComponentHubMock(httpServerContext);
var packageManager = componentHub.PackageManager as PackageManager;
var packagePath = httpServerContext.PackagePath;
var packageFile = Path.Combine(packagePath, "lifecycle.1.0.0.wxp");

try
{
Directory.CreateDirectory(packagePath);
CreatePackageArchive(packageFile, "lifecycle", "1.0.0");

// act + validation (install active)
var install = packageManager.InstallPackage(packageFile, true);
Assert.True(install.Success);
Assert.Equal(PackageCatalogeItemState.Active, packageManager.GetPackage("lifecycle")?.State);

// act + validation (deactivate)
var deactivate = packageManager.DeactivatePackage("lifecycle");
Assert.True(deactivate.Success);
Assert.Equal(PackageCatalogeItemState.Disable, packageManager.GetPackage("lifecycle")?.State);

// act + validation (activate)
var activate = packageManager.ActivatePackage("lifecycle");
Assert.True(activate.Success);
Assert.Equal(PackageCatalogeItemState.Active, packageManager.GetPackage("lifecycle")?.State);

// act + validation (uninstall)
var uninstall = packageManager.UninstallPackage("lifecycle");
Assert.True(uninstall.Success);
Assert.Null(packageManager.GetPackage("lifecycle"));
}
finally
{
if (Directory.Exists(packagePath))
{
Directory.Delete(packagePath, true);
}
}
}

/// <summary>
/// Tests that update fails when uploaded package id does not match the requested package id.
/// </summary>
[Fact]
public void UpdatePackageRejectsMismatchedId()
{
// arrange
var httpServerContext = UnitTestFixture.CreateHttpServerContextMock();
var componentHub = UnitTestFixture.CreateComponentHubMock(httpServerContext);
var packageManager = componentHub.PackageManager as PackageManager;
var packagePath = httpServerContext.PackagePath;
var packageFile = Path.Combine(packagePath, "other.1.0.0.wxp");

try
{
Directory.CreateDirectory(packagePath);
CreatePackageArchive(packageFile, "other", "1.0.0");

// act
var result = packageManager.UpdatePackage("expected", packageFile);

// validation
Assert.False(result.Success);
Assert.Contains("does not match", result.Message, StringComparison.OrdinalIgnoreCase);
}
finally
{
if (Directory.Exists(packagePath))
{
Directory.Delete(packagePath, true);
}
}
}

/// <summary>
/// Tests package SHA-256 verification support.
/// </summary>
[Fact]
public void ValidatePackageWithSha256()
{
// arrange
var httpServerContext = UnitTestFixture.CreateHttpServerContextMock();
var componentHub = UnitTestFixture.CreateComponentHubMock(httpServerContext);
var packageManager = componentHub.PackageManager as PackageManager;
var packagePath = httpServerContext.PackagePath;
var packageFile = Path.Combine(packagePath, "signed.1.0.0.wxp");

try
{
Directory.CreateDirectory(packagePath);
CreatePackageArchive(packageFile, "signed", "1.0.0");
var expectedHash = ComputeSha256(packageFile);

// act
var valid = packageManager.ValidatePackage(packageFile, expectedSha256: expectedHash);
var invalid = packageManager.ValidatePackage(packageFile, expectedSha256: "deadbeef");

// validation
Assert.True(valid.IsValid);
Assert.False(invalid.IsValid);
}
finally
{
if (Directory.Exists(packagePath))
{
Directory.Delete(packagePath, true);
}
}
}

/// <summary>
/// Creates a simple package archive for tests.
/// </summary>
/// <param name="file">The package file path.</param>
/// <param name="id">The package id.</param>
/// <param name="version">The package version.</param>
private static void CreatePackageArchive(string file, string id, string version)
{
using var zip = ZipFile.Open(file, ZipArchiveMode.Create);
var specEntry = zip.CreateEntry($"{id}.spec");
using (var writer = new StreamWriter(specEntry.Open()))
{
writer.Write($@"
<package>
<id>{id}</id>
<version>{version}</version>
<title>{id}-title</title>
<authors>UnitTest</authors>
</package>");
}

// add minimal lib folder marker to resemble package layout
zip.CreateEntry("lib/");
}

/// <summary>
/// Computes SHA-256 for a test file.
/// </summary>
/// <param name="file">The file path.</param>
/// <returns>The sha-256 hash as lowercase hex.</returns>
private static string ComputeSha256(string file)
{
using var stream = File.OpenRead(file);
var hash = SHA256.HashData(stream);
return Convert.ToHexString(hash).ToLowerInvariant();
}
}
}
}
22 changes: 21 additions & 1 deletion src/WebExpress.WebCore.Test/Manager/UnitTestPluginManager.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using WebExpress.WebCore.Test.Fixture;
using WebExpress.WebCore.Test.Fixture;
using WebExpress.WebCore.WebComponent;
using WebExpress.WebCore.WebPlugin;
using WebExpress.WebCore.WebPlugin.Model;

namespace WebExpress.WebCore.Test.Manager
{
Expand Down Expand Up @@ -253,5 +254,24 @@ public void IsIContext()
Assert.True(typeof(IContext).IsAssignableFrom(plugin.GetType()), $"Plugin context {plugin.GetType().Name} does not implement IContext.");
}
}

/// <summary>
/// Tests runtime plugin metadata retrieval.
/// </summary>
[Fact]
public void GetPluginRuntimeInfos()
{
// arrange
var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock();
var pluginManager = componentHub.PluginManager;

// act
var infos = pluginManager.GetPluginRuntimeInfos().ToList();

// validation
Assert.NotEmpty(infos);
Assert.Contains(infos, x => x.PluginContext.PluginId.ToString() == "webexpress.webcore.test");
Assert.All(infos, x => Assert.Equal(PluginRuntimeState.Active, x.State));
}
}
}
79 changes: 78 additions & 1 deletion src/WebExpress.WebCore/WebPackage/IPackageManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System;
using System.IO;
using System.Collections.Generic;
using WebExpress.WebCore.WebComponent;
using WebExpress.WebCore.WebPackage.Model;

Expand All @@ -24,5 +26,80 @@ public interface IPackageManager : IComponentManager
/// Gets the catalog of installed packages.
/// </summary>
PackageCatalog Catalog { get; }

/// <summary>
/// Returns all package entries from the package catalog.
/// </summary>
/// <returns>An enumerable collection with all package entries.</returns>
IEnumerable<PackageCatalogItem> GetPackages();

/// <summary>
/// Returns a package by id.
/// </summary>
/// <param name="packageId">The package id.</param>
/// <returns>The package or null.</returns>
PackageCatalogItem GetPackage(string packageId);

/// <summary>
/// Validates a package file.
/// </summary>
/// <param name="packageFile">The package file path.</param>
/// <param name="maxPackageBytes">Optional max allowed package size in bytes. 0 disables the limit check.</param>
/// <param name="expectedSha256">Optional expected SHA-256 hash in hex format.</param>
/// <returns>The validation result.</returns>
PackageValidationResult ValidatePackage(string packageFile, long maxPackageBytes = 0, string expectedSha256 = null);

/// <summary>
/// Uploads and installs a package from a stream.
/// </summary>
/// <param name="packageStream">The package stream.</param>
/// <param name="fileName">The package file name.</param>
/// <param name="activate">True to activate directly after install; false to keep it disabled.</param>
/// <param name="maxPackageBytes">Optional max allowed package size in bytes. 0 disables the limit check.</param>
/// <param name="expectedSha256">Optional expected SHA-256 hash in hex format.</param>
/// <returns>The operation result.</returns>
PackageOperationResult UploadPackage(Stream packageStream, string fileName, bool activate = true, long maxPackageBytes = 0, string expectedSha256 = null);

/// <summary>
/// Installs a package from a file.
/// </summary>
/// <param name="packageFile">The package file path.</param>
/// <param name="activate">True to activate directly after install; false to keep it disabled.</param>
/// <param name="maxPackageBytes">Optional max allowed package size in bytes. 0 disables the limit check.</param>
/// <param name="expectedSha256">Optional expected SHA-256 hash in hex format.</param>
/// <returns>The operation result.</returns>
PackageOperationResult InstallPackage(string packageFile, bool activate = true, long maxPackageBytes = 0, string expectedSha256 = null);

/// <summary>
/// Activates a package.
/// </summary>
/// <param name="packageId">The package id.</param>
/// <returns>The operation result.</returns>
PackageOperationResult ActivatePackage(string packageId);

/// <summary>
/// Deactivates a package.
/// </summary>
/// <param name="packageId">The package id.</param>
/// <returns>The operation result.</returns>
PackageOperationResult DeactivatePackage(string packageId);

/// <summary>
/// Updates a package from a file path.
/// </summary>
/// <param name="packageId">The package id.</param>
/// <param name="packageFile">The package file path.</param>
/// <param name="activate">True to activate directly after update; false to keep it disabled.</param>
/// <param name="maxPackageBytes">Optional max allowed package size in bytes. 0 disables the limit check.</param>
/// <param name="expectedSha256">Optional expected SHA-256 hash in hex format.</param>
/// <returns>The operation result.</returns>
PackageOperationResult UpdatePackage(string packageId, string packageFile, bool activate = true, long maxPackageBytes = 0, string expectedSha256 = null);

/// <summary>
/// Uninstalls and removes a package.
/// </summary>
/// <param name="packageId">The package id.</param>
/// <returns>The operation result.</returns>
PackageOperationResult UninstallPackage(string packageId);
}
}
5 changes: 5 additions & 0 deletions src/WebExpress.WebCore/WebPackage/Model/PackageItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ public class PackageItem
/// </summary>
public IEnumerable<string> PluginSources { get; set; }

/// <summary>
/// Gets or sets the package dependencies.
/// </summary>
public IEnumerable<string> Dependencies { get; set; }

/// <summary>
/// Initializes a new instance of the class.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions src/WebExpress.WebCore/WebPackage/Model/PackageItemSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ public class PackageItemSpec
[XmlElement("plugin", IsNullable = true)]
public string[] Plugins { get; set; }

/// <summary>
/// Gets or sets the package dependencies.
/// </summary>
[XmlElement("dependency", IsNullable = true)]
public string[] Dependencies { get; set; }

/// <summary>
/// Gets or sets the artifacts.
/// </summary>
Expand Down
Loading
Loading