audit notes work completed, test fixes work (95% done), new sprints, new data sources setup and configuration

This commit is contained in:
master
2026-01-14 10:48:00 +02:00
parent d7be6ba34b
commit 95d5898650
379 changed files with 40695 additions and 19041 deletions

View File

@@ -0,0 +1,203 @@
using StellaOps.DistroIntel;
using Xunit;
namespace StellaOps.DistroIntel.Tests;
/// <summary>
/// Tests for DerivativeConfidence enum.
/// </summary>
public sealed class DerivativeConfidenceTests
{
[Theory]
[InlineData(DerivativeConfidence.High)]
[InlineData(DerivativeConfidence.Medium)]
public void DerivativeConfidence_AllValues_AreDefined(DerivativeConfidence confidence)
{
Assert.True(Enum.IsDefined(confidence));
}
[Fact]
public void DerivativeConfidence_AllValues_AreCounted()
{
var values = Enum.GetValues<DerivativeConfidence>();
Assert.Equal(2, values.Length);
}
}
/// <summary>
/// Tests for DistroDerivative record.
/// </summary>
public sealed class DistroDerivativeTests
{
[Fact]
public void DistroDerivative_RequiredProperties_MustBeSet()
{
var derivative = new DistroDerivative("rhel", "almalinux", 9, DerivativeConfidence.High);
Assert.Equal("rhel", derivative.CanonicalDistro);
Assert.Equal("almalinux", derivative.DerivativeDistro);
Assert.Equal(9, derivative.MajorRelease);
Assert.Equal(DerivativeConfidence.High, derivative.Confidence);
}
[Fact]
public void DistroDerivative_RecordEquality_WorksCorrectly()
{
var d1 = new DistroDerivative("rhel", "rocky", 9, DerivativeConfidence.High);
var d2 = new DistroDerivative("rhel", "rocky", 9, DerivativeConfidence.High);
Assert.Equal(d1, d2);
}
[Fact]
public void DistroDerivative_DifferentReleases_AreNotEqual()
{
var d1 = new DistroDerivative("rhel", "rocky", 8, DerivativeConfidence.High);
var d2 = new DistroDerivative("rhel", "rocky", 9, DerivativeConfidence.High);
Assert.NotEqual(d1, d2);
}
}
/// <summary>
/// Tests for DistroMappings static class.
/// </summary>
public sealed class DistroMappingsTests
{
[Fact]
public void DistroMappings_Derivatives_ContainsKnownMappings()
{
Assert.NotEmpty(DistroMappings.Derivatives);
Assert.True(DistroMappings.Derivatives.Length >= 10);
}
[Theory]
[InlineData("rhel", 8)]
[InlineData("rhel", 9)]
[InlineData("debian", 11)]
[InlineData("debian", 12)]
[InlineData("ubuntu", 20)]
[InlineData("ubuntu", 22)]
public void FindDerivativesFor_KnownCanonical_ReturnsDerivatives(string canonical, int release)
{
var derivatives = DistroMappings.FindDerivativesFor(canonical, release).ToList();
Assert.NotEmpty(derivatives);
Assert.All(derivatives, d => Assert.Equal(canonical, d.CanonicalDistro));
Assert.All(derivatives, d => Assert.Equal(release, d.MajorRelease));
}
[Fact]
public void FindDerivativesFor_UnknownDistro_ReturnsEmpty()
{
var derivatives = DistroMappings.FindDerivativesFor("unknowndistro", 1).ToList();
Assert.Empty(derivatives);
}
[Fact]
public void FindDerivativesFor_ResultsOrderedByConfidence()
{
var derivatives = DistroMappings.FindDerivativesFor("rhel", 9).ToList();
Assert.NotEmpty(derivatives);
// All RHEL derivatives should be High confidence
Assert.All(derivatives, d => Assert.Equal(DerivativeConfidence.High, d.Confidence));
}
[Theory]
[InlineData("almalinux", 9, "rhel")]
[InlineData("rocky", 9, "rhel")]
[InlineData("centos", 7, "rhel")]
[InlineData("oracle", 8, "rhel")]
public void FindCanonicalFor_KnownDerivative_ReturnsCanonical(string derivative, int release, string expectedCanonical)
{
var canonical = DistroMappings.FindCanonicalFor(derivative, release);
Assert.NotNull(canonical);
Assert.Equal(expectedCanonical, canonical.CanonicalDistro);
}
[Fact]
public void FindCanonicalFor_UnknownDerivative_ReturnsNull()
{
var canonical = DistroMappings.FindCanonicalFor("unknowndistro", 1);
Assert.Null(canonical);
}
[Theory]
[InlineData(DerivativeConfidence.High, 0.95)]
[InlineData(DerivativeConfidence.Medium, 0.80)]
public void GetConfidenceMultiplier_ReturnsExpectedValues(DerivativeConfidence confidence, decimal expected)
{
var multiplier = DistroMappings.GetConfidenceMultiplier(confidence);
Assert.Equal(expected, multiplier);
}
[Theory]
[InlineData("rhel", true)]
[InlineData("debian", true)]
[InlineData("ubuntu", true)]
[InlineData("sles", true)]
[InlineData("alpine", true)]
[InlineData("almalinux", false)]
[InlineData("rocky", false)]
[InlineData("linuxmint", false)]
public void IsCanonicalDistro_ReturnsCorrectResult(string distro, bool expected)
{
var result = DistroMappings.IsCanonicalDistro(distro);
Assert.Equal(expected, result);
}
[Theory]
[InlineData("redhat", "rhel")]
[InlineData("red hat", "rhel")]
[InlineData("red-hat", "rhel")]
[InlineData("RHEL", "rhel")]
[InlineData("alma", "almalinux")]
[InlineData("almalinux-os", "almalinux")]
[InlineData("rockylinux", "rocky")]
[InlineData("rocky-linux", "rocky")]
[InlineData("oracle linux", "oracle")]
[InlineData("oraclelinux", "oracle")]
[InlineData("opensuse", "opensuse-leap")]
[InlineData("mint", "linuxmint")]
[InlineData("popos", "pop")]
[InlineData("pop_os", "pop")]
[InlineData("debian", "debian")]
[InlineData("ubuntu", "ubuntu")]
public void NormalizeDistroName_ReturnsCanonicalForm(string input, string expected)
{
var result = DistroMappings.NormalizeDistroName(input);
Assert.Equal(expected, result);
}
[Fact]
public void FindDerivativesFor_CaseInsensitive()
{
var lower = DistroMappings.FindDerivativesFor("rhel", 9).ToList();
var upper = DistroMappings.FindDerivativesFor("RHEL", 9).ToList();
var mixed = DistroMappings.FindDerivativesFor("RhEl", 9).ToList();
Assert.Equal(lower.Count, upper.Count);
Assert.Equal(lower.Count, mixed.Count);
}
[Fact]
public void FindCanonicalFor_CaseInsensitive()
{
var lower = DistroMappings.FindCanonicalFor("almalinux", 9);
var upper = DistroMappings.FindCanonicalFor("ALMALINUX", 9);
var mixed = DistroMappings.FindCanonicalFor("AlmaLinux", 9);
Assert.NotNull(lower);
Assert.NotNull(upper);
Assert.NotNull(mixed);
Assert.Equal(lower, upper);
Assert.Equal(lower, mixed);
}
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
<UseXunitV3>true</UseXunitV3>
</PropertyGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.DistroIntel\StellaOps.DistroIntel.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,7 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"diagnosticMessages": true,
"parallelizeAssembly": true,
"parallelizeTestCollections": true,
"maxParallelThreads": -1
}

View File

@@ -0,0 +1,116 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Doctor.Plugins;
using Xunit;
namespace StellaOps.Doctor.Plugins.Authority.Tests;
[Trait("Category", "Unit")]
public sealed class AuthorityPluginTests
{
private readonly AuthorityPlugin _plugin = new();
[Fact]
public void PluginId_ReturnsExpectedValue()
{
_plugin.PluginId.Should().Be("stellaops.doctor.authority");
}
[Fact]
public void DisplayName_ReturnsExpectedValue()
{
_plugin.DisplayName.Should().Be("Authority");
}
[Fact]
public void Category_ReturnsAuthority()
{
_plugin.Category.Should().Be(DoctorCategory.Authority);
}
[Fact]
public void GetChecks_ReturnsAllExpectedChecks()
{
// Arrange
var context = CreateContext();
// Act
var checks = _plugin.GetChecks(context);
// Assert
checks.Should().HaveCount(5);
checks.Select(c => c.CheckId).Should().BeEquivalentTo(new[]
{
"check.authority.plugin.configured",
"check.authority.plugin.connectivity",
"check.authority.bootstrap.exists",
"check.users.superuser.exists",
"check.users.password.policy"
});
}
[Fact]
public void GetChecks_AllChecksHaveUniqueIds()
{
// Arrange
var context = CreateContext();
// Act
var checks = _plugin.GetChecks(context);
// Assert
var checkIds = checks.Select(c => c.CheckId);
checkIds.Should().OnlyHaveUniqueItems();
}
[Fact]
public void GetChecks_AllChecksHaveDescriptions()
{
// Arrange
var context = CreateContext();
// Act
var checks = _plugin.GetChecks(context);
// Assert
foreach (var check in checks)
{
check.Description.Should().NotBeNullOrWhiteSpace(
$"Check {check.CheckId} should have a description");
}
}
[Fact]
public void GetChecks_AllChecksHaveTags()
{
// Arrange
var context = CreateContext();
// Act
var checks = _plugin.GetChecks(context);
// Assert
foreach (var check in checks)
{
check.Tags.Should().NotBeEmpty(
$"Check {check.CheckId} should have at least one tag");
}
}
private static DoctorPluginContext CreateContext()
{
var config = new ConfigurationBuilder().Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
}

View File

@@ -0,0 +1,215 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Authority.Checks;
using Xunit;
namespace StellaOps.Doctor.Plugins.Authority.Tests.Checks;
[Trait("Category", "Unit")]
public sealed class AuthorityPluginConfigurationCheckTests
{
private readonly AuthorityPluginConfigurationCheck _check = new();
[Fact]
public void CheckId_ReturnsExpectedValue()
{
_check.CheckId.Should().Be("check.authority.plugin.configured");
}
[Fact]
public void Name_ReturnsExpectedValue()
{
_check.Name.Should().Be("Authority Plugin Configuration");
}
[Fact]
public void DefaultSeverity_IsCritical()
{
_check.DefaultSeverity.Should().Be(DoctorSeverity.Fail);
}
[Fact]
public void Tags_ContainsExpectedValues()
{
_check.Tags.Should().Contain("authority");
_check.Tags.Should().Contain("authentication");
_check.Tags.Should().Contain("security");
}
[Fact]
public void CanRun_ReturnsTrue()
{
var context = CreateContext(new Dictionary<string, string?>());
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public async Task RunAsync_Fails_WhenNoPluginsConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>());
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("No authentication plugins configured");
}
[Fact]
public async Task RunAsync_Passes_WhenStandardPluginEnabled()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("1 authentication plugin(s) configured");
}
[Fact]
public async Task RunAsync_Passes_WhenStandardSectionExists()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:PasswordPolicy:MinLength"] = "12"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
}
[Fact]
public async Task RunAsync_Passes_WhenLdapPluginConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Ldap:Enabled"] = "true",
["Authority:Plugins:Ldap:Server"] = "ldap://ldap.example.com"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("1 authentication plugin(s) configured");
}
[Fact]
public async Task RunAsync_Warns_WhenLdapEnabledButServerMissing()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Ldap:Enabled"] = "true"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
}
[Fact]
public async Task RunAsync_Passes_WhenOidcPluginConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Oidc:Enabled"] = "true",
["Authority:Plugins:Oidc:Authority"] = "https://login.example.com"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
}
[Fact]
public async Task RunAsync_Warns_WhenOidcEnabledButAuthorityMissing()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Oidc:Enabled"] = "true"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
}
[Fact]
public async Task RunAsync_Passes_WhenSamlPluginEnabled()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Saml:Enabled"] = "true"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
}
[Fact]
public async Task RunAsync_ReportsMultiplePlugins()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:Plugins:Ldap:Enabled"] = "true",
["Authority:Plugins:Ldap:Server"] = "ldap://ldap.example.com"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("2 authentication plugin(s) configured");
}
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(configValues)
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
}

View File

@@ -0,0 +1,207 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Authority.Checks;
using Xunit;
namespace StellaOps.Doctor.Plugins.Authority.Tests.Checks;
[Trait("Category", "Unit")]
public sealed class AuthorityPluginConnectivityCheckTests
{
private readonly AuthorityPluginConnectivityCheck _check = new();
[Fact]
public void CheckId_ReturnsExpectedValue()
{
_check.CheckId.Should().Be("check.authority.plugin.connectivity");
}
[Fact]
public void Name_ReturnsExpectedValue()
{
_check.Name.Should().Be("Authority Backend Connectivity");
}
[Fact]
public void DefaultSeverity_IsCritical()
{
_check.DefaultSeverity.Should().Be(DoctorSeverity.Fail);
}
[Fact]
public void Tags_ContainsExpectedValues()
{
_check.Tags.Should().Contain("authority");
_check.Tags.Should().Contain("connectivity");
_check.Tags.Should().Contain("ldap");
_check.Tags.Should().Contain("database");
}
[Fact]
public void CanRun_ReturnsFalse_WhenNoPluginsConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>());
// Act & Assert
_check.CanRun(context).Should().BeFalse();
}
[Fact]
public void CanRun_ReturnsTrue_WhenStandardPluginSectionExists()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true"
});
// Act & Assert
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public void CanRun_ReturnsTrue_WhenLdapPluginSectionExists()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Ldap:Server"] = "ldap://ldap.example.com"
});
// Act & Assert
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public async Task RunAsync_Skips_WhenNoBackendsToTest()
{
// Arrange - Sections exist but no plugins enabled
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:SomeSetting"] = "value",
["Authority:Plugins:Standard:Enabled"] = "false"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Skip);
result.Diagnosis.Should().Contain("No authentication backends configured");
}
[Fact]
public async Task RunAsync_Fails_WhenDatabaseConnectionStringMissing()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("unreachable");
}
[Fact]
public async Task RunAsync_Passes_WhenDatabaseConnectionStringConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["ConnectionStrings:Authority"] = "Host=localhost;Database=authority"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("reachable");
}
[Fact]
public async Task RunAsync_UsesDefaultConnectionString_WhenAuthorityConnectionMissing()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["ConnectionStrings:Default"] = "Host=localhost;Database=stellaops"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
}
[Fact]
public async Task RunAsync_TestsBothBackends_WhenBothEnabled()
{
// Arrange - Standard with DB and LDAP both enabled
// Note: LDAP will fail in test because we can't actually connect
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["ConnectionStrings:Authority"] = "Host=localhost;Database=authority",
["Authority:Plugins:Ldap:Enabled"] = "true",
["Authority:Plugins:Ldap:Server"] = "ldap://127.0.0.1:3899"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert - LDAP will fail (can't connect in test), so overall should fail
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("unreachable");
}
[Fact]
public async Task RunAsync_SkipsLdapTest_WhenLdapEnabledButNoServer()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["ConnectionStrings:Authority"] = "Host=localhost;Database=authority",
["Authority:Plugins:Ldap:Enabled"] = "true"
// No Ldap:Server configured
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert - Should only test Standard (DB), which passes
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("1 backend(s) reachable");
}
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(configValues)
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
}

View File

@@ -0,0 +1,192 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Authority.Checks;
using Xunit;
namespace StellaOps.Doctor.Plugins.Authority.Tests.Checks;
[Trait("Category", "Unit")]
public sealed class BootstrapUserExistsCheckTests
{
private readonly BootstrapUserExistsCheck _check = new();
[Fact]
public void CheckId_ReturnsExpectedValue()
{
_check.CheckId.Should().Be("check.authority.bootstrap.exists");
}
[Fact]
public void Name_ReturnsExpectedValue()
{
_check.Name.Should().Be("Bootstrap User Exists");
}
[Fact]
public void DefaultSeverity_IsCritical()
{
_check.DefaultSeverity.Should().Be(DoctorSeverity.Fail);
}
[Fact]
public void Tags_ContainsExpectedValues()
{
_check.Tags.Should().Contain("authority");
_check.Tags.Should().Contain("user");
_check.Tags.Should().Contain("bootstrap");
_check.Tags.Should().Contain("admin");
}
[Fact]
public void CanRun_ReturnsFalse_WhenStandardPluginNotEnabled()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>());
// Act & Assert
_check.CanRun(context).Should().BeFalse();
}
[Fact]
public void CanRun_ReturnsTrue_WhenStandardPluginEnabled()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true"
});
// Act & Assert
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public async Task RunAsync_Fails_WhenAutoBootstrapDisabledAndNoConfig()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:Bootstrap:Enabled"] = "false"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("No bootstrap user configured");
}
[Fact]
public async Task RunAsync_Info_WhenAutoBootstrapEnabledButNoConfig()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:Bootstrap:Enabled"] = "true"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Info);
result.Diagnosis.Should().Contain("auto-created");
}
[Fact]
public async Task RunAsync_Warns_WhenUsernameMissing()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:Bootstrap:Email"] = "admin@example.com"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("incomplete");
}
[Fact]
public async Task RunAsync_Warns_WhenEmailMissing()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:Bootstrap:Username"] = "admin"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("incomplete");
}
[Fact]
public async Task RunAsync_Passes_WhenFullyConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:Bootstrap:Username"] = "admin",
["Authority:Bootstrap:Email"] = "admin@example.com"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("properly configured");
}
[Fact]
public async Task RunAsync_ReadsFromAlternativeConfigPath()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:Plugins:Standard:Bootstrap:Username"] = "admin",
["Authority:Plugins:Standard:Bootstrap:Email"] = "admin@example.com"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
}
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(configValues)
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
}

View File

@@ -0,0 +1,212 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Authority.Checks;
using Xunit;
namespace StellaOps.Doctor.Plugins.Authority.Tests.Checks;
[Trait("Category", "Unit")]
public sealed class SuperUserExistsCheckTests
{
private readonly SuperUserExistsCheck _check = new();
[Fact]
public void CheckId_ReturnsExpectedValue()
{
_check.CheckId.Should().Be("check.users.superuser.exists");
}
[Fact]
public void Name_ReturnsExpectedValue()
{
_check.Name.Should().Be("Super User Exists");
}
[Fact]
public void DefaultSeverity_IsCritical()
{
_check.DefaultSeverity.Should().Be(DoctorSeverity.Fail);
}
[Fact]
public void Tags_ContainsExpectedValues()
{
_check.Tags.Should().Contain("authority");
_check.Tags.Should().Contain("user");
_check.Tags.Should().Contain("admin");
_check.Tags.Should().Contain("superuser");
_check.Tags.Should().Contain("security");
}
[Fact]
public void CanRun_ReturnsFalse_WhenStandardPluginNotEnabled()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>());
// Act & Assert
_check.CanRun(context).Should().BeFalse();
}
[Fact]
public void CanRun_ReturnsTrue_WhenStandardPluginEnabled()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true"
});
// Act & Assert
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public async Task RunAsync_Fails_WhenNoAdminsAndAutoBootstrapDisabled()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:Bootstrap:Enabled"] = "false"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("No administrator users configured");
}
[Fact]
public async Task RunAsync_Info_WhenNoAdminsButAutoBootstrapEnabled()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:Bootstrap:Enabled"] = "true"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Info);
result.Diagnosis.Should().Contain("auto-bootstrap");
}
[Fact]
public async Task RunAsync_Passes_WhenBootstrapUsernameConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:Bootstrap:Username"] = "admin"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("1 administrator(s) configured");
}
[Fact]
public async Task RunAsync_Passes_WhenAdministratorsConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:Users:Administrators:0"] = "admin1",
["Authority:Users:Administrators:1"] = "admin2"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("2 administrator(s) configured");
}
[Fact]
public async Task RunAsync_Passes_WhenAdminRolesConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:Roles:Administrators:0"] = "alice",
["Authority:Roles:Administrators:1"] = "bob"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("2 administrator(s) configured");
}
[Fact]
public async Task RunAsync_DeduplicatesAdminUsers()
{
// Arrange - Same user in both bootstrap and admin list
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:Bootstrap:Username"] = "admin",
["Authority:Users:Administrators:0"] = "admin"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("1 administrator(s) configured");
}
[Fact]
public async Task RunAsync_DefaultsAutoBootstrapToTrue()
{
// Arrange - No explicit Bootstrap:Enabled setting
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
// Should return Info because auto-bootstrap defaults to true
result.Severity.Should().Be(DoctorSeverity.Info);
}
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(configValues)
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
}

View File

@@ -0,0 +1,219 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Authority.Checks;
using Xunit;
namespace StellaOps.Doctor.Plugins.Authority.Tests.Checks;
[Trait("Category", "Unit")]
public sealed class UserPasswordPolicyCheckTests
{
private readonly UserPasswordPolicyCheck _check = new();
[Fact]
public void CheckId_ReturnsExpectedValue()
{
_check.CheckId.Should().Be("check.users.password.policy");
}
[Fact]
public void Name_ReturnsExpectedValue()
{
_check.Name.Should().Be("Password Policy");
}
[Fact]
public void DefaultSeverity_IsWarn()
{
_check.DefaultSeverity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public void Tags_ContainsExpectedValues()
{
_check.Tags.Should().Contain("authority");
_check.Tags.Should().Contain("password");
_check.Tags.Should().Contain("policy");
_check.Tags.Should().Contain("security");
}
[Fact]
public void CanRun_ReturnsFalse_WhenStandardPluginNotEnabled()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>());
// Act & Assert
_check.CanRun(context).Should().BeFalse();
}
[Fact]
public void CanRun_ReturnsTrue_WhenStandardPluginEnabled()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true"
});
// Act & Assert
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public async Task RunAsync_Fails_WhenMinLengthBelowAbsoluteMinimum()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:PasswordPolicy:MinLength"] = "6"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("violation");
}
[Fact]
public async Task RunAsync_Warns_WhenMinLengthBelowRecommended()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:PasswordPolicy:MinLength"] = "10"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("recommendation");
}
[Fact]
public async Task RunAsync_Warns_WhenInsufficientComplexityRequirements()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:PasswordPolicy:MinLength"] = "12",
["Authority:PasswordPolicy:RequireUppercase"] = "true",
["Authority:PasswordPolicy:RequireLowercase"] = "true",
["Authority:PasswordPolicy:RequireDigit"] = "false",
["Authority:PasswordPolicy:RequireSpecialCharacter"] = "false"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("recommendation");
}
[Fact]
public async Task RunAsync_Warns_WhenMaxAgeVeryShort()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:PasswordPolicy:MinLength"] = "12",
["Authority:PasswordPolicy:MaxAgeDays"] = "14"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("recommendation");
}
[Fact]
public async Task RunAsync_Warns_WhenPreventReuseCountLow()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:PasswordPolicy:MinLength"] = "12",
["Authority:PasswordPolicy:PreventReuseCount"] = "2"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("recommendation");
}
[Fact]
public async Task RunAsync_Passes_WhenPolicyMeetsRequirements()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true",
["Authority:PasswordPolicy:MinLength"] = "14",
["Authority:PasswordPolicy:RequireUppercase"] = "true",
["Authority:PasswordPolicy:RequireLowercase"] = "true",
["Authority:PasswordPolicy:RequireDigit"] = "true",
["Authority:PasswordPolicy:RequireSpecialCharacter"] = "true",
["Authority:PasswordPolicy:PreventReuseCount"] = "5"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("meets security requirements");
}
[Fact]
public async Task RunAsync_UsesDefaults_WhenNoPolicyConfigured()
{
// Arrange - Only Standard enabled, no password policy explicitly set
var context = CreateContext(new Dictionary<string, string?>
{
["Authority:Plugins:Standard:Enabled"] = "true"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
// Default MinLength is 8, which is below recommended 12, so should warn
result.Severity.Should().Be(DoctorSeverity.Warn);
}
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(configValues)
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Doctor\StellaOps.Doctor.csproj" />
<ProjectReference Include="..\..\StellaOps.Doctor.Plugins.Authority\StellaOps.Doctor.Plugins.Authority.csproj" />
<ProjectReference Include="..\..\StellaOps.TestKit\StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,296 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Notify.Checks;
using Xunit;
namespace StellaOps.Doctor.Plugins.Notify.Tests.Checks;
[Trait("Category", "Unit")]
public sealed class NotifyChannelConfigurationCheckTests
{
private readonly NotifyChannelConfigurationCheck _check = new();
[Fact]
public void CheckId_ReturnsExpectedValue()
{
_check.CheckId.Should().Be("check.notify.channel.configured");
}
[Fact]
public void Name_ReturnsExpectedValue()
{
_check.Name.Should().Be("Notification Channel Configuration");
}
[Fact]
public void DefaultSeverity_IsInfo()
{
_check.DefaultSeverity.Should().Be(DoctorSeverity.Info);
}
[Fact]
public void Tags_ContainsExpectedValues()
{
_check.Tags.Should().Contain("notify");
_check.Tags.Should().Contain("channel");
_check.Tags.Should().Contain("configuration");
}
[Fact]
public void EstimatedDuration_IsShort()
{
_check.EstimatedDuration.Should().BeLessThanOrEqualTo(TimeSpan.FromSeconds(1));
}
[Fact]
public void CanRun_ReturnsTrue()
{
var context = CreateContext(new Dictionary<string, string?>());
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public async Task RunAsync_ReturnsInfo_WhenNoChannelsConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>());
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Info);
result.Diagnosis.Should().Contain("No notification channels configured");
}
[Fact]
public async Task RunAsync_Passes_WhenEmailChannelConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Channels:Email:SmtpHost"] = "smtp.example.com"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("1 notification channel(s) configured");
}
[Fact]
public async Task RunAsync_Warns_WhenEmailEnabledButSmtpHostMissing()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public async Task RunAsync_Passes_WhenSlackChannelConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Slack:Enabled"] = "true",
["Notify:Channels:Slack:WebhookUrl"] = "https://hooks.slack.com/services/..."
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("1 notification channel(s) configured");
}
[Fact]
public async Task RunAsync_Passes_WhenSlackTokenConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Slack:Enabled"] = "true",
["Notify:Channels:Slack:Token"] = "xoxb-123456789"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
}
[Fact]
public async Task RunAsync_Warns_WhenSlackEnabledButNoWebhookOrToken()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Slack:Enabled"] = "true"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public async Task RunAsync_Passes_WhenTeamsChannelConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Teams:Enabled"] = "true",
["Notify:Channels:Teams:WebhookUrl"] = "https://outlook.office.com/webhook/..."
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("1 notification channel(s) configured");
}
[Fact]
public async Task RunAsync_Warns_WhenTeamsEnabledButNoWebhook()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Teams:Enabled"] = "true"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public async Task RunAsync_Passes_WhenWebhookChannelConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Webhook:Enabled"] = "true",
["Notify:Channels:Webhook:Endpoint"] = "https://api.example.com/webhook"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("1 notification channel(s) configured");
}
[Fact]
public async Task RunAsync_Warns_WhenWebhookEnabledButNoEndpoint()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Webhook:Enabled"] = "true"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public async Task RunAsync_ReportsMultipleChannels()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Channels:Email:SmtpHost"] = "smtp.example.com",
["Notify:Channels:Slack:Enabled"] = "true",
["Notify:Channels:Slack:WebhookUrl"] = "https://hooks.slack.com/services/..."
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("2 notification channel(s) configured");
}
[Fact]
public async Task RunAsync_IncludesIssues_WhenSomeChannelsMisconfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Channels:Email:SmtpHost"] = "smtp.example.com",
["Notify:Channels:Slack:Enabled"] = "true" // Missing webhook URL
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("1 channel(s) configured");
result.Diagnosis.Should().Contain("1 issue(s)");
}
[Fact]
public async Task RunAsync_DetectsChannelFromSection_WhenEnabledNotExplicit()
{
// Arrange - Section exists but Enabled not set (defaults to true for existing sections)
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:SmtpHost"] = "smtp.example.com",
["Notify:Channels:Email:SmtpPort"] = "587"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
}
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(configValues)
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
}

View File

@@ -0,0 +1,268 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Notify.Checks;
using Xunit;
namespace StellaOps.Doctor.Plugins.Notify.Tests.Checks;
[Trait("Category", "Unit")]
public sealed class NotifyChannelConnectivityCheckTests
{
private readonly NotifyChannelConnectivityCheck _check = new();
[Fact]
public void CheckId_ReturnsExpectedValue()
{
_check.CheckId.Should().Be("check.notify.channel.connectivity");
}
[Fact]
public void Name_ReturnsExpectedValue()
{
_check.Name.Should().Be("Notification Channel Connectivity");
}
[Fact]
public void Description_ReturnsExpectedValue()
{
_check.Description.Should().Contain("connectivity");
}
[Fact]
public void DefaultSeverity_IsWarn()
{
_check.DefaultSeverity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public void Tags_ContainsExpectedValues()
{
_check.Tags.Should().Contain("notify");
_check.Tags.Should().Contain("channel");
_check.Tags.Should().Contain("connectivity");
_check.Tags.Should().Contain("network");
}
[Fact]
public void EstimatedDuration_AllowsForNetworkTimeout()
{
_check.EstimatedDuration.Should().BeGreaterThanOrEqualTo(TimeSpan.FromSeconds(5));
}
[Fact]
public void CanRun_ReturnsFalse_WhenNoChannelsConfigured()
{
var context = CreateContext(new Dictionary<string, string?>());
_check.CanRun(context).Should().BeFalse();
}
[Fact]
public void CanRun_ReturnsTrue_WhenEmailChannelConfigured()
{
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:SmtpHost"] = "smtp.example.com"
});
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public void CanRun_ReturnsTrue_WhenSlackChannelConfigured()
{
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Slack:WebhookUrl"] = "https://hooks.slack.com/services/..."
});
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public void CanRun_ReturnsTrue_WhenTeamsChannelConfigured()
{
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Teams:WebhookUrl"] = "https://outlook.office.com/webhook/..."
});
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public void CanRun_ReturnsTrue_WhenWebhookChannelConfigured()
{
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Webhook:Endpoint"] = "https://api.example.com/webhook"
});
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public async Task RunAsync_Skips_WhenNoChannelsConfiguredToTest()
{
// Arrange - Channels exist but are disabled
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "false"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Skip);
}
[Fact]
public async Task RunAsync_TestsEmailSmtpConnectivity()
{
// Arrange - Use localhost which should fail quickly
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Channels:Email:SmtpHost"] = "localhost",
["Notify:Channels:Email:SmtpPort"] = "9999" // Unlikely to be open
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert - Should warn about connectivity failure
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("unreachable");
}
[Fact]
public async Task RunAsync_UsesDefaultSmtpPort_WhenNotSpecified()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Channels:Email:SmtpHost"] = "localhost"
// No port specified - should use 587
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert - Should attempt connection (will likely fail but uses correct port)
result.Should().NotBeNull();
}
[Fact]
public async Task RunAsync_ReportsInvalidWebhookUrl()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Slack:Enabled"] = "true",
["Notify:Channels:Slack:WebhookUrl"] = "not-a-valid-url"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public async Task RunAsync_IncludesRemediationForFailedChannels()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Channels:Email:SmtpHost"] = "nonexistent.example.com"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Remediation.Should().NotBeNull();
result.Remediation!.Steps.Should().NotBeEmpty();
}
[Fact]
public async Task RunAsync_ReportsMultipleChannelResults()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Channels:Email:SmtpHost"] = "localhost",
["Notify:Channels:Slack:Enabled"] = "true",
["Notify:Channels:Slack:WebhookUrl"] = "https://hooks.slack.com/test"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Evidence.Should().NotBeNull();
result.Evidence.Description.Should().Contain("Connectivity");
}
[Fact]
public async Task RunAsync_SkipsDisabledChannels()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "false",
["Notify:Channels:Email:SmtpHost"] = "smtp.example.com",
["Notify:Channels:Slack:Enabled"] = "true",
["Notify:Channels:Slack:WebhookUrl"] = "https://hooks.slack.com/test"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert - Email should not be tested since it's disabled
// But Slack should be tested
result.Should().NotBeNull();
}
[Fact]
public async Task RunAsync_IncludesVerificationCommand()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Channels:Email:SmtpHost"] = "localhost"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.VerificationCommand.Should().Contain("stella doctor --check check.notify.channel.connectivity");
}
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(configValues)
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
}

View File

@@ -0,0 +1,376 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Notify.Checks;
using Xunit;
namespace StellaOps.Doctor.Plugins.Notify.Tests.Checks;
[Trait("Category", "Unit")]
public sealed class NotifyDeliveryTestCheckTests
{
private readonly NotifyDeliveryTestCheck _check = new();
[Fact]
public void CheckId_ReturnsExpectedValue()
{
_check.CheckId.Should().Be("check.notify.delivery.test");
}
[Fact]
public void Name_ReturnsExpectedValue()
{
_check.Name.Should().Be("Notification Delivery Health");
}
[Fact]
public void Description_ReturnsExpectedValue()
{
_check.Description.Should().Contain("delivery");
}
[Fact]
public void DefaultSeverity_IsWarn()
{
_check.DefaultSeverity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public void Tags_ContainsExpectedValues()
{
_check.Tags.Should().Contain("notify");
_check.Tags.Should().Contain("delivery");
_check.Tags.Should().Contain("queue");
_check.Tags.Should().Contain("health");
}
[Fact]
public void EstimatedDuration_IsReasonable()
{
_check.EstimatedDuration.Should().BeLessThanOrEqualTo(TimeSpan.FromSeconds(5));
}
[Fact]
public void CanRun_ReturnsFalse_WhenNotifyNotConfigured()
{
var context = CreateContext(new Dictionary<string, string?>());
_check.CanRun(context).Should().BeFalse();
}
[Fact]
public void CanRun_ReturnsTrue_WhenNotifySectionExists()
{
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true"
});
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public async Task RunAsync_Passes_WithDefaultConfiguration()
{
// Arrange - Notify section exists but no specific delivery settings
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert - Should pass with defaults
result.Severity.Should().BeOneOf(DoctorSeverity.Pass, DoctorSeverity.Warn);
}
[Fact]
public async Task RunAsync_Warns_WhenNoQueueTransportConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true"
// No queue transport configured
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
if (result.Severity == DoctorSeverity.Warn && result.LikelyCauses is not null)
{
result.LikelyCauses.Should().Contain(c => c.Contains("queue") || c.Contains("in-memory"));
}
}
[Fact]
public async Task RunAsync_Passes_WhenRedisQueueConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Queue:Transport"] = "Redis",
["Notify:Queue:Redis:ConnectionString"] = "localhost:6379"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().BeOneOf(DoctorSeverity.Pass, DoctorSeverity.Warn);
}
[Fact]
public async Task RunAsync_Passes_WhenNatsQueueConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Queue:Transport"] = "NATS",
["Notify:Queue:Nats:Url"] = "nats://localhost:4222"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().BeOneOf(DoctorSeverity.Pass, DoctorSeverity.Warn);
}
[Fact]
public async Task RunAsync_Fails_WhenMaxRetriesIsZero()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Delivery:MaxRetries"] = "0"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
}
[Fact]
public async Task RunAsync_Fails_WhenMaxRetriesIsNegative()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Delivery:MaxRetries"] = "-1"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
}
[Fact]
public async Task RunAsync_Warns_WhenMaxRetriesIsVeryHigh()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Delivery:MaxRetries"] = "15"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public async Task RunAsync_Warns_WhenThrottleLimitIsVeryLow()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Throttle:Enabled"] = "true",
["Notify:Throttle:Limit"] = "5"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public async Task RunAsync_Passes_WhenThrottleConfiguredProperly()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Throttle:Enabled"] = "true",
["Notify:Throttle:Limit"] = "100",
["Notify:Throttle:Window"] = "1m"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().BeOneOf(DoctorSeverity.Pass, DoctorSeverity.Warn);
}
[Fact]
public async Task RunAsync_IncludesEvidenceForConfiguration()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Queue:Transport"] = "Redis",
["Notify:Delivery:MaxRetries"] = "5"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Evidence.Should().NotBeNull();
result.Evidence.Description.Should().Contain("Delivery");
}
[Fact]
public async Task RunAsync_IncludesDefaultChannel_WhenConfigured()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:DefaultChannel"] = "email"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Evidence.Should().NotBeNull();
}
[Fact]
public async Task RunAsync_IncludesRemediation_OnFailure()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Delivery:MaxRetries"] = "0"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Remediation.Should().NotBeNull();
result.Remediation!.Steps.Should().NotBeEmpty();
}
[Fact]
public async Task RunAsync_IncludesVerificationCommand()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
if (result.Severity != DoctorSeverity.Pass)
{
result.VerificationCommand.Should().Contain("stella doctor --check check.notify.delivery.test");
}
}
[Fact]
public async Task RunAsync_FallsBackToRedisConnectionString_WhenTransportNotSet()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["ConnectionStrings:Redis"] = "localhost:6379"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert - Should detect Redis from connection string
result.Severity.Should().BeOneOf(DoctorSeverity.Pass, DoctorSeverity.Warn);
}
[Fact]
public async Task RunAsync_FallsBackToNatsUrl_WhenTransportNotSet()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Nats:Url"] = "nats://localhost:4222"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert - Should detect NATS from global config
result.Severity.Should().BeOneOf(DoctorSeverity.Pass, DoctorSeverity.Warn);
}
[Fact]
public async Task RunAsync_ReportsDigestConfiguration()
{
// Arrange
var context = CreateContext(new Dictionary<string, string?>
{
["Notify:Channels:Email:Enabled"] = "true",
["Notify:Digest:Enabled"] = "true",
["Notify:Digest:Interval"] = "1h"
});
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Evidence.Should().NotBeNull();
result.Evidence.Data.Should().ContainKey("DigestEnabled");
}
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(configValues)
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
}

View File

@@ -0,0 +1,115 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Notify.Checks;
using Xunit;
namespace StellaOps.Doctor.Plugins.Notify.Tests;
[Trait("Category", "Unit")]
public sealed class NotifyPluginTests
{
private readonly NotifyPlugin _plugin = new();
[Fact]
public void PluginId_ReturnsExpectedValue()
{
_plugin.PluginId.Should().Be("stellaops.doctor.notify");
}
[Fact]
public void DisplayName_ReturnsExpectedValue()
{
_plugin.DisplayName.Should().Be("Notifications");
}
[Fact]
public void Category_ReturnsNotify()
{
_plugin.Category.Should().Be(DoctorCategory.Notify);
}
[Fact]
public void Version_ReturnsVersion1()
{
_plugin.Version.Should().Be(new Version(1, 0, 0));
}
[Fact]
public void MinEngineVersion_ReturnsVersion1()
{
_plugin.MinEngineVersion.Should().Be(new Version(1, 0, 0));
}
[Fact]
public void IsAvailable_ReturnsTrue()
{
var services = new ServiceCollection().BuildServiceProvider();
_plugin.IsAvailable(services).Should().BeTrue();
}
[Fact]
public void GetChecks_ReturnsThreeChecks()
{
var context = CreateContext(new Dictionary<string, string?>());
var checks = _plugin.GetChecks(context);
checks.Should().HaveCount(3);
}
[Fact]
public void GetChecks_ContainsChannelConfigurationCheck()
{
var context = CreateContext(new Dictionary<string, string?>());
var checks = _plugin.GetChecks(context);
checks.Should().ContainSingle(c => c is NotifyChannelConfigurationCheck);
}
[Fact]
public void GetChecks_ContainsChannelConnectivityCheck()
{
var context = CreateContext(new Dictionary<string, string?>());
var checks = _plugin.GetChecks(context);
checks.Should().ContainSingle(c => c is NotifyChannelConnectivityCheck);
}
[Fact]
public void GetChecks_ContainsDeliveryTestCheck()
{
var context = CreateContext(new Dictionary<string, string?>());
var checks = _plugin.GetChecks(context);
checks.Should().ContainSingle(c => c is NotifyDeliveryTestCheck);
}
[Fact]
public async Task InitializeAsync_CompletesSuccessfully()
{
var context = CreateContext(new Dictionary<string, string?>());
var act = () => _plugin.InitializeAsync(context, CancellationToken.None);
await act.Should().NotThrowAsync();
}
private static DoctorPluginContext CreateContext(Dictionary<string, string?> configValues)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(configValues)
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Doctor\StellaOps.Doctor.csproj" />
<ProjectReference Include="..\..\StellaOps.Doctor.Plugins.Notify\StellaOps.Doctor.Plugins.Notify.csproj" />
<ProjectReference Include="..\..\StellaOps.TestKit\StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,271 @@
using StellaOps.Orchestrator.Schemas;
using System.Text.Json;
using Xunit;
namespace StellaOps.Orchestrator.Schemas.Tests;
/// <summary>
/// Tests for OrchestratorEnvelope generic record.
/// </summary>
public sealed class OrchestratorEnvelopeTests
{
[Fact]
public void OrchestratorEnvelope_RequiredProperties_HaveDefaults()
{
var envelope = new OrchestratorEnvelope<string>
{
Payload = "test"
};
Assert.Equal(Guid.Empty, envelope.EventId);
Assert.Equal(string.Empty, envelope.Kind);
Assert.Equal(0, envelope.Version);
Assert.Equal(string.Empty, envelope.Tenant);
Assert.Equal(string.Empty, envelope.Source);
Assert.Equal(string.Empty, envelope.IdempotencyKey);
Assert.Equal("test", envelope.Payload);
}
[Fact]
public void OrchestratorEnvelope_WithAllProperties_ContainsValues()
{
var eventId = Guid.NewGuid();
var occurredAt = DateTimeOffset.UtcNow;
var recordedAt = occurredAt.AddSeconds(1);
var envelope = new OrchestratorEnvelope<string>
{
EventId = eventId,
Kind = OrchestratorEventKinds.ScannerReportReady,
Version = 1,
Tenant = "tenant-001",
OccurredAt = occurredAt,
RecordedAt = recordedAt,
Source = "scanner-service",
IdempotencyKey = "idem-key-123",
CorrelationId = "corr-456",
TraceId = "trace-789",
SpanId = "span-abc",
Payload = "report-data"
};
Assert.Equal(eventId, envelope.EventId);
Assert.Equal(OrchestratorEventKinds.ScannerReportReady, envelope.Kind);
Assert.Equal(1, envelope.Version);
Assert.Equal("tenant-001", envelope.Tenant);
Assert.Equal(occurredAt, envelope.OccurredAt);
Assert.Equal(recordedAt, envelope.RecordedAt);
Assert.Equal("scanner-service", envelope.Source);
Assert.Equal("idem-key-123", envelope.IdempotencyKey);
Assert.Equal("corr-456", envelope.CorrelationId);
}
[Fact]
public void OrchestratorEnvelope_WithScope_ContainsScope()
{
var scope = new OrchestratorScope
{
Namespace = "production",
Repo = "myapp",
Digest = "sha256:abc123",
Component = "api",
Image = "myapp:v1.0.0"
};
var envelope = new OrchestratorEnvelope<string>
{
Scope = scope,
Payload = "test"
};
Assert.NotNull(envelope.Scope);
Assert.Equal("production", envelope.Scope.Namespace);
Assert.Equal("myapp", envelope.Scope.Repo);
Assert.Equal("sha256:abc123", envelope.Scope.Digest);
}
[Fact]
public void OrchestratorEnvelope_WithAttributes_ContainsAttributes()
{
var attributes = new Dictionary<string, string>
{
["region"] = "us-east-1",
["environment"] = "staging"
};
var envelope = new OrchestratorEnvelope<string>
{
Attributes = attributes,
Payload = "test"
};
Assert.NotNull(envelope.Attributes);
Assert.Equal(2, envelope.Attributes.Count);
Assert.Equal("us-east-1", envelope.Attributes["region"]);
}
[Fact]
public void OrchestratorEnvelope_OptionalProperties_AreNullByDefault()
{
var envelope = new OrchestratorEnvelope<string>
{
Payload = "test"
};
Assert.Null(envelope.RecordedAt);
Assert.Null(envelope.CorrelationId);
Assert.Null(envelope.TraceId);
Assert.Null(envelope.SpanId);
Assert.Null(envelope.Scope);
Assert.Null(envelope.Attributes);
}
[Fact]
public void OrchestratorEnvelope_GenericPayload_WorksWithComplexTypes()
{
var payload = new ScannerReportReadyPayload
{
ReportId = "report-001",
Verdict = "PASS"
};
var envelope = new OrchestratorEnvelope<ScannerReportReadyPayload>
{
Kind = OrchestratorEventKinds.ScannerReportReady,
Payload = payload
};
Assert.Equal("report-001", envelope.Payload.ReportId);
Assert.Equal("PASS", envelope.Payload.Verdict);
}
}
/// <summary>
/// Tests for OrchestratorScope record.
/// </summary>
public sealed class OrchestratorScopeTests
{
[Fact]
public void OrchestratorScope_RequiredProperties_HaveDefaults()
{
var scope = new OrchestratorScope();
Assert.Null(scope.Namespace);
Assert.Equal(string.Empty, scope.Repo);
Assert.Equal(string.Empty, scope.Digest);
Assert.Null(scope.Component);
Assert.Null(scope.Image);
}
[Fact]
public void OrchestratorScope_WithAllProperties_ContainsValues()
{
var scope = new OrchestratorScope
{
Namespace = "default",
Repo = "registry.example.com/myapp",
Digest = "sha256:1234567890abcdef",
Component = "backend",
Image = "myapp:latest"
};
Assert.Equal("default", scope.Namespace);
Assert.Equal("registry.example.com/myapp", scope.Repo);
Assert.Equal("sha256:1234567890abcdef", scope.Digest);
Assert.Equal("backend", scope.Component);
Assert.Equal("myapp:latest", scope.Image);
}
[Fact]
public void OrchestratorScope_RecordEquality_WorksCorrectly()
{
var scope1 = new OrchestratorScope
{
Repo = "myapp",
Digest = "sha256:abc"
};
var scope2 = new OrchestratorScope
{
Repo = "myapp",
Digest = "sha256:abc"
};
Assert.Equal(scope1, scope2);
}
}
/// <summary>
/// Tests for OrchestratorEventKinds constants.
/// </summary>
public sealed class OrchestratorEventKindsTests
{
[Fact]
public void OrchestratorEventKinds_ScannerReportReady_HasCorrectValue()
{
Assert.Equal("scanner.event.report.ready", OrchestratorEventKinds.ScannerReportReady);
}
[Fact]
public void OrchestratorEventKinds_ScannerScanCompleted_HasCorrectValue()
{
Assert.Equal("scanner.event.scan.completed", OrchestratorEventKinds.ScannerScanCompleted);
}
}
/// <summary>
/// Tests for ScannerReportReadyPayload record.
/// </summary>
public sealed class ScannerReportReadyPayloadTests
{
[Fact]
public void ScannerReportReadyPayload_RequiredProperties_HaveDefaults()
{
var payload = new ScannerReportReadyPayload();
Assert.Equal(string.Empty, payload.ReportId);
Assert.Null(payload.ScanId);
Assert.Null(payload.ImageDigest);
Assert.Equal(string.Empty, payload.Verdict);
Assert.NotNull(payload.Summary);
Assert.NotNull(payload.Links);
}
[Fact]
public void ScannerReportReadyPayload_WithAllProperties_ContainsValues()
{
var generatedAt = DateTimeOffset.UtcNow;
var payload = new ScannerReportReadyPayload
{
ReportId = "rpt-001",
ScanId = "scan-001",
ImageDigest = "sha256:xyz789",
GeneratedAt = generatedAt,
Verdict = "FAIL",
QuietedFindingCount = 5
};
Assert.Equal("rpt-001", payload.ReportId);
Assert.Equal("scan-001", payload.ScanId);
Assert.Equal("sha256:xyz789", payload.ImageDigest);
Assert.Equal(generatedAt, payload.GeneratedAt);
Assert.Equal("FAIL", payload.Verdict);
Assert.Equal(5, payload.QuietedFindingCount);
}
[Theory]
[InlineData("PASS")]
[InlineData("FAIL")]
[InlineData("WARN")]
[InlineData("UNKNOWN")]
public void ScannerReportReadyPayload_Verdict_SupportedValues(string verdict)
{
var payload = new ScannerReportReadyPayload
{
Verdict = verdict
};
Assert.Equal(verdict, payload.Verdict);
}
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
<UseXunitV3>true</UseXunitV3>
</PropertyGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Orchestrator.Schemas\StellaOps.Orchestrator.Schemas.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,7 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"diagnosticMessages": true,
"parallelizeAssembly": true,
"parallelizeTestCollections": true,
"maxParallelThreads": -1
}

View File

@@ -324,7 +324,7 @@ internal static class LatticeArbs
public static Arbitrary<List<EvidenceType>> EvidenceSequence(int minLength, int maxLength) =>
(from length in Gen.Choose(minLength, maxLength)
from sequence in Gen.ListOf(length, Gen.Elements(AllEvidenceTypes))
from sequence in Gen.ListOf<EvidenceType>(Gen.Elements(AllEvidenceTypes), length)
select sequence.ToList()).ToArbitrary();
public static Arbitrary<(LatticeState, EvidenceType)> ReinforcingEvidencePair()
@@ -345,7 +345,8 @@ internal static class LatticeArbs
{
Purl = "pkg:npm/test@1.0.0",
Namespace = "test",
SymbolName = "testFunc"
Type = "_",
Method = "testFunc"
};
var reachableResult = new StaticReachabilityResult
@@ -381,7 +382,8 @@ internal static class LatticeArbs
{
Purl = "pkg:npm/test@1.0.0",
Namespace = "test",
SymbolName = "testFunc"
Type = "_",
Method = "testFunc"
};
var now = DateTimeOffset.UtcNow;

View File

@@ -0,0 +1,181 @@
using StellaOps.Signals.Contracts;
using Xunit;
namespace StellaOps.Signals.Contracts.Tests;
/// <summary>
/// Tests for SignalEnvelope model.
/// </summary>
public sealed class SignalEnvelopeTests
{
[Fact]
public void SignalEnvelope_RequiredProperties_MustBeSet()
{
var envelope = new SignalEnvelope
{
SignalKey = "pkg:npm/lodash@4.17.21:reachability",
SignalType = SignalType.Reachability,
Value = new { reachable = true, confidence = 0.95 },
ComputedAt = DateTimeOffset.UtcNow,
SourceService = "reachability-analyzer"
};
Assert.Equal("pkg:npm/lodash@4.17.21:reachability", envelope.SignalKey);
Assert.Equal(SignalType.Reachability, envelope.SignalType);
Assert.Equal("reachability-analyzer", envelope.SourceService);
}
[Theory]
[InlineData(SignalType.Reachability)]
[InlineData(SignalType.Entropy)]
[InlineData(SignalType.Exploitability)]
[InlineData(SignalType.Trust)]
[InlineData(SignalType.UnknownSymbol)]
[InlineData(SignalType.Custom)]
public void SignalEnvelope_SignalType_AllValues_AreValid(SignalType type)
{
var envelope = new SignalEnvelope
{
SignalKey = $"test:{type.ToString().ToLowerInvariant()}",
SignalType = type,
Value = new { test = true },
ComputedAt = DateTimeOffset.UtcNow,
SourceService = "test-service"
};
Assert.Equal(type, envelope.SignalType);
}
[Fact]
public void SignalEnvelope_DefaultSchemaVersion_IsOne()
{
var envelope = new SignalEnvelope
{
SignalKey = "test:schema",
SignalType = SignalType.Custom,
Value = new { },
ComputedAt = DateTimeOffset.UtcNow,
SourceService = "test"
};
Assert.Equal("1.0", envelope.SchemaVersion);
}
[Fact]
public void SignalEnvelope_OptionalProperties_AreNullByDefault()
{
var envelope = new SignalEnvelope
{
SignalKey = "test:optional",
SignalType = SignalType.Reachability,
Value = new { },
ComputedAt = DateTimeOffset.UtcNow,
SourceService = "test"
};
Assert.Null(envelope.TenantId);
Assert.Null(envelope.CorrelationId);
Assert.Null(envelope.ProvenanceDigest);
}
[Fact]
public void SignalEnvelope_WithAllOptionalProperties_ContainsValues()
{
var envelope = new SignalEnvelope
{
SignalKey = "pkg:pypi/django@4.2.0:trust",
SignalType = SignalType.Trust,
Value = new { score = 0.85 },
ComputedAt = DateTimeOffset.UtcNow,
SourceService = "trust-engine",
TenantId = "tenant-001",
CorrelationId = "corr-abc123",
ProvenanceDigest = "sha256:xyz789",
SchemaVersion = "2.0"
};
Assert.Equal("tenant-001", envelope.TenantId);
Assert.Equal("corr-abc123", envelope.CorrelationId);
Assert.Equal("sha256:xyz789", envelope.ProvenanceDigest);
Assert.Equal("2.0", envelope.SchemaVersion);
}
[Fact]
public void SignalEnvelope_Value_CanBeAnyObject()
{
var reachabilityValue = new
{
reachable = true,
paths = new[] { "main->helper->vulnerable" },
confidence = 0.92
};
var envelope = new SignalEnvelope
{
SignalKey = "test:value",
SignalType = SignalType.Reachability,
Value = reachabilityValue,
ComputedAt = DateTimeOffset.UtcNow,
SourceService = "analyzer"
};
Assert.NotNull(envelope.Value);
}
[Fact]
public void SignalEnvelope_RecordEquality_WorksCorrectly()
{
var computedAt = DateTimeOffset.UtcNow;
var value = new { test = 123 };
var envelope1 = new SignalEnvelope
{
SignalKey = "test:eq",
SignalType = SignalType.Entropy,
Value = value,
ComputedAt = computedAt,
SourceService = "test"
};
var envelope2 = new SignalEnvelope
{
SignalKey = "test:eq",
SignalType = SignalType.Entropy,
Value = value,
ComputedAt = computedAt,
SourceService = "test"
};
Assert.Equal(envelope1, envelope2);
}
}
/// <summary>
/// Tests for SignalType enum.
/// </summary>
public sealed class SignalTypeTests
{
[Fact]
public void SignalType_AllDefinedValues_AreCounted()
{
var values = Enum.GetValues<SignalType>();
// Ensure we have expected signal types
Assert.Equal(6, values.Length);
}
[Fact]
public void SignalType_Reachability_HasValue()
{
Assert.Equal(0, (int)SignalType.Reachability);
}
[Fact]
public void SignalType_Custom_IsLast()
{
var values = Enum.GetValues<SignalType>();
var last = values.Max();
Assert.Equal(SignalType.Custom, last);
}
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
<UseXunitV3>true</UseXunitV3>
</PropertyGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Signals.Contracts\StellaOps.Signals.Contracts.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,7 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"diagnosticMessages": true,
"parallelizeAssembly": true,
"parallelizeTestCollections": true,
"maxParallelThreads": -1
}