Add tests for SBOM generation determinism across multiple formats

- Created `StellaOps.TestKit.Tests` project for unit tests related to determinism.
- Implemented `DeterminismManifestTests` to validate deterministic output for canonical bytes and strings, file read/write operations, and error handling for invalid schema versions.
- Added `SbomDeterminismTests` to ensure identical inputs produce consistent SBOMs across SPDX 3.0.1 and CycloneDX 1.6/1.7 formats, including parallel execution tests.
- Updated project references in `StellaOps.Integration.Determinism` to include the new determinism testing library.
This commit is contained in:
master
2025-12-23 18:56:12 +02:00
committed by StellaOps Bot
parent 7ac70ece71
commit 491e883653
409 changed files with 23797 additions and 17779 deletions

View File

@@ -0,0 +1,166 @@
using System.Reflection;
using NetArchTest.Rules;
using Xunit;
using FluentAssertions;
namespace StellaOps.Architecture.Tests;
/// <summary>
/// Architecture tests for forbidden package rules.
/// Enforces compliance constraints on library usage.
/// </summary>
[Trait("Category", "Architecture")]
public sealed class ForbiddenPackageRulesTests
{
/// <summary>
/// No direct Redis library usage - only Valkey-compatible clients allowed.
/// </summary>
[Fact]
public void Assemblies_MustNot_Use_Direct_Redis_Clients()
{
var stellaOpsAssemblies = GetStellaOpsAssemblies();
if (!stellaOpsAssemblies.Any())
{
return;
}
// ServiceStack.Redis and similar direct Redis clients are forbidden
// StackExchange.Redis is allowed as it's Valkey-compatible
var forbiddenRedisPackages = new[]
{
"ServiceStack.Redis",
"CSRedis",
"FreeRedis"
};
var result = Types.InAssemblies(stellaOpsAssemblies)
.ShouldNot()
.HaveDependencyOnAny(forbiddenRedisPackages)
.GetResult();
result.IsSuccessful.Should().BeTrue(
$"StellaOps assemblies must not use non-Valkey-compatible Redis clients. " +
$"Use StackExchange.Redis (Valkey-compatible). " +
$"Violations: {string.Join(", ", result.FailingTypeNames ?? Enumerable.Empty<string>())}");
}
/// <summary>
/// No MongoDB usage - deprecated per Sprint 4400.
/// </summary>
[Fact]
public void Assemblies_MustNot_Use_MongoDB()
{
var stellaOpsAssemblies = GetStellaOpsAssemblies();
if (!stellaOpsAssemblies.Any())
{
return;
}
var result = Types.InAssemblies(stellaOpsAssemblies)
.ShouldNot()
.HaveDependencyOnAny(
"MongoDB.Driver",
"MongoDB.Bson",
"MongoDb.*")
.GetResult();
result.IsSuccessful.Should().BeTrue(
$"MongoDB is deprecated (Sprint 4400). Use PostgreSQL. " +
$"Violations: {string.Join(", ", result.FailingTypeNames ?? Enumerable.Empty<string>())}");
}
/// <summary>
/// Crypto libraries must be plugin-based - no direct BouncyCastle references in core.
/// </summary>
[Fact]
public void CoreAssemblies_MustNot_Reference_BouncyCastle_Directly()
{
var coreAssemblies = GetCoreAssemblies();
if (!coreAssemblies.Any())
{
return;
}
// Core assemblies should use crypto through plugin abstraction
var result = Types.InAssemblies(coreAssemblies)
.ShouldNot()
.HaveDependencyOnAny(
"Org.BouncyCastle.*",
"BouncyCastle.*")
.GetResult();
result.IsSuccessful.Should().BeTrue(
$"Core assemblies must not reference BouncyCastle directly. " +
$"Use crypto plugins instead. " +
$"Violations: {string.Join(", ", result.FailingTypeNames ?? Enumerable.Empty<string>())}");
}
/// <summary>
/// No Newtonsoft.Json in new code - use System.Text.Json or StellaOps.Canonical.Json.
/// </summary>
[Fact]
public void Assemblies_Should_Prefer_SystemTextJson()
{
var stellaOpsAssemblies = GetStellaOpsAssemblies()
.Where(a => !a.GetName().Name?.Contains("Test") ?? false);
if (!stellaOpsAssemblies.Any())
{
return;
}
// This is a warning-level check, not a hard requirement
// Some interop scenarios may require Newtonsoft
var result = Types.InAssemblies(stellaOpsAssemblies)
.That()
.HaveDependencyOn("Newtonsoft.Json")
.GetTypes();
// Log but don't fail - this is advisory
if (result.Any())
{
// Advisory: consider migrating to System.Text.Json
}
}
/// <summary>
/// No Entity Framework - use Dapper or raw Npgsql.
/// </summary>
[Fact]
public void Assemblies_MustNot_Use_EntityFramework()
{
var stellaOpsAssemblies = GetStellaOpsAssemblies();
if (!stellaOpsAssemblies.Any())
{
return;
}
var result = Types.InAssemblies(stellaOpsAssemblies)
.ShouldNot()
.HaveDependencyOnAny(
"Microsoft.EntityFrameworkCore",
"Microsoft.EntityFrameworkCore.*")
.GetResult();
result.IsSuccessful.Should().BeTrue(
$"Entity Framework is not used in StellaOps. Use Dapper or Npgsql. " +
$"Violations: {string.Join(", ", result.FailingTypeNames ?? Enumerable.Empty<string>())}");
}
private static IEnumerable<Assembly> GetStellaOpsAssemblies()
{
return AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetName().Name?.StartsWith("StellaOps") == true);
}
private static IEnumerable<Assembly> GetCoreAssemblies()
{
return AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetName().Name?.Contains("Core") == true &&
a.GetName().Name?.StartsWith("StellaOps") == true);
}
}

View File

@@ -0,0 +1,87 @@
using System.Reflection;
using NetArchTest.Rules;
using Xunit;
using FluentAssertions;
namespace StellaOps.Architecture.Tests;
/// <summary>
/// Architecture tests for lattice engine placement rules.
/// Ensures lattice algorithms are only in Scanner.WebService, not in Concelier or Excititor.
/// </summary>
[Trait("Category", "Architecture")]
public sealed class LatticeEngineRulesTests
{
private const string ScannerLatticeNamespace = "StellaOps.Scanner.Lattice";
/// <summary>
/// Concelier modules must not reference Scanner lattice engine.
/// Lattice decisions are made in Scanner, not in Concelier.
/// </summary>
[Fact]
public void Concelier_MustNot_Reference_ScannerLattice()
{
var concelierAssemblies = GetAssembliesByPattern("StellaOps.Concelier");
if (!concelierAssemblies.Any())
{
// Skip if assemblies not loaded (test discovery phase)
return;
}
var result = Types.InAssemblies(concelierAssemblies)
.ShouldNot()
.HaveDependencyOn(ScannerLatticeNamespace)
.GetResult();
result.IsSuccessful.Should().BeTrue(
$"Concelier assemblies must not reference Scanner lattice. " +
$"Violations: {string.Join(", ", result.FailingTypeNames ?? Enumerable.Empty<string>())}");
}
/// <summary>
/// Excititor modules must not reference Scanner lattice engine.
/// Excititor preserves prune source - does not evaluate lattice decisions.
/// </summary>
[Fact]
public void Excititor_MustNot_Reference_ScannerLattice()
{
var excititorAssemblies = GetAssembliesByPattern("StellaOps.Excititor");
if (!excititorAssemblies.Any())
{
return;
}
var result = Types.InAssemblies(excititorAssemblies)
.ShouldNot()
.HaveDependencyOn(ScannerLatticeNamespace)
.GetResult();
result.IsSuccessful.Should().BeTrue(
$"Excititor assemblies must not reference Scanner lattice. " +
$"Violations: {string.Join(", ", result.FailingTypeNames ?? Enumerable.Empty<string>())}");
}
/// <summary>
/// Scanner.WebService MAY reference Scanner lattice engine (it's the authorized host).
/// This test documents the allowed dependency.
/// </summary>
[Fact]
public void ScannerWebService_May_Reference_ScannerLattice()
{
// This is a documentation test - Scanner.WebService is allowed to use lattice
// The test validates that the architectural rule is correctly documented
var allowedAssemblies = new[] { "StellaOps.Scanner.WebService" };
// Positive assertion: these assemblies ARE allowed to reference lattice
allowedAssemblies.Should().Contain("StellaOps.Scanner.WebService",
"Scanner.WebService is the authorized host for lattice algorithms");
}
private static IEnumerable<Assembly> GetAssembliesByPattern(string pattern)
{
return AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetName().Name?.StartsWith(pattern) == true);
}
}

View File

@@ -0,0 +1,136 @@
using System.Reflection;
using NetArchTest.Rules;
using Xunit;
using FluentAssertions;
namespace StellaOps.Architecture.Tests;
/// <summary>
/// Architecture tests for module dependency rules.
/// Enforces proper layering between Core, Infrastructure, and WebService assemblies.
/// </summary>
[Trait("Category", "Architecture")]
public sealed class ModuleDependencyRulesTests
{
/// <summary>
/// Core libraries must not depend on infrastructure (e.g., Postgres storage).
/// Core should be pure business logic, infrastructure-agnostic.
/// </summary>
[Fact]
public void CoreLibraries_MustNot_Depend_On_Infrastructure()
{
var coreAssemblies = GetAssembliesByPattern("Core");
if (!coreAssemblies.Any())
{
return;
}
var result = Types.InAssemblies(coreAssemblies)
.ShouldNot()
.HaveDependencyOnAny(
"StellaOps.*.Storage.Postgres",
"StellaOps.*.Storage.Valkey",
"Npgsql",
"StackExchange.Redis")
.GetResult();
result.IsSuccessful.Should().BeTrue(
$"Core libraries must not depend on infrastructure. " +
$"Violations: {string.Join(", ", result.FailingTypeNames ?? Enumerable.Empty<string>())}");
}
/// <summary>
/// WebServices may depend on Core and Storage, but not on other WebServices.
/// Each WebService should be independently deployable.
/// </summary>
[Fact]
public void WebServices_MustNot_Depend_On_Other_WebServices()
{
var webServiceAssemblies = GetAssembliesByPattern("WebService");
if (!webServiceAssemblies.Any())
{
return;
}
foreach (var assembly in webServiceAssemblies)
{
var assemblyName = assembly.GetName().Name;
var otherWebServices = webServiceAssemblies
.Where(a => a.GetName().Name != assemblyName)
.Select(a => a.GetName().Name!)
.ToArray();
if (!otherWebServices.Any())
{
continue;
}
var result = Types.InAssembly(assembly)
.ShouldNot()
.HaveDependencyOnAny(otherWebServices)
.GetResult();
result.IsSuccessful.Should().BeTrue(
$"WebService {assemblyName} must not depend on other WebServices. " +
$"Violations: {string.Join(", ", result.FailingTypeNames ?? Enumerable.Empty<string>())}");
}
}
/// <summary>
/// Workers may depend on Core and Storage, but not directly on WebServices.
/// Workers are background processes, independent of HTTP layer.
/// </summary>
[Fact]
public void Workers_MustNot_Depend_On_WebServices()
{
var workerAssemblies = GetAssembliesByPattern("Worker");
if (!workerAssemblies.Any())
{
return;
}
var result = Types.InAssemblies(workerAssemblies)
.ShouldNot()
.HaveDependencyOnAny("Microsoft.AspNetCore.Mvc", "StellaOps.*.WebService")
.GetResult();
result.IsSuccessful.Should().BeTrue(
$"Worker assemblies must not depend on WebServices. " +
$"Violations: {string.Join(", ", result.FailingTypeNames ?? Enumerable.Empty<string>())}");
}
/// <summary>
/// Models/DTOs should not depend on infrastructure or services.
/// </summary>
[Fact]
public void Models_MustNot_Depend_On_Services()
{
var modelAssemblies = GetAssembliesByPattern("Models");
if (!modelAssemblies.Any())
{
return;
}
var result = Types.InAssemblies(modelAssemblies)
.ShouldNot()
.HaveDependencyOnAny(
"StellaOps.*.Storage.*",
"StellaOps.*.WebService",
"Microsoft.AspNetCore.*")
.GetResult();
result.IsSuccessful.Should().BeTrue(
$"Model assemblies must not depend on services. " +
$"Violations: {string.Join(", ", result.FailingTypeNames ?? Enumerable.Empty<string>())}");
}
private static IEnumerable<Assembly> GetAssembliesByPattern(string pattern)
{
return AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetName().Name?.Contains(pattern) == true);
}
}

View File

@@ -0,0 +1,158 @@
using System.Reflection;
using NetArchTest.Rules;
using Xunit;
using FluentAssertions;
namespace StellaOps.Architecture.Tests;
/// <summary>
/// Architecture tests for naming convention rules.
/// Enforces consistent naming across the codebase.
/// </summary>
[Trait("Category", "Architecture")]
public sealed class NamingConventionRulesTests
{
/// <summary>
/// Test projects must end with .Tests.
/// </summary>
[Fact]
public void TestProjects_MustEndWith_Tests()
{
var testAssemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetName().Name?.StartsWith("StellaOps") == true)
.Where(a => ContainsTestTypes(a));
foreach (var assembly in testAssemblies)
{
var name = assembly.GetName().Name;
// If it has test types, it should end with .Tests
if (!name!.EndsWith(".Tests"))
{
// Check if it's in a known test location pattern
var isValidTestAssembly = name.Contains("Test") ||
name.EndsWith(".Tests") ||
name.Contains("Testing");
isValidTestAssembly.Should().BeTrue(
$"Assembly {name} contains tests but doesn't follow naming convention (.Tests suffix)");
}
}
}
/// <summary>
/// Plugin assemblies must follow naming pattern.
/// </summary>
[Fact]
public void Plugins_MustFollow_NamingPattern()
{
var pluginAssemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetName().Name?.Contains("Plugin") == true &&
a.GetName().Name?.StartsWith("StellaOps") == true);
foreach (var assembly in pluginAssemblies)
{
var name = assembly.GetName().Name!;
// Valid patterns: StellaOps.<Module>.Plugin.* or StellaOps.<Module>.Plugins.*
var isValidPluginName =
System.Text.RegularExpressions.Regex.IsMatch(name, @"^StellaOps\.\w+\.Plugin\.\w+$") ||
System.Text.RegularExpressions.Regex.IsMatch(name, @"^StellaOps\.\w+\.Plugins\.\w+$") ||
System.Text.RegularExpressions.Regex.IsMatch(name, @"^StellaOps\.\w+\.Plugin$");
isValidPluginName.Should().BeTrue(
$"Plugin assembly {name} doesn't follow naming pattern StellaOps.<Module>.Plugin[s].*");
}
}
/// <summary>
/// Connector assemblies must follow naming pattern.
/// </summary>
[Fact]
public void Connectors_MustFollow_NamingPattern()
{
var connectorAssemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetName().Name?.Contains("Connector") == true &&
a.GetName().Name?.StartsWith("StellaOps") == true);
foreach (var assembly in connectorAssemblies)
{
var name = assembly.GetName().Name!;
// Valid patterns: StellaOps.<Module>.Connector.* or StellaOps.<Module>.Connector
var isValidConnectorName =
System.Text.RegularExpressions.Regex.IsMatch(name, @"^StellaOps\.\w+\.Connector\.\w+$") ||
System.Text.RegularExpressions.Regex.IsMatch(name, @"^StellaOps\.\w+\.Connector$") ||
System.Text.RegularExpressions.Regex.IsMatch(name, @"^StellaOps\.\w+\.Connector\.Common$");
isValidConnectorName.Should().BeTrue(
$"Connector assembly {name} doesn't follow naming pattern StellaOps.<Module>.Connector[.*]");
}
}
/// <summary>
/// Storage assemblies must follow naming pattern.
/// </summary>
[Fact]
public void Storage_MustFollow_NamingPattern()
{
var storageAssemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetName().Name?.Contains("Storage") == true &&
a.GetName().Name?.StartsWith("StellaOps") == true);
foreach (var assembly in storageAssemblies)
{
var name = assembly.GetName().Name!;
// Valid patterns: StellaOps.<Module>.Storage or StellaOps.<Module>.Storage.<Provider>
var isValidStorageName =
System.Text.RegularExpressions.Regex.IsMatch(name, @"^StellaOps\.\w+\.Storage$") ||
System.Text.RegularExpressions.Regex.IsMatch(name, @"^StellaOps\.\w+\.Storage\.\w+$");
isValidStorageName.Should().BeTrue(
$"Storage assembly {name} doesn't follow naming pattern StellaOps.<Module>.Storage[.<Provider>]");
}
}
/// <summary>
/// Interface types should start with 'I'.
/// </summary>
[Fact]
public void Interfaces_MustStartWith_I()
{
var stellaOpsAssemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetName().Name?.StartsWith("StellaOps") == true &&
!a.GetName().Name?.Contains("Test") == true);
if (!stellaOpsAssemblies.Any())
{
return;
}
var result = Types.InAssemblies(stellaOpsAssemblies)
.That()
.AreInterfaces()
.Should()
.HaveNameStartingWith("I")
.GetResult();
result.IsSuccessful.Should().BeTrue(
$"Interface types must start with 'I'. " +
$"Violations: {string.Join(", ", result.FailingTypeNames ?? Enumerable.Empty<string>())}");
}
private static bool ContainsTestTypes(Assembly assembly)
{
try
{
return assembly.GetTypes()
.Any(t => t.GetMethods()
.Any(m => m.GetCustomAttributes(typeof(FactAttribute), false).Any() ||
m.GetCustomAttributes(typeof(TheoryAttribute), false).Any()));
}
catch
{
return false;
}
}
}

View File

@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.2" />
<PackageReference Include="NetArchTest.Rules" Version="1.3.2" />
</ItemGroup>
<!-- Reference assemblies under test -->
<ItemGroup>
<!-- Core/Canonical -->
<ProjectReference Include="..\..\..\src\__Libraries\StellaOps.Canonical.Json\StellaOps.Canonical.Json.csproj" />
<ProjectReference Include="..\..\..\src\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
<!-- Scanner module (sample reference - expand as needed) -->
<!-- Note: Add module references as architecture rules are implemented -->
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
<Using Include="FluentAssertions" />
<Using Include="NetArchTest.Rules" />
</ItemGroup>
</Project>