consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -0,0 +1,28 @@
# Excititor Plugin Tests Charter
## Mission
Validate plugin catalog behavior and VEX connector registration with deterministic tests.
## Responsibilities
- Maintain `StellaOps.Excititor.Plugin.Tests`.
- Keep tests deterministic and offline-friendly.
- Surface open work on `TASKS.md`; update statuses (TODO/DOING/DONE/BLOCKED/REVIEW).
## Key Paths
- `PluginCatalogTests.cs`
- `VexConnectorRegistrationTests.cs`
## Coordination
- Excititor connector owners.
## Required Reading
- `docs/modules/excititor/architecture.md`
- `docs/modules/excititor/attestation-plan.md`
- `docs/modules/platform/architecture-overview.md`
- `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`
## Working Agreement
- 1. Update task status to `DOING`/`DONE` in both corresponding sprint file and local `TASKS.md`.
- 2. Keep tests deterministic (stable ordering, timestamps, IDs).
- 3. Avoid network in tests; use in-memory or fixtures.
- 4. Log any cross-module edits in the sprint Execution Log.

View File

@@ -0,0 +1,276 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Connectors.RedHat.CSAF;
using StellaOps.Plugin;
using StellaOps.TestKit;
using System.Reflection;
namespace StellaOps.Excititor.Plugin.Tests;
/// <summary>
/// Integration tests for PluginCatalog with VEX connectors.
/// Validates plugin discovery, loading, and availability filtering.
/// </summary>
public sealed class PluginCatalogTests
{
#region Assembly Loading Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddAssembly_RedHatConnector_AddsToAssemblies()
{
// Arrange
var catalog = new PluginCatalog();
var assembly = typeof(RedHatCsafConnector).Assembly;
// Act
catalog.AddAssembly(assembly);
// Assert - Should not throw and should be able to get plugins
var plugins = catalog.GetConnectorPlugins();
// RedHatCsafConnector is not an IConnectorPlugin, it's an IVexConnector
// but the catalog should still accept the assembly
plugins.Should().NotBeNull();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddAssembly_DuplicateAssembly_IsIdempotent()
{
// Arrange
var catalog = new PluginCatalog();
var assembly = typeof(RedHatCsafConnector).Assembly;
// Act
catalog.AddAssembly(assembly);
catalog.AddAssembly(assembly);
catalog.AddAssembly(assembly);
// Assert - Should not duplicate
var plugins = catalog.GetConnectorPlugins();
// Verify no exception is thrown
plugins.Should().NotBeNull();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddAssembly_NullAssembly_ThrowsException()
{
// Arrange
var catalog = new PluginCatalog();
// Act
var act = () => catalog.AddAssembly(null!);
// Assert
act.Should().Throw<ArgumentNullException>();
}
#endregion
#region AddFromDirectory Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddFromDirectory_NonExistentDirectory_HandlesGracefully()
{
// Arrange
var catalog = new PluginCatalog();
var nonExistentDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "plugins");
// Act - Should not throw
catalog.AddFromDirectory(nonExistentDir);
// Assert
var plugins = catalog.GetConnectorPlugins();
plugins.Should().BeEmpty();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddFromDirectory_EmptyDirectory_LoadsNoPlugins()
{
// Arrange
var catalog = new PluginCatalog();
var emptyDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(emptyDir);
try
{
// Act
catalog.AddFromDirectory(emptyDir);
// Assert
var plugins = catalog.GetConnectorPlugins();
plugins.Should().BeEmpty();
}
finally
{
Directory.Delete(emptyDir);
}
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddFromDirectory_NullDirectory_ThrowsException()
{
// Arrange
var catalog = new PluginCatalog();
// Act
var act = () => catalog.AddFromDirectory(null!);
// Assert
act.Should().Throw<ArgumentException>();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddFromDirectory_EmptyStringDirectory_ThrowsException()
{
// Arrange
var catalog = new PluginCatalog();
// Act
var act = () => catalog.AddFromDirectory("");
// Assert
act.Should().Throw<ArgumentException>();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddFromDirectory_WhitespaceDirectory_ThrowsException()
{
// Arrange
var catalog = new PluginCatalog();
// Act
var act = () => catalog.AddFromDirectory(" ");
// Assert
act.Should().Throw<ArgumentException>();
}
#endregion
#region PluginLoader Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void LoadPlugins_WithValidAssemblies_DiscoverTypes()
{
// Arrange
var assemblies = new List<Assembly> { typeof(RedHatCsafConnector).Assembly };
// Act - Discover types that implement IVexConnector
var vexConnectorTypes = assemblies
.SelectMany(a => a.GetTypes())
.Where(t => typeof(IVexConnector).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract)
.ToList();
// Assert - RedHatCsafConnector should be discovered
vexConnectorTypes.Should().Contain(typeof(RedHatCsafConnector));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void LoadPlugins_NullAssemblies_ThrowsException()
{
// Act
var act = () => PluginLoader.LoadPlugins<IVexConnector>(null!);
// Assert
act.Should().Throw<ArgumentNullException>();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void LoadPlugins_EmptyAssemblies_ReturnsEmpty()
{
// Arrange
var assemblies = new List<Assembly>();
// Act
var plugins = PluginLoader.LoadPlugins<IConnectorPlugin>(assemblies);
// Assert
plugins.Should().BeEmpty();
}
#endregion
#region Availability Filter Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetAvailableConnectorPlugins_WithMockProvider_FiltersCorrectly()
{
// Arrange
var catalog = new PluginCatalog();
// Note: The test assembly doesn't have IConnectorPlugin implementations
// This tests the filtering mechanism
var services = new ServiceCollection()
.AddLogging()
.BuildServiceProvider();
// Act
var availablePlugins = catalog.GetAvailableConnectorPlugins(services);
// Assert
availablePlugins.Should().NotBeNull();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetAvailableExporterPlugins_WithMockProvider_FiltersCorrectly()
{
// Arrange
var catalog = new PluginCatalog();
var services = new ServiceCollection()
.AddLogging()
.BuildServiceProvider();
// Act
var availablePlugins = catalog.GetAvailableExporterPlugins(services);
// Assert
availablePlugins.Should().NotBeNull();
}
#endregion
#region Method Chaining Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PluginCatalog_MethodChaining_Works()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
var assembly = typeof(RedHatCsafConnector).Assembly;
// Act
var catalog = new PluginCatalog()
.AddAssembly(assembly)
.AddFromDirectory(tempDir);
// Assert
catalog.Should().NotBeNull();
}
finally
{
Directory.Delete(tempDir);
}
}
#endregion
}

View File

@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<NoWarn>$(NoWarn);CA2255</NoWarn>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>StellaOps.Excititor.Plugin.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Using Include="FluentAssertions" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Excititor.Connectors.Abstractions\StellaOps.Excititor.Connectors.Abstractions.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Excititor.Connectors.RedHat.CSAF\StellaOps.Excititor.Connectors.RedHat.CSAF.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Excititor.Connectors.Cisco.CSAF\StellaOps.Excititor.Connectors.Cisco.CSAF.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Excititor.Connectors.Ubuntu.CSAF\StellaOps.Excititor.Connectors.Ubuntu.CSAF.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Plugin\StellaOps.Plugin.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,10 @@
# Excititor Plugin Tests Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0752-M | DONE | Revalidated 2026-01-07 (test project). |
| AUDIT-0752-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0752-A | DONE | Waived (test project; revalidated 2026-01-07). |

View File

@@ -0,0 +1,249 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Storage;
using StellaOps.Excititor.Connectors.Abstractions;
using StellaOps.Excititor.Connectors.RedHat.CSAF;
using StellaOps.Excititor.Connectors.RedHat.CSAF.DependencyInjection;
using StellaOps.TestKit;
namespace StellaOps.Excititor.Plugin.Tests;
/// <summary>
/// Integration tests for VEX connector service registration.
/// Validates that connector DI extensions correctly register services.
/// </summary>
public sealed class VexConnectorRegistrationTests
{
#region RedHat Connector Registration Tests
[Trait("Category", TestCategories.Integration)]
[Fact]
public void AddRedHatCsafConnector_RegistersIVexConnector()
{
// Arrange
var services = new ServiceCollection();
services.AddLogging();
// Add required dependencies
services.AddSingleton(TimeProvider.System);
services.AddSingleton<IVexConnectorStateRepository, InMemoryVexConnectorStateRepository>();
// Add connector-specific descriptor
services.AddSingleton(new VexConnectorDescriptor(
"excititor:redhat",
VexProviderKind.Vendor,
"Red Hat CSAF"));
// Act
services.AddRedHatCsafConnector();
// Assert
var provider = services.BuildServiceProvider();
var connector = provider.GetService<IVexConnector>();
connector.Should().NotBeNull();
connector.Should().BeOfType<RedHatCsafConnector>();
}
[Trait("Category", TestCategories.Integration)]
[Fact]
public void AddRedHatCsafConnector_WithConfiguration_AppliesOptions()
{
// Arrange
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton(TimeProvider.System);
services.AddSingleton<IVexConnectorStateRepository, InMemoryVexConnectorStateRepository>();
services.AddSingleton(new VexConnectorDescriptor(
"excititor:redhat",
VexProviderKind.Vendor,
"Red Hat CSAF"));
// Act
services.AddRedHatCsafConnector(options =>
{
options.MetadataUri = new Uri("https://custom.redhat.com/csaf/provider-metadata.json");
});
// Assert
var provider = services.BuildServiceProvider();
var options = provider.GetService<Microsoft.Extensions.Options.IOptions<
StellaOps.Excititor.Connectors.RedHat.CSAF.Configuration.RedHatConnectorOptions>>();
options.Should().NotBeNull();
options!.Value.MetadataUri.Should().Be(new Uri("https://custom.redhat.com/csaf/provider-metadata.json"));
}
[Trait("Category", TestCategories.Integration)]
[Fact]
public void AddRedHatCsafConnector_RegistersHttpClient()
{
// Arrange
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton(TimeProvider.System);
services.AddSingleton<IVexConnectorStateRepository, InMemoryVexConnectorStateRepository>();
services.AddSingleton(new VexConnectorDescriptor(
"excititor:redhat",
VexProviderKind.Vendor,
"Red Hat CSAF"));
// Act
services.AddRedHatCsafConnector();
// Assert
var provider = services.BuildServiceProvider();
var httpClientFactory = provider.GetService<IHttpClientFactory>();
httpClientFactory.Should().NotBeNull();
var client = httpClientFactory!.CreateClient(
StellaOps.Excititor.Connectors.RedHat.CSAF.Configuration.RedHatConnectorOptions.HttpClientName);
client.Should().NotBeNull();
client.DefaultRequestHeaders.UserAgent.Should().NotBeEmpty();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddRedHatCsafConnector_NullServices_ThrowsException()
{
// Arrange
IServiceCollection services = null!;
// Act
var act = () => services.AddRedHatCsafConnector();
// Assert
act.Should().Throw<ArgumentNullException>();
}
#endregion
#region Connector Descriptor Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void VexConnectorDescriptor_RequiresId()
{
// Act
var act = () => new VexConnectorDescriptor(null!, VexProviderKind.Vendor, "Display Name");
// Assert
act.Should().Throw<ArgumentException>();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void VexConnectorDescriptor_EmptyId_ThrowsException()
{
// Act
var act = () => new VexConnectorDescriptor("", VexProviderKind.Vendor, "Display Name");
// Assert
act.Should().Throw<ArgumentException>();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void VexConnectorDescriptor_WhitespaceId_ThrowsException()
{
// Act
var act = () => new VexConnectorDescriptor(" ", VexProviderKind.Vendor, "Display Name");
// Assert
act.Should().Throw<ArgumentException>();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void VexConnectorDescriptor_NullDisplayName_UsesId()
{
// Act
var descriptor = new VexConnectorDescriptor("test:connector", VexProviderKind.Vendor, null!);
// Assert
descriptor.DisplayName.Should().Be("test:connector");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void VexConnectorDescriptor_ValidParameters_StoresValues()
{
// Act
var descriptor = new VexConnectorDescriptor("test:connector", VexProviderKind.Hub, "Test Connector");
// Assert
descriptor.Id.Should().Be("test:connector");
descriptor.Kind.Should().Be(VexProviderKind.Hub);
descriptor.DisplayName.Should().Be("Test Connector");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void VexConnectorDescriptor_ToString_FormatsCorrectly()
{
// Arrange
var descriptor = new VexConnectorDescriptor("test:connector", VexProviderKind.Vendor, "Test");
// Act
var result = descriptor.ToString();
// Assert
result.Should().Be("test:connector (Vendor)");
}
#endregion
#region Multiple Connector Registration Tests
[Trait("Category", TestCategories.Integration)]
[Fact]
public void MultipleConnectors_RegisteredCorrectly()
{
// Arrange
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton(TimeProvider.System);
services.AddSingleton<IVexConnectorStateRepository, InMemoryVexConnectorStateRepository>();
// Add descriptor for RedHat
services.AddSingleton(new VexConnectorDescriptor(
"excititor:redhat",
VexProviderKind.Vendor,
"Red Hat CSAF"));
// Act - Register RedHat connector
services.AddRedHatCsafConnector();
// Assert - Verify services are registered
var descriptors = services.Where(d => d.ServiceType == typeof(IVexConnector)).ToList();
descriptors.Should().HaveCountGreaterThanOrEqualTo(1);
}
#endregion
}
/// <summary>
/// In-memory implementation of IVexConnectorStateRepository for testing.
/// </summary>
file sealed class InMemoryVexConnectorStateRepository : IVexConnectorStateRepository
{
private readonly Dictionary<string, VexConnectorState> _states = new(StringComparer.OrdinalIgnoreCase);
public ValueTask<VexConnectorState?> GetAsync(string connectorId, CancellationToken cancellationToken)
{
_states.TryGetValue(connectorId, out var state);
return ValueTask.FromResult(state);
}
public ValueTask SaveAsync(VexConnectorState state, CancellationToken cancellationToken)
{
_states[state.ConnectorId] = state;
return ValueTask.CompletedTask;
}
public ValueTask<IReadOnlyCollection<VexConnectorState>> ListAsync(CancellationToken cancellationToken)
{
return ValueTask.FromResult<IReadOnlyCollection<VexConnectorState>>(_states.Values.ToList());
}
}