stabilizaiton work - projects rework for maintenanceability and ui livening

This commit is contained in:
master
2026-02-03 23:40:04 +02:00
parent 074ce117ba
commit 557feefdc3
3305 changed files with 186813 additions and 107843 deletions

View File

@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.DependencyInjection;
using StellaOps.Plugin.Hosting;
namespace StellaOps.Plugin.Tests.DependencyInjection;
public sealed partial class PluginDependencyInjectionExtensionsTests
{
private sealed class TestPluginAssembly : IDisposable
{
private TestPluginAssembly(string directoryPath, string assemblyPath)
{
DirectoryPath = directoryPath;
AssemblyPath = assemblyPath;
Options = new PluginHostOptions
{
PluginsDirectory = directoryPath,
EnsureDirectoryExists = false,
RecursiveSearch = false,
};
Options.SearchPatterns.Add(Path.GetFileName(assemblyPath));
}
public string DirectoryPath { get; }
public string AssemblyPath { get; }
public PluginHostOptions Options { get; }
public static TestPluginAssembly Create(string source)
{
var directoryPath = Path.Combine(Path.GetTempPath(), "stellaops-plugin-tests-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directoryPath);
var assemblyName = "SamplePlugin" + Guid.NewGuid().ToString("N");
var assemblyPath = Path.Combine(directoryPath, assemblyName + ".dll");
var syntaxTree = CSharpSyntaxTree.ParseText(source);
var references = CollectMetadataReferences();
var compilation = CSharpCompilation.Create(
assemblyName,
new[] { syntaxTree },
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release));
var emitResult = compilation.Emit(assemblyPath);
if (!emitResult.Success)
{
var diagnostics = string.Join(Environment.NewLine, emitResult.Diagnostics);
throw new InvalidOperationException("Failed to compile plugin assembly:" + Environment.NewLine + diagnostics);
}
return new TestPluginAssembly(directoryPath, assemblyPath);
}
public void Dispose()
{
try
{
if (Directory.Exists(DirectoryPath))
{
Directory.Delete(DirectoryPath, recursive: true);
}
}
catch
{
// Ignore cleanup failures - plugin load contexts may keep files locked on Windows.
}
}
private static IReadOnlyCollection<MetadataReference> CollectMetadataReferences()
{
var referencePaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string tpa)
{
foreach (var path in tpa.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
{
referencePaths.Add(path);
}
}
referencePaths.Add(typeof(object).Assembly.Location);
referencePaths.Add(typeof(ServiceBindingAttribute).Assembly.Location);
referencePaths.Add(typeof(IDependencyInjectionRoutine).Assembly.Location);
referencePaths.Add(typeof(IServiceCollection).Assembly.Location);
referencePaths.Add(typeof(IConfiguration).Assembly.Location);
return referencePaths
.Select(path => MetadataReference.CreateFromFile(path))
.ToArray();
}
}
}

View File

@@ -1,25 +1,20 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.DependencyInjection;
using StellaOps.Plugin.DependencyInjection;
using StellaOps.Plugin.Hosting;
using Xunit;
namespace StellaOps.Plugin.Tests.DependencyInjection;
public sealed class PluginDependencyInjectionExtensionsTests
public sealed partial class PluginDependencyInjectionExtensionsTests
{
[Fact]
public void RegisterPluginRoutines_RegistersServiceBindingsAndHonoursLifetimes()
public void RegisterPluginRoutines_RegistersBindingsAndRoutines()
{
const string source = """
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.DependencyInjection;
@@ -27,12 +22,24 @@ namespace SamplePlugin;
public interface IScopedExample {}
public interface ISingletonExample {}
public interface IRoutineMarker {}
[ServiceBinding(typeof(IScopedExample), ServiceLifetime.Scoped, RegisterAsSelf = true)]
public sealed class ScopedExample : IScopedExample {}
[ServiceBinding(typeof(ISingletonExample), ServiceLifetime.Singleton)]
public sealed class SingletonExample : ISingletonExample {}
public sealed class RoutineMarker : IRoutineMarker {}
public sealed class SampleRoutine : IDependencyInjectionRoutine
{
public IServiceCollection Register(IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IRoutineMarker, RoutineMarker>();
return services;
}
}
""";
using var plugin = TestPluginAssembly.Create(source);
@@ -58,26 +65,11 @@ public sealed class SingletonExample : ISingletonExample {}
static d => d.ServiceType.FullName == "SamplePlugin.ISingletonExample");
Assert.Equal(ServiceLifetime.Singleton, singletonDescriptor.Lifetime);
using var provider = services.BuildServiceProvider();
object firstScopeInstance;
using (var scope = provider.CreateScope())
{
var resolvedFirst = ServiceProviderServiceExtensions.GetRequiredService(scope.ServiceProvider, scopedDescriptor.ServiceType);
var resolvedSecond = ServiceProviderServiceExtensions.GetRequiredService(scope.ServiceProvider, scopedDescriptor.ServiceType);
Assert.Same(resolvedFirst, resolvedSecond);
firstScopeInstance = resolvedFirst;
}
using (var scope = provider.CreateScope())
{
var resolved = ServiceProviderServiceExtensions.GetRequiredService(scope.ServiceProvider, scopedDescriptor.ServiceType);
Assert.NotSame(firstScopeInstance, resolved);
}
var singletonFirst = ServiceProviderServiceExtensions.GetRequiredService(provider, singletonDescriptor.ServiceType);
var singletonSecond = ServiceProviderServiceExtensions.GetRequiredService(provider, singletonDescriptor.ServiceType);
Assert.Same(singletonFirst, singletonSecond);
var routineDescriptor = Assert.Single(
services,
static d => d.ServiceType.FullName == "SamplePlugin.IRoutineMarker");
Assert.Equal(ServiceLifetime.Singleton, routineDescriptor.Lifetime);
Assert.Equal("SamplePlugin.RoutineMarker", routineDescriptor.ImplementationType?.FullName);
services.RegisterPluginRoutines(configuration, plugin.Options, NullLogger.Instance);
@@ -86,91 +78,4 @@ public sealed class SingletonExample : ISingletonExample {}
d.ImplementationType?.FullName == "SamplePlugin.ScopedExample");
Assert.Equal(1, scopedRegistrations);
}
private sealed class TestPluginAssembly : IDisposable
{
private TestPluginAssembly(string directoryPath, string assemblyPath)
{
DirectoryPath = directoryPath;
AssemblyPath = assemblyPath;
Options = new PluginHostOptions
{
PluginsDirectory = directoryPath,
EnsureDirectoryExists = false,
RecursiveSearch = false,
};
Options.SearchPatterns.Add(Path.GetFileName(assemblyPath));
}
public string DirectoryPath { get; }
public string AssemblyPath { get; }
public PluginHostOptions Options { get; }
public static TestPluginAssembly Create(string source)
{
var directoryPath = Path.Combine(Path.GetTempPath(), "stellaops-plugin-tests-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directoryPath);
var assemblyName = "SamplePlugin" + Guid.NewGuid().ToString("N");
var assemblyPath = Path.Combine(directoryPath, assemblyName + ".dll");
var syntaxTree = CSharpSyntaxTree.ParseText(source);
var references = CollectMetadataReferences();
var compilation = CSharpCompilation.Create(
assemblyName,
new[] { syntaxTree },
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release));
var emitResult = compilation.Emit(assemblyPath);
if (!emitResult.Success)
{
var diagnostics = string.Join(Environment.NewLine, emitResult.Diagnostics);
throw new InvalidOperationException("Failed to compile plugin assembly:" + Environment.NewLine + diagnostics);
}
return new TestPluginAssembly(directoryPath, assemblyPath);
}
public void Dispose()
{
try
{
if (Directory.Exists(DirectoryPath))
{
Directory.Delete(DirectoryPath, recursive: true);
}
}
catch
{
// Ignore cleanup failures plugin load contexts may keep files locked on Windows.
}
}
private static IReadOnlyCollection<MetadataReference> CollectMetadataReferences()
{
var referencePaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string tpa)
{
foreach (var path in tpa.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
{
referencePaths.Add(path);
}
}
referencePaths.Add(typeof(object).Assembly.Location);
referencePaths.Add(typeof(ServiceBindingAttribute).Assembly.Location);
referencePaths.Add(typeof(IDependencyInjectionRoutine).Assembly.Location);
referencePaths.Add(typeof(ServiceLifetime).Assembly.Location);
return referencePaths
.Select(path => MetadataReference.CreateFromFile(path))
.ToArray();
}
}
}

View File

@@ -0,0 +1,47 @@
using Microsoft.Extensions.DependencyInjection;
using StellaOps.DependencyInjection;
namespace StellaOps.Plugin.Tests.DependencyInjection;
public sealed partial class PluginServiceRegistrationTests
{
private interface IScopedService
{
}
private interface ISelfContract
{
}
private interface IReplacementService
{
}
private interface IAnotherService
{
}
private sealed class ExistingReplacementService : IReplacementService
{
}
[ServiceBinding(typeof(IScopedService), ServiceLifetime.Scoped)]
private sealed class ScopedTestService : IScopedService
{
}
[ServiceBinding(typeof(ISelfContract), ServiceLifetime.Singleton, RegisterAsSelf = true)]
private sealed class SelfRegisteringService : ISelfContract
{
}
[ServiceBinding(typeof(IReplacementService), ServiceLifetime.Transient, ReplaceExisting = true)]
private sealed class ReplacementService : IReplacementService
{
}
[ServiceBinding(typeof(IAnotherService), ServiceLifetime.Singleton)]
private sealed class InvalidServiceBinding
{
}
}

View File

@@ -1,13 +1,11 @@
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.DependencyInjection;
using StellaOps.Plugin.DependencyInjection;
using Xunit;
namespace StellaOps.Plugin.Tests.DependencyInjection;
public sealed class PluginServiceRegistrationTests
public sealed partial class PluginServiceRegistrationTests
{
[Fact]
public void RegisterAssemblyMetadata_RegistersScopedDescriptor()
@@ -19,7 +17,9 @@ public sealed class PluginServiceRegistrationTests
typeof(ScopedTestService).Assembly,
NullLogger.Instance);
var descriptor = Assert.Single(services, static d => d.ServiceType == typeof(IScopedService));
var descriptor = Assert.Single(
services,
static d => d.ServiceType == typeof(IScopedService));
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
Assert.Equal(typeof(ScopedTestService), descriptor.ImplementationType);
}
@@ -34,6 +34,10 @@ public sealed class PluginServiceRegistrationTests
typeof(SelfRegisteringService).Assembly,
NullLogger.Instance);
Assert.Contains(services, static d =>
d.ServiceType == typeof(ISelfContract) &&
d.ImplementationType == typeof(SelfRegisteringService));
Assert.Contains(services, static d =>
d.ServiceType == typeof(SelfRegisteringService) &&
d.ImplementationType == typeof(SelfRegisteringService));
@@ -69,44 +73,4 @@ public sealed class PluginServiceRegistrationTests
Assert.DoesNotContain(services, static d => d.ServiceType == typeof(IAnotherService));
}
private interface IScopedService
{
}
private interface ISelfContract
{
}
private interface IReplacementService
{
}
private interface IAnotherService
{
}
private sealed class ExistingReplacementService : IReplacementService
{
}
[ServiceBinding(typeof(IScopedService), ServiceLifetime.Scoped)]
private sealed class ScopedTestService : IScopedService
{
}
[ServiceBinding(typeof(ISelfContract), ServiceLifetime.Singleton, RegisterAsSelf = true)]
private sealed class SelfRegisteringService : ISelfContract
{
}
[ServiceBinding(typeof(IReplacementService), ServiceLifetime.Transient, ReplaceExisting = true)]
private sealed class ReplacementService : IReplacementService
{
}
[ServiceBinding(typeof(IAnotherService), ServiceLifetime.Singleton)]
private sealed class InvalidServiceBinding
{
}
}

View File

@@ -5,29 +5,8 @@ using Xunit;
namespace StellaOps.Plugin.Tests.Hosting;
public sealed class PluginHostOptionsTests
public sealed partial class PluginHostOptionsTests
{
[Fact]
public void DefaultValues_AreCorrect()
{
var options = new PluginHostOptions();
Assert.Null(options.BaseDirectory);
Assert.Null(options.PluginsDirectory);
Assert.Null(options.PrimaryPrefix);
Assert.Empty(options.AdditionalPrefixes);
Assert.Empty(options.PluginOrder);
Assert.Empty(options.SearchPatterns);
Assert.True(options.EnsureDirectoryExists);
Assert.True(options.RecursiveSearch);
Assert.Null(options.HostVersion);
Assert.True(options.EnforceVersionCompatibility);
Assert.True(options.RequireVersionAttribute);
Assert.True(options.StrictMajorVersionCheck);
Assert.Null(options.SignatureVerifier);
Assert.False(options.EnforceSignatureVerification);
}
[Fact]
public void HostVersion_CanBeSet()
{
@@ -106,19 +85,4 @@ public sealed class PluginHostOptionsTests
Assert.False(options.StrictMajorVersionCheck);
}
[Fact]
public void ProductionConfiguration_HasSecureDefaults()
{
// Verify that out-of-the-box defaults are secure for production
var options = new PluginHostOptions();
// All enforcement options should default to strict/true
Assert.True(options.EnforceVersionCompatibility, "Version compatibility should be enforced by default");
Assert.True(options.RequireVersionAttribute, "Version attribute should be required by default");
Assert.True(options.StrictMajorVersionCheck, "Strict major version check should be enabled by default");
// Signature verification is opt-in since it requires infrastructure
Assert.False(options.EnforceSignatureVerification, "Signature verification is opt-in");
}
}

View File

@@ -0,0 +1,39 @@
using StellaOps.Plugin.Hosting;
using Xunit;
namespace StellaOps.Plugin.Tests.Hosting;
public sealed partial class PluginHostOptionsTests
{
[Fact]
public void DefaultValues_AreCorrect()
{
var options = new PluginHostOptions();
Assert.Null(options.BaseDirectory);
Assert.Null(options.PluginsDirectory);
Assert.Null(options.PrimaryPrefix);
Assert.Empty(options.AdditionalPrefixes);
Assert.Empty(options.PluginOrder);
Assert.Empty(options.SearchPatterns);
Assert.True(options.EnsureDirectoryExists);
Assert.True(options.RecursiveSearch);
Assert.Null(options.HostVersion);
Assert.True(options.EnforceVersionCompatibility);
Assert.True(options.RequireVersionAttribute);
Assert.True(options.StrictMajorVersionCheck);
Assert.Null(options.SignatureVerifier);
Assert.False(options.EnforceSignatureVerification);
}
[Fact]
public void ProductionConfiguration_HasSecureDefaults()
{
var options = new PluginHostOptions();
Assert.True(options.EnforceVersionCompatibility, "Version compatibility should be enforced by default");
Assert.True(options.RequireVersionAttribute, "Version attribute should be required by default");
Assert.True(options.StrictMajorVersionCheck, "Strict major version check should be enabled by default");
Assert.False(options.EnforceSignatureVerification, "Signature verification is opt-in");
}
}

View File

@@ -0,0 +1,63 @@
using System;
using StellaOps.Plugin.Versioning;
using Xunit;
namespace StellaOps.Plugin.Tests;
public sealed partial class PluginCompatibilityCheckerTests
{
[Trait("Category", "Unit")]
[Fact]
public void CheckCompatibility_AssemblyWithoutAttribute_Lenient_ReturnsCompatible()
{
var assembly = typeof(PluginCompatibilityCheckerTests).Assembly;
var hostVersion = new Version(1, 0, 0);
var result = PluginCompatibilityChecker.CheckCompatibility(
assembly, hostVersion, CompatibilityCheckOptions.Lenient);
Assert.True(result.IsCompatible);
Assert.False(result.HasVersionAttribute);
Assert.Null(result.PluginVersion);
}
[Trait("Category", "Unit")]
[Fact]
public void CheckCompatibility_AssemblyWithoutAttribute_RequireVersion_ReturnsIncompatible()
{
var assembly = typeof(PluginCompatibilityCheckerTests).Assembly;
var hostVersion = new Version(1, 0, 0);
var options = new CompatibilityCheckOptions
{
RequireVersionAttribute = true
};
var result = PluginCompatibilityChecker.CheckCompatibility(assembly, hostVersion, options);
Assert.False(result.IsCompatible);
Assert.False(result.HasVersionAttribute);
Assert.NotNull(result.FailureReason);
Assert.Contains("StellaPluginVersion", result.FailureReason);
}
[Trait("Category", "Unit")]
[Fact]
public void CheckCompatibility_NullAssembly_ThrowsException()
{
var hostVersion = new Version(1, 0, 0);
Assert.Throws<ArgumentNullException>(
() => PluginCompatibilityChecker.CheckCompatibility(null!, hostVersion));
}
[Trait("Category", "Unit")]
[Fact]
public void CheckCompatibility_NullHostVersion_ThrowsException()
{
var assembly = typeof(PluginCompatibilityCheckerTests).Assembly;
Assert.Throws<ArgumentNullException>(
() => PluginCompatibilityChecker.CheckCompatibility(assembly, null!));
}
}

View File

@@ -0,0 +1,41 @@
using StellaOps.Plugin.Versioning;
using Xunit;
namespace StellaOps.Plugin.Tests;
public sealed partial class PluginCompatibilityCheckerTests
{
[Trait("Category", "Unit")]
[Fact]
public void DefaultOptions_HasCorrectValues()
{
var options = CompatibilityCheckOptions.Default;
Assert.True(options.RequireVersionAttribute);
Assert.True(options.StrictMajorVersionCheck);
}
[Trait("Category", "Unit")]
[Fact]
public void LenientOptions_HasCorrectValues()
{
var options = CompatibilityCheckOptions.Lenient;
Assert.False(options.RequireVersionAttribute);
Assert.False(options.StrictMajorVersionCheck);
}
[Trait("Category", "Unit")]
[Fact]
public void CustomOptions_CanBeConfigured()
{
var options = new CompatibilityCheckOptions
{
RequireVersionAttribute = true,
StrictMajorVersionCheck = false
};
Assert.True(options.RequireVersionAttribute);
Assert.False(options.StrictMajorVersionCheck);
}
}

View File

@@ -0,0 +1,60 @@
using System;
using StellaOps.Plugin.Versioning;
using Xunit;
namespace StellaOps.Plugin.Tests;
public sealed partial class PluginCompatibilityCheckerTests
{
[Trait("Category", "Unit")]
[Fact]
public void PluginCompatibilityResult_RecordEquality()
{
var result1 = new PluginCompatibilityResult(
IsCompatible: true,
PluginVersion: new Version(1, 0, 0),
MinimumHostVersion: new Version(1, 0, 0),
MaximumHostVersion: new Version(2, 0, 0),
RequiresSignature: true,
FailureReason: null,
HasVersionAttribute: true);
var result2 = new PluginCompatibilityResult(
IsCompatible: true,
PluginVersion: new Version(1, 0, 0),
MinimumHostVersion: new Version(1, 0, 0),
MaximumHostVersion: new Version(2, 0, 0),
RequiresSignature: true,
FailureReason: null,
HasVersionAttribute: true);
Assert.Equal(result1, result2);
}
[Trait("Category", "Unit")]
[Fact]
public void PluginCompatibilityResult_PropertiesAreSet()
{
var pluginVersion = new Version(1, 2, 3);
var minVersion = new Version(1, 0, 0);
var maxVersion = new Version(2, 0, 0);
var failureReason = "Test failure";
var result = new PluginCompatibilityResult(
IsCompatible: false,
PluginVersion: pluginVersion,
MinimumHostVersion: minVersion,
MaximumHostVersion: maxVersion,
RequiresSignature: true,
FailureReason: failureReason,
HasVersionAttribute: true);
Assert.False(result.IsCompatible);
Assert.Equal(pluginVersion, result.PluginVersion);
Assert.Equal(minVersion, result.MinimumHostVersion);
Assert.Equal(maxVersion, result.MaximumHostVersion);
Assert.True(result.RequiresSignature);
Assert.Equal(failureReason, result.FailureReason);
Assert.True(result.HasVersionAttribute);
}
}

View File

@@ -1,183 +0,0 @@
using System.Reflection;
using StellaOps.Plugin.Versioning;
namespace StellaOps.Plugin.Tests;
/// <summary>
/// Unit tests for PluginCompatibilityChecker.
/// </summary>
public sealed class PluginCompatibilityCheckerTests
{
#region Basic Compatibility Tests
[Trait("Category", "Unit")]
[Fact]
public void CheckCompatibility_AssemblyWithoutAttribute_Lenient_ReturnsCompatible()
{
// Arrange - Use current test assembly which doesn't have StellaPluginVersionAttribute
var assembly = typeof(PluginCompatibilityCheckerTests).Assembly;
var hostVersion = new Version(1, 0, 0);
// Act
var result = PluginCompatibilityChecker.CheckCompatibility(
assembly, hostVersion, CompatibilityCheckOptions.Lenient);
// Assert
Assert.True(result.IsCompatible);
Assert.False(result.HasVersionAttribute);
Assert.Null(result.PluginVersion);
}
[Trait("Category", "Unit")]
[Fact]
public void CheckCompatibility_AssemblyWithoutAttribute_RequireVersion_ReturnsIncompatible()
{
// Arrange
var assembly = typeof(PluginCompatibilityCheckerTests).Assembly;
var hostVersion = new Version(1, 0, 0);
var options = new CompatibilityCheckOptions
{
RequireVersionAttribute = true
};
// Act
var result = PluginCompatibilityChecker.CheckCompatibility(assembly, hostVersion, options);
// Assert
Assert.False(result.IsCompatible);
Assert.False(result.HasVersionAttribute);
Assert.NotNull(result.FailureReason);
Assert.Contains("StellaPluginVersion", result.FailureReason);
}
[Trait("Category", "Unit")]
[Fact]
public void CheckCompatibility_NullAssembly_ThrowsException()
{
// Arrange
var hostVersion = new Version(1, 0, 0);
// Act & Assert
Assert.Throws<ArgumentNullException>(
() => PluginCompatibilityChecker.CheckCompatibility(null!, hostVersion));
}
[Trait("Category", "Unit")]
[Fact]
public void CheckCompatibility_NullHostVersion_ThrowsException()
{
// Arrange
var assembly = typeof(PluginCompatibilityCheckerTests).Assembly;
// Act & Assert
Assert.Throws<ArgumentNullException>(
() => PluginCompatibilityChecker.CheckCompatibility(assembly, null!));
}
#endregion
#region CompatibilityCheckOptions Tests
[Trait("Category", "Unit")]
[Fact]
public void DefaultOptions_HasCorrectValues()
{
// Act
var options = CompatibilityCheckOptions.Default;
// Assert
Assert.True(options.RequireVersionAttribute);
Assert.True(options.StrictMajorVersionCheck);
}
[Trait("Category", "Unit")]
[Fact]
public void LenientOptions_HasCorrectValues()
{
// Act
var options = CompatibilityCheckOptions.Lenient;
// Assert
Assert.False(options.RequireVersionAttribute);
Assert.False(options.StrictMajorVersionCheck);
}
[Trait("Category", "Unit")]
[Fact]
public void CustomOptions_CanBeConfigured()
{
// Arrange & Act
var options = new CompatibilityCheckOptions
{
RequireVersionAttribute = true,
StrictMajorVersionCheck = false
};
// Assert
Assert.True(options.RequireVersionAttribute);
Assert.False(options.StrictMajorVersionCheck);
}
#endregion
#region PluginCompatibilityResult Tests
[Trait("Category", "Unit")]
[Fact]
public void PluginCompatibilityResult_RecordEquality()
{
// Arrange
var result1 = new PluginCompatibilityResult(
IsCompatible: true,
PluginVersion: new Version(1, 0, 0),
MinimumHostVersion: new Version(1, 0, 0),
MaximumHostVersion: new Version(2, 0, 0),
RequiresSignature: true,
FailureReason: null,
HasVersionAttribute: true);
var result2 = new PluginCompatibilityResult(
IsCompatible: true,
PluginVersion: new Version(1, 0, 0),
MinimumHostVersion: new Version(1, 0, 0),
MaximumHostVersion: new Version(2, 0, 0),
RequiresSignature: true,
FailureReason: null,
HasVersionAttribute: true);
// Assert
Assert.Equal(result1, result2);
}
[Trait("Category", "Unit")]
[Fact]
public void PluginCompatibilityResult_PropertiesAreSet()
{
// Arrange
var pluginVersion = new Version(1, 2, 3);
var minVersion = new Version(1, 0, 0);
var maxVersion = new Version(2, 0, 0);
var failureReason = "Test failure";
// Act
var result = new PluginCompatibilityResult(
IsCompatible: false,
PluginVersion: pluginVersion,
MinimumHostVersion: minVersion,
MaximumHostVersion: maxVersion,
RequiresSignature: true,
FailureReason: failureReason,
HasVersionAttribute: true);
// Assert
Assert.False(result.IsCompatible);
Assert.Equal(pluginVersion, result.PluginVersion);
Assert.Equal(minVersion, result.MinimumHostVersion);
Assert.Equal(maxVersion, result.MaximumHostVersion);
Assert.True(result.RequiresSignature);
Assert.Equal(failureReason, result.FailureReason);
Assert.True(result.HasVersionAttribute);
}
#endregion
}

View File

@@ -0,0 +1,59 @@
using StellaOps.Plugin.Hosting;
using Xunit;
namespace StellaOps.Plugin.Tests;
public sealed partial class PluginHostOptionsTests
{
[Trait("Category", "Unit")]
[Fact]
public void DefaultValues_AreCorrect()
{
var options = new PluginHostOptions();
Assert.True(options.EnsureDirectoryExists);
Assert.True(options.RecursiveSearch);
Assert.False(options.EnforceSignatureVerification);
Assert.True(options.RequireVersionAttribute);
Assert.True(options.EnforceVersionCompatibility);
Assert.True(options.StrictMajorVersionCheck);
Assert.Null(options.HostVersion);
Assert.Null(options.SignatureVerifier);
Assert.Empty(options.SearchPatterns);
Assert.Empty(options.PluginOrder);
Assert.Empty(options.AdditionalPrefixes);
Assert.Null(options.BaseDirectory);
Assert.Null(options.PluginsDirectory);
Assert.Null(options.PrimaryPrefix);
}
[Trait("Category", "Unit")]
[Fact]
public void SearchPatterns_CanBeAdded()
{
var options = new PluginHostOptions();
options.SearchPatterns.Add("MyPrefix.Plugin.*.dll");
options.SearchPatterns.Add("Another.Plugin.*.dll");
Assert.Equal(2, options.SearchPatterns.Count);
Assert.Contains("MyPrefix.Plugin.*.dll", options.SearchPatterns);
Assert.Contains("Another.Plugin.*.dll", options.SearchPatterns);
}
[Trait("Category", "Unit")]
[Fact]
public void PluginOrder_CanBeConfigured()
{
var options = new PluginHostOptions();
options.PluginOrder.Add("PluginA");
options.PluginOrder.Add("PluginB");
options.PluginOrder.Add("PluginC");
Assert.Equal(3, options.PluginOrder.Count);
Assert.Equal("PluginA", options.PluginOrder[0]);
Assert.Equal("PluginB", options.PluginOrder[1]);
Assert.Equal("PluginC", options.PluginOrder[2]);
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.IO;
using StellaOps.Plugin.Hosting;
using Xunit;
namespace StellaOps.Plugin.Tests;
public sealed partial class PluginHostOptionsTests
{
[Trait("Category", "Unit")]
[Fact]
public void ResolveBaseDirectory_WhenNull_ReturnsAppContextBaseDirectory()
{
var options = new PluginHostOptions
{
BaseDirectory = null
};
var resolved = options.ResolveBaseDirectory();
Assert.Equal(AppContext.BaseDirectory, resolved);
}
[Trait("Category", "Unit")]
[Fact]
public void ResolveBaseDirectory_WhenSet_ReturnsConfiguredValue()
{
var customDir = Path.GetTempPath();
var options = new PluginHostOptions
{
BaseDirectory = customDir
};
var resolved = options.ResolveBaseDirectory();
Assert.Equal(customDir, resolved);
}
[Trait("Category", "Unit")]
[Fact]
public void ResolveBaseDirectory_WhenRelativePath_ReturnsFullPath()
{
var relativePath = Path.Combine("relative", "plugins");
var options = new PluginHostOptions
{
BaseDirectory = relativePath
};
var resolved = options.ResolveBaseDirectory();
Assert.True(Path.IsPathRooted(resolved));
Assert.Equal(Path.GetFullPath(relativePath), resolved);
}
}

View File

@@ -0,0 +1,53 @@
using StellaOps.Plugin.Hosting;
using Xunit;
namespace StellaOps.Plugin.Tests;
public sealed partial class PluginHostOptionsTests
{
[Trait("Category", "Unit")]
[Fact]
public void PrimaryPrefix_AffectsDefaultDirectory()
{
var options = new PluginHostOptions
{
PrimaryPrefix = "MyModule"
};
Assert.Equal("MyModule", options.PrimaryPrefix);
}
[Trait("Category", "Unit")]
[Fact]
public void AdditionalPrefixes_CanBeConfigured()
{
var options = new PluginHostOptions();
options.AdditionalPrefixes.Add("Prefix1");
options.AdditionalPrefixes.Add("Prefix2");
Assert.Equal(2, options.AdditionalPrefixes.Count);
}
[Trait("Category", "Unit")]
[Fact]
public void SignatureVerification_CanBeEnforced()
{
var options = new PluginHostOptions();
options.EnforceSignatureVerification = true;
Assert.True(options.EnforceSignatureVerification);
}
[Trait("Category", "Unit")]
[Fact]
public void RecursiveSearch_CanBeEnabled()
{
var options = new PluginHostOptions();
options.RecursiveSearch = true;
Assert.True(options.RecursiveSearch);
}
}

View File

@@ -0,0 +1,38 @@
using System;
using StellaOps.Plugin.Hosting;
using Xunit;
namespace StellaOps.Plugin.Tests;
public sealed partial class PluginHostOptionsTests
{
[Trait("Category", "Unit")]
[Fact]
public void HostVersion_CanBeConfigured()
{
var options = new PluginHostOptions();
options.HostVersion = new Version(2, 1, 0);
Assert.NotNull(options.HostVersion);
Assert.Equal(2, options.HostVersion.Major);
Assert.Equal(1, options.HostVersion.Minor);
Assert.Equal(0, options.HostVersion.Build);
}
[Trait("Category", "Unit")]
[Fact]
public void VersionCompatibility_CanBeEnforced()
{
var options = new PluginHostOptions();
options.HostVersion = new Version(1, 0, 0);
options.RequireVersionAttribute = true;
options.EnforceVersionCompatibility = true;
options.StrictMajorVersionCheck = true;
Assert.True(options.RequireVersionAttribute);
Assert.True(options.EnforceVersionCompatibility);
Assert.True(options.StrictMajorVersionCheck);
}
}

View File

@@ -1,229 +0,0 @@
using StellaOps.Plugin.Hosting;
namespace StellaOps.Plugin.Tests;
/// <summary>
/// Unit tests for PluginHostOptions configuration.
/// </summary>
public sealed class PluginHostOptionsTests
{
#region Default Values Tests
[Trait("Category", "Unit")]
[Fact]
public void DefaultValues_AreCorrect()
{
// Arrange & Act
var options = new PluginHostOptions();
// Assert
Assert.True(options.EnsureDirectoryExists);
Assert.True(options.RecursiveSearch); // Default is true
Assert.False(options.EnforceSignatureVerification);
Assert.True(options.RequireVersionAttribute); // Default is true
Assert.True(options.EnforceVersionCompatibility); // Default is true
Assert.True(options.StrictMajorVersionCheck); // Default is true
Assert.Null(options.HostVersion);
Assert.Null(options.SignatureVerifier);
Assert.Empty(options.SearchPatterns);
Assert.Empty(options.PluginOrder);
Assert.Empty(options.AdditionalPrefixes);
Assert.Null(options.BaseDirectory);
Assert.Null(options.PluginsDirectory);
Assert.Null(options.PrimaryPrefix);
}
#endregion
#region Search Pattern Tests
[Trait("Category", "Unit")]
[Fact]
public void SearchPatterns_CanBeAdded()
{
// Arrange
var options = new PluginHostOptions();
// Act
options.SearchPatterns.Add("MyPrefix.Plugin.*.dll");
options.SearchPatterns.Add("Another.Plugin.*.dll");
// Assert
Assert.Equal(2, options.SearchPatterns.Count);
Assert.Contains("MyPrefix.Plugin.*.dll", options.SearchPatterns);
Assert.Contains("Another.Plugin.*.dll", options.SearchPatterns);
}
#endregion
#region Plugin Order Tests
[Trait("Category", "Unit")]
[Fact]
public void PluginOrder_CanBeConfigured()
{
// Arrange
var options = new PluginHostOptions();
// Act
options.PluginOrder.Add("PluginA");
options.PluginOrder.Add("PluginB");
options.PluginOrder.Add("PluginC");
// Assert
Assert.Equal(3, options.PluginOrder.Count);
Assert.Equal("PluginA", options.PluginOrder[0]);
Assert.Equal("PluginB", options.PluginOrder[1]);
Assert.Equal("PluginC", options.PluginOrder[2]);
}
#endregion
#region BaseDirectory Resolution Tests
[Trait("Category", "Unit")]
[Fact]
public void ResolveBaseDirectory_WhenNull_ReturnsAppContextBaseDirectory()
{
// Arrange
var options = new PluginHostOptions
{
BaseDirectory = null
};
// Act
var resolved = options.ResolveBaseDirectory();
// Assert
Assert.Equal(AppContext.BaseDirectory, resolved);
}
[Trait("Category", "Unit")]
[Fact]
public void ResolveBaseDirectory_WhenSet_ReturnsConfiguredValue()
{
// Arrange
var customDir = Path.GetTempPath();
var options = new PluginHostOptions
{
BaseDirectory = customDir
};
// Act
var resolved = options.ResolveBaseDirectory();
// Assert
Assert.Equal(customDir, resolved);
}
#endregion
#region Version Configuration Tests
[Trait("Category", "Unit")]
[Fact]
public void HostVersion_CanBeConfigured()
{
// Arrange
var options = new PluginHostOptions();
// Act
options.HostVersion = new Version(2, 1, 0);
// Assert
Assert.NotNull(options.HostVersion);
Assert.Equal(2, options.HostVersion.Major);
Assert.Equal(1, options.HostVersion.Minor);
Assert.Equal(0, options.HostVersion.Build);
}
[Trait("Category", "Unit")]
[Fact]
public void VersionCompatibility_CanBeEnforced()
{
// Arrange
var options = new PluginHostOptions();
// Act
options.HostVersion = new Version(1, 0, 0);
options.RequireVersionAttribute = true;
options.EnforceVersionCompatibility = true;
options.StrictMajorVersionCheck = true;
// Assert
Assert.True(options.RequireVersionAttribute);
Assert.True(options.EnforceVersionCompatibility);
Assert.True(options.StrictMajorVersionCheck);
}
#endregion
#region Prefix Configuration Tests
[Trait("Category", "Unit")]
[Fact]
public void PrimaryPrefix_AffectsDefaultDirectory()
{
// Arrange
var options = new PluginHostOptions
{
PrimaryPrefix = "MyModule"
};
// Assert
Assert.Equal("MyModule", options.PrimaryPrefix);
}
[Trait("Category", "Unit")]
[Fact]
public void AdditionalPrefixes_CanBeConfigured()
{
// Arrange
var options = new PluginHostOptions();
// Act
options.AdditionalPrefixes.Add("Prefix1");
options.AdditionalPrefixes.Add("Prefix2");
// Assert
Assert.Equal(2, options.AdditionalPrefixes.Count);
}
#endregion
#region Signature Verification Tests
[Trait("Category", "Unit")]
[Fact]
public void SignatureVerification_CanBeEnforced()
{
// Arrange
var options = new PluginHostOptions();
// Act
options.EnforceSignatureVerification = true;
// Assert
Assert.True(options.EnforceSignatureVerification);
}
#endregion
#region Recursive Search Tests
[Trait("Category", "Unit")]
[Fact]
public void RecursiveSearch_CanBeEnabled()
{
// Arrange
var options = new PluginHostOptions();
// Act
options.RecursiveSearch = true;
// Assert
Assert.True(options.RecursiveSearch);
}
#endregion
}

View File

@@ -0,0 +1,54 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Plugin.Hosting;
using Xunit;
namespace StellaOps.Plugin.Tests;
public sealed partial class PluginHostTests
{
[Trait("Category", "Unit")]
[Fact]
public async Task LoadPluginsAsync_NonExistentDirectory_ReturnsEmptyResultAsync()
{
var options = new PluginHostOptions
{
PluginsDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "plugins"),
EnsureDirectoryExists = false
};
var result = await PluginHost.LoadPluginsAsync(options);
Assert.Empty(result.Plugins);
Assert.Empty(result.Failures);
}
[Trait("Category", "Unit")]
[Fact]
public async Task LoadPluginsAsync_WithCancellationToken_AcceptsTokenAsync()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
var options = new PluginHostOptions
{
PluginsDirectory = tempDir,
EnsureDirectoryExists = false
};
using var cts = new CancellationTokenSource();
var result = await PluginHost.LoadPluginsAsync(options, cancellationToken: cts.Token);
Assert.Empty(result.Plugins);
}
finally
{
Directory.Delete(tempDir, true);
}
}
}

View File

@@ -0,0 +1,92 @@
using System;
using System.IO;
using StellaOps.Plugin.Hosting;
using Xunit;
namespace StellaOps.Plugin.Tests;
public sealed partial class PluginHostTests
{
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_NonExistentDirectory_ReturnsEmptyResult()
{
var options = new PluginHostOptions
{
PluginsDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "plugins"),
EnsureDirectoryExists = false
};
var result = PluginHost.LoadPlugins(options);
Assert.Empty(result.Plugins);
Assert.Empty(result.Failures);
}
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_EmptyDirectory_ReturnsEmptyResult()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
var options = new PluginHostOptions
{
PluginsDirectory = tempDir,
EnsureDirectoryExists = false
};
var result = PluginHost.LoadPlugins(options);
Assert.Empty(result.Plugins);
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_EnsureDirectoryExists_CreatesDirectory()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "plugins");
Assert.False(Directory.Exists(tempDir));
try
{
var options = new PluginHostOptions
{
PluginsDirectory = tempDir,
EnsureDirectoryExists = true
};
var result = PluginHost.LoadPlugins(options);
Assert.True(Directory.Exists(tempDir));
Assert.Empty(result.Plugins);
}
finally
{
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, true);
}
var parent = Path.GetDirectoryName(tempDir);
if (parent is not null && Directory.Exists(parent))
{
Directory.Delete(parent, true);
}
}
}
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_NullOptions_ThrowsException()
{
Assert.Throws<ArgumentNullException>(() => PluginHost.LoadPlugins(null!));
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.IO;
using StellaOps.Plugin.Hosting;
using Xunit;
namespace StellaOps.Plugin.Tests;
public sealed partial class PluginHostTests
{
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_WithPluginOrder_AcceptsOrderConfiguration()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
var options = new PluginHostOptions
{
PluginsDirectory = tempDir,
EnsureDirectoryExists = false
};
options.PluginOrder.Add("PluginA");
options.PluginOrder.Add("PluginB");
var result = PluginHost.LoadPlugins(options);
Assert.Empty(result.Plugins);
Assert.NotNull(result.MissingOrderedPlugins);
}
finally
{
Directory.Delete(tempDir, true);
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.IO;
using StellaOps.Plugin.Hosting;
using Xunit;
namespace StellaOps.Plugin.Tests;
public sealed partial class PluginHostTests
{
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_CustomSearchPattern_RespectsPattern()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
File.WriteAllText(Path.Combine(tempDir, "NotAPlugin.dll"), "dummy");
var options = new PluginHostOptions
{
PluginsDirectory = tempDir,
EnsureDirectoryExists = false
};
options.SearchPatterns.Add("MyPrefix.Plugin.*.dll");
var result = PluginHost.LoadPlugins(options);
Assert.Empty(result.Plugins);
}
finally
{
Directory.Delete(tempDir, true);
}
}
}

View File

@@ -1,235 +0,0 @@
using Microsoft.Extensions.Logging;
using StellaOps.Plugin.Hosting;
using StellaOps.Plugin.Versioning;
namespace StellaOps.Plugin.Tests;
/// <summary>
/// Integration tests for PluginHost plugin loading.
/// </summary>
public sealed class PluginHostTests
{
#region LoadPlugins Directory Tests
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_NonExistentDirectory_ReturnsEmptyResult()
{
// Arrange
var options = new PluginHostOptions
{
PluginsDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "plugins"),
EnsureDirectoryExists = false
};
// Act
var result = PluginHost.LoadPlugins(options);
// Assert
Assert.Empty(result.Plugins);
Assert.Empty(result.Failures);
}
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_EmptyDirectory_ReturnsEmptyResult()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
var options = new PluginHostOptions
{
PluginsDirectory = tempDir,
EnsureDirectoryExists = false
};
// Act
var result = PluginHost.LoadPlugins(options);
// Assert
Assert.Empty(result.Plugins);
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_EnsureDirectoryExists_CreatesDirectory()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "plugins");
Assert.False(Directory.Exists(tempDir));
try
{
var options = new PluginHostOptions
{
PluginsDirectory = tempDir,
EnsureDirectoryExists = true
};
// Act
var result = PluginHost.LoadPlugins(options);
// Assert
Assert.True(Directory.Exists(tempDir));
Assert.Empty(result.Plugins);
}
finally
{
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, true);
}
var parent = Path.GetDirectoryName(tempDir);
if (parent is not null && Directory.Exists(parent))
{
Directory.Delete(parent, true);
}
}
}
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_NullOptions_ThrowsException()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => PluginHost.LoadPlugins(null!));
}
#endregion
#region Search Pattern Tests
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_CustomSearchPattern_RespectsPattern()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
// Create a file that won't match the pattern
File.WriteAllText(Path.Combine(tempDir, "NotAPlugin.dll"), "dummy");
var options = new PluginHostOptions
{
PluginsDirectory = tempDir,
EnsureDirectoryExists = false
};
options.SearchPatterns.Add("MyPrefix.Plugin.*.dll");
// Act
var result = PluginHost.LoadPlugins(options);
// Assert
Assert.Empty(result.Plugins);
}
finally
{
Directory.Delete(tempDir, true);
}
}
#endregion
#region Plugin Order Tests
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_WithPluginOrder_AcceptsOrderConfiguration()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
var options = new PluginHostOptions
{
PluginsDirectory = tempDir,
EnsureDirectoryExists = false
};
options.PluginOrder.Add("PluginA");
options.PluginOrder.Add("PluginB");
// Act
var result = PluginHost.LoadPlugins(options);
// Assert - In an empty directory, no plugins are loaded
Assert.Empty(result.Plugins);
Assert.NotNull(result.MissingOrderedPlugins);
// MissingOrderedPlugins only tracks plugins that were in PluginOrder but not found in discovered files
// In an empty directory, no files are discovered, so the list may be empty
}
finally
{
Directory.Delete(tempDir, true);
}
}
#endregion
#region Async Tests
[Trait("Category", "Unit")]
[Fact]
public async Task LoadPluginsAsync_NonExistentDirectory_ReturnsEmptyResult()
{
// Arrange
var options = new PluginHostOptions
{
PluginsDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "plugins"),
EnsureDirectoryExists = false
};
// Act
var result = await PluginHost.LoadPluginsAsync(options);
// Assert
Assert.Empty(result.Plugins);
Assert.Empty(result.Failures);
}
[Trait("Category", "Unit")]
[Fact]
public async Task LoadPluginsAsync_WithCancellationToken_AcceptsToken()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
var options = new PluginHostOptions
{
PluginsDirectory = tempDir,
EnsureDirectoryExists = false
};
using var cts = new CancellationTokenSource();
// Don't cancel - just verify the async method accepts a cancellation token
// Act
var result = await PluginHost.LoadPluginsAsync(options, cancellationToken: cts.Token);
// Assert
Assert.Empty(result.Plugins);
}
finally
{
Directory.Delete(tempDir, true);
}
}
#endregion
}

View File

@@ -7,7 +7,7 @@ namespace StellaOps.Plugin.Tests.Security;
public sealed class NullPluginVerifierTests
{
[Fact]
public async Task VerifyAsync_AlwaysReturnsValid()
public async Task VerifyAsync_AlwaysReturnsValidAsync()
{
var verifier = NullPluginVerifier.Instance;
@@ -30,13 +30,14 @@ public sealed class NullPluginVerifierTests
}
[Fact]
public async Task VerifyAsync_HandlesNullPath()
public async Task VerifyAsync_HandlesNullPathAsync()
{
var verifier = NullPluginVerifier.Instance;
// NullPluginVerifier doesn't validate the path, just returns success
var result = await verifier.VerifyAsync(null!);
Assert.True(result.IsValid);
Assert.Null(result.FailureReason);
Assert.Null(result.SignerIdentity);
}
}

View File

@@ -9,3 +9,6 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0031-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
| AUDIT-0031-A | DONE | Waived (test project; revalidated 2026-01-08). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| REMED-03 | DONE | Tier 0 remediation (usings normalized); dotnet test passed 2026-02-02 (MTP0001 warning). |
| REMED-04 | DONE | Async naming updates; ConfigureAwait(false) skipped in tests per xUnit1030; dotnet test passed 2026-02-02 (MTP0001 warning). |
| REMED-05 | DONE | Files split <= 100 lines; service locator removed; tests enriched; dotnet test passed 2026-02-02 (MTP0001 warning). |

View File

@@ -0,0 +1,83 @@
using System;
using StellaOps.Plugin.Versioning;
using Xunit;
namespace StellaOps.Plugin.Tests.Versioning;
public sealed partial class PluginCompatibilityCheckerTests
{
[Fact]
public void CheckCompatibility_WithAttribute_ReturnsCompatible()
{
var assembly = CreateAssemblyWithVersion(
pluginVersion: "1.2.3",
minimumHostVersion: "1.0.0",
maximumHostVersion: "2.0.0",
requiresSignature: false);
var hostVersion = new Version(1, 5, 0);
var result = PluginCompatibilityChecker.CheckCompatibility(
assembly,
hostVersion,
CompatibilityCheckOptions.Default);
Assert.True(result.IsCompatible);
Assert.True(result.HasVersionAttribute);
Assert.Equal(new Version(1, 2, 3), result.PluginVersion);
Assert.Equal(new Version(1, 0, 0), result.MinimumHostVersion);
Assert.Equal(new Version(2, 0, 0), result.MaximumHostVersion);
Assert.False(result.RequiresSignature);
Assert.Null(result.FailureReason);
}
[Fact]
public void CheckCompatibility_RejectsHostBelowMinimum()
{
var assembly = CreateAssemblyWithVersion(
pluginVersion: "1.0.0",
minimumHostVersion: "2.0.0");
var hostVersion = new Version(1, 0, 0);
var result = PluginCompatibilityChecker.CheckCompatibility(
assembly,
hostVersion,
CompatibilityCheckOptions.Default);
Assert.False(result.IsCompatible);
Assert.Contains("below minimum required version", result.FailureReason);
}
[Fact]
public void CheckCompatibility_RejectsHostAboveMaximum()
{
var assembly = CreateAssemblyWithVersion(
pluginVersion: "1.0.0",
maximumHostVersion: "2.0.0");
var hostVersion = new Version(3, 0, 0);
var result = PluginCompatibilityChecker.CheckCompatibility(
assembly,
hostVersion,
CompatibilityCheckOptions.Default);
Assert.False(result.IsCompatible);
Assert.Contains("exceeds maximum supported version", result.FailureReason);
}
[Fact]
public void CheckCompatibility_StrictMajorVersionRejectsWhenMinimumSpecified()
{
var assembly = CreateAssemblyWithVersion(
pluginVersion: "1.0.0",
minimumHostVersion: "1.0.0");
var hostVersion = new Version(2, 0, 0);
var result = PluginCompatibilityChecker.CheckCompatibility(
assembly,
hostVersion,
CompatibilityCheckOptions.Default);
Assert.False(result.IsCompatible);
Assert.Contains("declared compatibility range", result.FailureReason);
}
}

View File

@@ -0,0 +1,54 @@
using System;
using StellaOps.Plugin.Versioning;
using Xunit;
namespace StellaOps.Plugin.Tests.Versioning;
public sealed partial class PluginCompatibilityCheckerTests
{
[Fact]
public void CheckCompatibility_LenientMode_ReturnsCompatibleForAssemblyWithoutAttribute()
{
var assembly = typeof(PluginCompatibilityCheckerTests).Assembly;
var hostVersion = new Version(1, 0, 0);
var result = PluginCompatibilityChecker.CheckCompatibility(assembly, hostVersion);
Assert.True(result.IsCompatible);
Assert.False(result.HasVersionAttribute);
Assert.Null(result.PluginVersion);
Assert.Null(result.FailureReason);
}
[Fact]
public void CheckCompatibility_StrictMode_RejectsAssemblyWithoutAttribute()
{
var assembly = typeof(PluginCompatibilityCheckerTests).Assembly;
var hostVersion = new Version(1, 0, 0);
var options = CompatibilityCheckOptions.Default;
var result = PluginCompatibilityChecker.CheckCompatibility(assembly, hostVersion, options);
Assert.False(result.IsCompatible);
Assert.False(result.HasVersionAttribute);
Assert.Null(result.PluginVersion);
Assert.NotNull(result.FailureReason);
Assert.Contains("[StellaPluginVersion]", result.FailureReason);
}
[Fact]
public void CheckCompatibility_ThrowsOnNullAssembly()
{
Assert.Throws<ArgumentNullException>(() =>
PluginCompatibilityChecker.CheckCompatibility(null!, new Version(1, 0, 0)));
}
[Fact]
public void CheckCompatibility_ThrowsOnNullHostVersion()
{
var assembly = typeof(PluginCompatibilityCheckerTests).Assembly;
Assert.Throws<ArgumentNullException>(() =>
PluginCompatibilityChecker.CheckCompatibility(assembly, null!));
}
}

View File

@@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading;
using StellaOps.Plugin.Versioning;
namespace StellaOps.Plugin.Tests.Versioning;
public sealed partial class PluginCompatibilityCheckerTests
{
private static int _assemblySequence;
private static Assembly CreateAssemblyWithVersion(
string pluginVersion,
string? minimumHostVersion = null,
string? maximumHostVersion = null,
bool? requiresSignature = null)
{
var assemblyName = new AssemblyName(
$"StellaOps.Plugin.Tests.Dynamic.{Interlocked.Increment(ref _assemblySequence)}");
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name!);
moduleBuilder.DefineType("CompatibilityMarker", TypeAttributes.Public).CreateType();
var ctor = typeof(StellaPluginVersionAttribute).GetConstructor(new[] { typeof(string) })!;
var properties = new List<PropertyInfo>();
var values = new List<object>();
if (minimumHostVersion != null)
{
properties.Add(typeof(StellaPluginVersionAttribute).GetProperty(
nameof(StellaPluginVersionAttribute.MinimumHostVersion))!);
values.Add(minimumHostVersion);
}
if (maximumHostVersion != null)
{
properties.Add(typeof(StellaPluginVersionAttribute).GetProperty(
nameof(StellaPluginVersionAttribute.MaximumHostVersion))!);
values.Add(maximumHostVersion);
}
if (requiresSignature.HasValue)
{
properties.Add(typeof(StellaPluginVersionAttribute).GetProperty(
nameof(StellaPluginVersionAttribute.RequiresSignature))!);
values.Add(requiresSignature.Value);
}
var attributeBuilder = new CustomAttributeBuilder(
ctor,
new object[] { pluginVersion },
properties.ToArray(),
values.ToArray());
assemblyBuilder.SetCustomAttribute(attributeBuilder);
return assemblyBuilder;
}
}

View File

@@ -0,0 +1,38 @@
using StellaOps.Plugin.Versioning;
using Xunit;
namespace StellaOps.Plugin.Tests.Versioning;
public sealed partial class PluginCompatibilityCheckerTests
{
[Fact]
public void CompatibilityCheckOptions_DefaultRequiresVersionAttribute()
{
var options = CompatibilityCheckOptions.Default;
Assert.True(options.RequireVersionAttribute);
Assert.True(options.StrictMajorVersionCheck);
}
[Fact]
public void CompatibilityCheckOptions_LenientDoesNotRequireVersionAttribute()
{
var options = CompatibilityCheckOptions.Lenient;
Assert.False(options.RequireVersionAttribute);
Assert.False(options.StrictMajorVersionCheck);
}
[Fact]
public void CompatibilityCheckOptions_CanBeCustomized()
{
var options = new CompatibilityCheckOptions
{
RequireVersionAttribute = false,
StrictMajorVersionCheck = true
};
Assert.False(options.RequireVersionAttribute);
Assert.True(options.StrictMajorVersionCheck);
}
}

View File

@@ -1,145 +0,0 @@
using System;
using System.Reflection;
using System.Reflection.Emit;
using StellaOps.Plugin.Versioning;
using Xunit;
namespace StellaOps.Plugin.Tests.Versioning;
public sealed class PluginCompatibilityCheckerTests
{
[Fact]
public void CheckCompatibility_LenientMode_ReturnsCompatibleForAssemblyWithoutAttribute()
{
var assembly = typeof(PluginCompatibilityCheckerTests).Assembly;
var hostVersion = new Version(1, 0, 0);
// Default overload uses lenient options
var result = PluginCompatibilityChecker.CheckCompatibility(assembly, hostVersion);
Assert.True(result.IsCompatible);
Assert.False(result.HasVersionAttribute);
Assert.Null(result.PluginVersion);
Assert.Null(result.FailureReason);
}
[Fact]
public void CheckCompatibility_StrictMode_RejectsAssemblyWithoutAttribute()
{
var assembly = typeof(PluginCompatibilityCheckerTests).Assembly;
var hostVersion = new Version(1, 0, 0);
var options = CompatibilityCheckOptions.Default; // Requires version attribute
var result = PluginCompatibilityChecker.CheckCompatibility(assembly, hostVersion, options);
Assert.False(result.IsCompatible);
Assert.False(result.HasVersionAttribute);
Assert.Null(result.PluginVersion);
Assert.NotNull(result.FailureReason);
Assert.Contains("[StellaPluginVersion]", result.FailureReason);
}
[Fact]
public void CheckCompatibility_ThrowsOnNullAssembly()
{
Assert.Throws<ArgumentNullException>(() =>
PluginCompatibilityChecker.CheckCompatibility(null!, new Version(1, 0, 0)));
}
[Fact]
public void CheckCompatibility_ThrowsOnNullHostVersion()
{
var assembly = typeof(PluginCompatibilityCheckerTests).Assembly;
Assert.Throws<ArgumentNullException>(() =>
PluginCompatibilityChecker.CheckCompatibility(assembly, null!));
}
[Theory]
[InlineData("1.0.0", "1.0.0", true)] // host == min → compatible
[InlineData("2.0.0", "1.0.0", true)] // host > min → compatible
[InlineData("1.0.0", "2.0.0", false)] // host < min → NOT compatible
public void CheckCompatibility_ValidatesMinimumHostVersion(
string hostVersionStr,
string minHostVersionStr,
bool expectedCompatible)
{
// This test validates the logic conceptually
// In a real scenario, we'd need a dynamically created assembly with the attribute
var hostVersion = Version.Parse(hostVersionStr);
var minHostVersion = Version.Parse(minHostVersionStr);
// Direct validation of version comparison logic
var isCompatible = hostVersion >= minHostVersion;
Assert.Equal(expectedCompatible, isCompatible);
}
[Theory]
[InlineData("3.0.0", "2.0.0", false)]
[InlineData("2.0.0", "2.0.0", true)]
[InlineData("1.0.0", "2.0.0", true)]
public void CheckCompatibility_ValidatesMaximumHostVersion(
string hostVersionStr,
string maxHostVersionStr,
bool expectedCompatible)
{
var hostVersion = Version.Parse(hostVersionStr);
var maxHostVersion = Version.Parse(maxHostVersionStr);
var isCompatible = hostVersion <= maxHostVersion;
Assert.Equal(expectedCompatible, isCompatible);
}
[Theory]
[InlineData("2.0.0", "1.0.0", false)] // host major 2 > min major 1 → NOT compatible (strict)
[InlineData("1.5.0", "1.0.0", true)] // host major 1 == min major 1 → compatible
[InlineData("1.0.0", "1.0.0", true)] // host == min → compatible
public void StrictMajorVersionCheck_RejectsCrossMajorVersionWhenNoMaxSpecified(
string hostVersionStr,
string minHostVersionStr,
bool expectedCompatible)
{
var hostVersion = Version.Parse(hostVersionStr);
var minHostVersion = Version.Parse(minHostVersionStr);
// With strict major version check, if plugin declares min=1.0.0 but no max,
// and host is 2.x, it should be rejected
var hostMajorExceedsMin = hostVersion.Major > minHostVersion.Major;
var isCompatible = !hostMajorExceedsMin && hostVersion >= minHostVersion;
Assert.Equal(expectedCompatible, isCompatible);
}
[Fact]
public void CompatibilityCheckOptions_DefaultRequiresVersionAttribute()
{
var options = CompatibilityCheckOptions.Default;
Assert.True(options.RequireVersionAttribute);
Assert.True(options.StrictMajorVersionCheck);
}
[Fact]
public void CompatibilityCheckOptions_LenientDoesNotRequireVersionAttribute()
{
var options = CompatibilityCheckOptions.Lenient;
Assert.False(options.RequireVersionAttribute);
Assert.False(options.StrictMajorVersionCheck);
}
[Fact]
public void CompatibilityCheckOptions_CanBeCustomized()
{
var options = new CompatibilityCheckOptions
{
RequireVersionAttribute = false,
StrictMajorVersionCheck = true
};
Assert.False(options.RequireVersionAttribute);
Assert.True(options.StrictMajorVersionCheck);
}
}

View File

@@ -0,0 +1,50 @@
using System;
using StellaOps.Plugin.Versioning;
using Xunit;
namespace StellaOps.Plugin.Tests.Versioning;
public sealed partial class StellaPluginVersionAttributeTests
{
[Fact]
public void Constructor_ParsesValidVersion()
{
var attr = new StellaPluginVersionAttribute("1.2.3");
Assert.Equal(new Version(1, 2, 3), attr.PluginVersion);
}
[Fact]
public void Constructor_ParsesTwoPartVersion()
{
var attr = new StellaPluginVersionAttribute("1.0");
Assert.Equal(new Version(1, 0), attr.PluginVersion);
}
[Fact]
public void Constructor_ParsesFourPartVersion()
{
var attr = new StellaPluginVersionAttribute("1.2.3.4");
Assert.Equal(new Version(1, 2, 3, 4), attr.PluginVersion);
}
[Fact]
public void Constructor_ThrowsOnInvalidVersion()
{
Assert.Throws<ArgumentException>(() => new StellaPluginVersionAttribute("invalid"));
}
[Fact]
public void Constructor_ThrowsOnEmptyVersion()
{
Assert.Throws<ArgumentException>(() => new StellaPluginVersionAttribute(""));
}
[Fact]
public void Constructor_ThrowsOnNullVersion()
{
Assert.Throws<ArgumentException>(() => new StellaPluginVersionAttribute(null!));
}
}

View File

@@ -0,0 +1,38 @@
using System;
using StellaOps.Plugin.Versioning;
using Xunit;
namespace StellaOps.Plugin.Tests.Versioning;
public sealed partial class StellaPluginVersionAttributeTests
{
[Fact]
public void GetMaximumHostVersion_ReturnsNullWhenNotSet()
{
var attr = new StellaPluginVersionAttribute("1.0.0");
Assert.Null(attr.GetMaximumHostVersion());
}
[Fact]
public void GetMaximumHostVersion_ParsesValidVersion()
{
var attr = new StellaPluginVersionAttribute("1.0.0")
{
MaximumHostVersion = "3.0.0"
};
Assert.Equal(new Version(3, 0, 0), attr.GetMaximumHostVersion());
}
[Fact]
public void GetMaximumHostVersion_ThrowsOnInvalidVersion()
{
var attr = new StellaPluginVersionAttribute("1.0.0")
{
MaximumHostVersion = "invalid"
};
Assert.Throws<ArgumentException>(() => attr.GetMaximumHostVersion());
}
}

View File

@@ -0,0 +1,38 @@
using System;
using StellaOps.Plugin.Versioning;
using Xunit;
namespace StellaOps.Plugin.Tests.Versioning;
public sealed partial class StellaPluginVersionAttributeTests
{
[Fact]
public void GetMinimumHostVersion_ReturnsNullWhenNotSet()
{
var attr = new StellaPluginVersionAttribute("1.0.0");
Assert.Null(attr.GetMinimumHostVersion());
}
[Fact]
public void GetMinimumHostVersion_ParsesValidVersion()
{
var attr = new StellaPluginVersionAttribute("1.0.0")
{
MinimumHostVersion = "2.0.0"
};
Assert.Equal(new Version(2, 0, 0), attr.GetMinimumHostVersion());
}
[Fact]
public void GetMinimumHostVersion_ThrowsOnInvalidVersion()
{
var attr = new StellaPluginVersionAttribute("1.0.0")
{
MinimumHostVersion = "invalid"
};
Assert.Throws<ArgumentException>(() => attr.GetMinimumHostVersion());
}
}

View File

@@ -0,0 +1,26 @@
using StellaOps.Plugin.Versioning;
using Xunit;
namespace StellaOps.Plugin.Tests.Versioning;
public sealed partial class StellaPluginVersionAttributeTests
{
[Fact]
public void RequiresSignature_DefaultsToTrue()
{
var attr = new StellaPluginVersionAttribute("1.0.0");
Assert.True(attr.RequiresSignature);
}
[Fact]
public void RequiresSignature_CanBeSetToFalse()
{
var attr = new StellaPluginVersionAttribute("1.0.0")
{
RequiresSignature = false
};
Assert.False(attr.RequiresSignature);
}
}

View File

@@ -1,107 +0,0 @@
using System;
using StellaOps.Plugin.Versioning;
using Xunit;
namespace StellaOps.Plugin.Tests.Versioning;
public sealed class StellaPluginVersionAttributeTests
{
[Fact]
public void Constructor_ParsesValidVersion()
{
var attr = new StellaPluginVersionAttribute("1.2.3");
Assert.Equal(new Version(1, 2, 3), attr.PluginVersion);
}
[Fact]
public void Constructor_ParsesTwoPartVersion()
{
var attr = new StellaPluginVersionAttribute("1.0");
Assert.Equal(new Version(1, 0), attr.PluginVersion);
}
[Fact]
public void Constructor_ParsesFourPartVersion()
{
var attr = new StellaPluginVersionAttribute("1.2.3.4");
Assert.Equal(new Version(1, 2, 3, 4), attr.PluginVersion);
}
[Fact]
public void Constructor_ThrowsOnInvalidVersion()
{
Assert.Throws<ArgumentException>(() => new StellaPluginVersionAttribute("invalid"));
}
[Fact]
public void Constructor_ThrowsOnEmptyVersion()
{
Assert.Throws<ArgumentException>(() => new StellaPluginVersionAttribute(""));
}
[Fact]
public void Constructor_ThrowsOnNullVersion()
{
Assert.Throws<ArgumentException>(() => new StellaPluginVersionAttribute(null!));
}
[Fact]
public void GetMinimumHostVersion_ReturnsNullWhenNotSet()
{
var attr = new StellaPluginVersionAttribute("1.0.0");
Assert.Null(attr.GetMinimumHostVersion());
}
[Fact]
public void GetMinimumHostVersion_ParsesValidVersion()
{
var attr = new StellaPluginVersionAttribute("1.0.0")
{
MinimumHostVersion = "2.0.0"
};
Assert.Equal(new Version(2, 0, 0), attr.GetMinimumHostVersion());
}
[Fact]
public void GetMaximumHostVersion_ReturnsNullWhenNotSet()
{
var attr = new StellaPluginVersionAttribute("1.0.0");
Assert.Null(attr.GetMaximumHostVersion());
}
[Fact]
public void GetMaximumHostVersion_ParsesValidVersion()
{
var attr = new StellaPluginVersionAttribute("1.0.0")
{
MaximumHostVersion = "3.0.0"
};
Assert.Equal(new Version(3, 0, 0), attr.GetMaximumHostVersion());
}
[Fact]
public void RequiresSignature_DefaultsToTrue()
{
var attr = new StellaPluginVersionAttribute("1.0.0");
Assert.True(attr.RequiresSignature);
}
[Fact]
public void RequiresSignature_CanBeSetToFalse()
{
var attr = new StellaPluginVersionAttribute("1.0.0")
{
RequiresSignature = false
};
Assert.False(attr.RequiresSignature);
}
}