sprints work.

This commit is contained in:
master
2026-01-20 00:45:38 +02:00
parent b34bde89fa
commit 4903395618
275 changed files with 52785 additions and 79 deletions

View File

@@ -0,0 +1,175 @@
// -----------------------------------------------------------------------------
// BinaryAnalysisDoctorPluginTests.cs
// Sprint: SPRINT_20260119_003_Doctor_binary_analysis_checks
// Task: DBIN-001 - Binary Analysis Doctor Plugin Scaffold
// Description: Unit tests for BinaryAnalysisDoctorPlugin
// -----------------------------------------------------------------------------
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Doctor.Plugins;
using Xunit;
namespace StellaOps.Doctor.Plugin.BinaryAnalysis.Tests;
[Trait("Category", "Unit")]
public class BinaryAnalysisDoctorPluginTests
{
private readonly BinaryAnalysisDoctorPlugin _plugin = new();
[Fact]
public void PluginId_ReturnsExpectedValue()
{
// Assert
_plugin.PluginId.Should().Be("stellaops.doctor.binaryanalysis");
}
[Fact]
public void Category_IsSecurity()
{
// Assert
_plugin.Category.Should().Be(DoctorCategory.Security);
}
[Fact]
public void DisplayName_IsBinaryAnalysis()
{
// Assert
_plugin.DisplayName.Should().Be("Binary Analysis");
}
[Fact]
public void IsAvailable_ReturnsTrue_Always()
{
// Arrange
var services = new ServiceCollection().BuildServiceProvider();
// Act & Assert
_plugin.IsAvailable(services).Should().BeTrue();
}
[Fact]
public void GetChecks_ReturnsAtLeastOneCheck()
{
// Arrange
var context = CreateContext();
// Act
var checks = _plugin.GetChecks(context);
// Assert
checks.Should().NotBeEmpty();
checks.Should().HaveCountGreaterThanOrEqualTo(1);
}
[Fact]
public void GetChecks_ContainsDebuginfodCheck()
{
// Arrange
var context = CreateContext();
// Act
var checks = _plugin.GetChecks(context);
// Assert
checks.Select(c => c.CheckId).Should().Contain("check.binaryanalysis.debuginfod.available");
}
[Fact]
public void GetChecks_ContainsDdebCheck()
{
// Arrange
var context = CreateContext();
// Act
var checks = _plugin.GetChecks(context);
// Assert
checks.Select(c => c.CheckId).Should().Contain("check.binaryanalysis.ddeb.enabled");
}
[Fact]
public void GetChecks_ContainsBuildinfoCacheCheck()
{
// Arrange
var context = CreateContext();
// Act
var checks = _plugin.GetChecks(context);
// Assert
checks.Select(c => c.CheckId).Should().Contain("check.binaryanalysis.buildinfo.cache");
}
[Fact]
public void GetChecks_ContainsSymbolRecoveryFallbackCheck()
{
// Arrange
var context = CreateContext();
// Act
var checks = _plugin.GetChecks(context);
// Assert
checks.Select(c => c.CheckId).Should().Contain("check.binaryanalysis.symbol.recovery.fallback");
}
[Fact]
public void GetChecks_ReturnsFourChecks()
{
// Arrange
var context = CreateContext();
// Act
var checks = _plugin.GetChecks(context);
// Assert
checks.Should().HaveCount(4);
}
[Fact]
public async Task InitializeAsync_CompletesWithoutError()
{
// Arrange
var context = CreateContext();
// Act & Assert
await _plugin.Invoking(p => p.InitializeAsync(context, CancellationToken.None))
.Should().NotThrowAsync();
}
[Fact]
public void Version_IsNotNull()
{
// Assert
_plugin.Version.Should().NotBeNull();
_plugin.Version.Major.Should().BeGreaterThanOrEqualTo(1);
}
[Fact]
public void MinEngineVersion_IsNotNull()
{
// Assert
_plugin.MinEngineVersion.Should().NotBeNull();
_plugin.MinEngineVersion.Major.Should().BeGreaterThanOrEqualTo(1);
}
private static DoctorPluginContext CreateContext()
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.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,214 @@
// -----------------------------------------------------------------------------
// BuildinfoCacheCheckTests.cs
// Sprint: SPRINT_20260119_003_Doctor_binary_analysis_checks
// Task: DBIN-004 - Buildinfo Cache Check
// Description: Unit tests for BuildinfoCacheCheck
// -----------------------------------------------------------------------------
using System.Net;
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Moq.Protected;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugin.BinaryAnalysis.Checks;
using StellaOps.Doctor.Plugins;
using Xunit;
namespace StellaOps.Doctor.Plugin.BinaryAnalysis.Tests.Checks;
[Trait("Category", "Unit")]
public class BuildinfoCacheCheckTests
{
private readonly BuildinfoCacheCheck _check = new();
[Fact]
public void CheckId_ReturnsExpectedValue()
{
// Assert
_check.CheckId.Should().Be("check.binaryanalysis.buildinfo.cache");
}
[Fact]
public void Name_ReturnsDebianBuildinfoCache()
{
// Assert
_check.Name.Should().Be("Debian Buildinfo Cache");
}
[Fact]
public void DefaultSeverity_IsWarn()
{
// Assert
_check.DefaultSeverity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public void Tags_ContainsBuildinfo()
{
// Assert
_check.Tags.Should().Contain("buildinfo");
_check.Tags.Should().Contain("debian");
_check.Tags.Should().Contain("cache");
}
[Fact]
public void CanRun_ReturnsTrue_Always()
{
// Arrange
var context = CreateContext();
// Act & Assert
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public async Task RunAsync_IncludesVerificationCommand()
{
// Arrange
var mockHandler = CreateMockHttpHandler(HttpStatusCode.OK);
var context = CreateContextWithHttpClient(mockHandler);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.VerificationCommand.Should().NotBeNullOrEmpty();
result.VerificationCommand.Should().Contain("stella doctor --check");
}
[Fact]
public async Task RunAsync_ReturnsWarningOrPass_WhenServicesReachable()
{
// Arrange
var mockHandler = CreateMockHttpHandler(HttpStatusCode.OK);
var context = CreateContextWithHttpClient(mockHandler);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert - should be Pass, Info, or Warn (not Fail) when services are reachable
result.Severity.Should().NotBe(DoctorSeverity.Fail);
}
[Fact]
public async Task RunAsync_IncludesEvidence_WithServiceStatus()
{
// Arrange
var mockHandler = CreateMockHttpHandler(HttpStatusCode.OK);
var context = CreateContextWithHttpClient(mockHandler);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Evidence.Should().NotBeNull();
result.Evidence.Data.Should().ContainKey("buildinfos_debian_net_reachable");
}
[Fact]
public async Task RunAsync_ReturnsFailOrWarn_WhenServicesUnreachable()
{
// Arrange
var mockHandler = CreateMockHttpHandler(throwException: true);
var context = CreateContextWithHttpClient(mockHandler);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert - should be Fail or Warn when services unreachable
result.Severity.Should().BeOneOf(DoctorSeverity.Fail, DoctorSeverity.Warn);
}
[Fact]
public void EstimatedDuration_IsReasonable()
{
// Assert
_check.EstimatedDuration.Should().BeGreaterThan(TimeSpan.Zero);
_check.EstimatedDuration.Should().BeLessThanOrEqualTo(TimeSpan.FromSeconds(30));
}
[Fact]
public void Description_IsNotEmpty()
{
// Assert
_check.Description.Should().NotBeNullOrEmpty();
}
private static DoctorPluginContext CreateContext()
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
private static DoctorPluginContext CreateContextWithHttpClient(Mock<HttpMessageHandler> mockHandler)
{
var httpClient = new HttpClient(mockHandler.Object);
var mockFactory = new Mock<IHttpClientFactory>();
mockFactory.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient);
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.Build();
var services = new ServiceCollection()
.AddSingleton(mockFactory.Object)
.BuildServiceProvider();
return new DoctorPluginContext
{
Services = services,
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
private static Mock<HttpMessageHandler> CreateMockHttpHandler(
HttpStatusCode statusCode = HttpStatusCode.OK,
bool throwException = false)
{
var mockHandler = new Mock<HttpMessageHandler>();
if (throwException)
{
mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ThrowsAsync(new HttpRequestException("Connection refused"));
}
else
{
mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = statusCode
});
}
return mockHandler;
}
}

View File

@@ -0,0 +1,129 @@
// -----------------------------------------------------------------------------
// DdebRepoEnabledCheckTests.cs
// Sprint: SPRINT_20260119_003_Doctor_binary_analysis_checks
// Task: DBIN-003 - Ddeb Repository Check
// Description: Unit tests for DdebRepoEnabledCheck
// -----------------------------------------------------------------------------
using System.Net;
using System.Runtime.InteropServices;
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Moq.Protected;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugin.BinaryAnalysis.Checks;
using StellaOps.Doctor.Plugins;
using Xunit;
namespace StellaOps.Doctor.Plugin.BinaryAnalysis.Tests.Checks;
[Trait("Category", "Unit")]
public class DdebRepoEnabledCheckTests
{
private readonly DdebRepoEnabledCheck _check = new();
[Fact]
public void CheckId_ReturnsExpectedValue()
{
// Assert
_check.CheckId.Should().Be("check.binaryanalysis.ddeb.enabled");
}
[Fact]
public void Name_ReturnsUbuntuDdebRepository()
{
// Assert
_check.Name.Should().Be("Ubuntu Ddeb Repository");
}
[Fact]
public void DefaultSeverity_IsWarn()
{
// Assert
_check.DefaultSeverity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public void Tags_ContainsDdeb()
{
// Assert
_check.Tags.Should().Contain("ddeb");
_check.Tags.Should().Contain("ubuntu");
_check.Tags.Should().Contain("binaryanalysis");
}
[Fact]
public void CanRun_ReturnsFalse_OnWindows()
{
// Arrange
var context = CreateContext();
// Act
var canRun = _check.CanRun(context);
// Assert
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
canRun.Should().BeFalse();
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
canRun.Should().BeTrue();
}
}
[Fact]
public async Task RunAsync_ReturnsSkip_OnNonLinux()
{
// Arrange
var context = CreateContext();
// Skip this test on Linux
if (OperatingSystem.IsLinux())
{
return;
}
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Skip);
result.Diagnosis.Should().Contain("Linux");
}
[Fact]
public void EstimatedDuration_IsReasonable()
{
// Assert
_check.EstimatedDuration.Should().BeGreaterThan(TimeSpan.Zero);
_check.EstimatedDuration.Should().BeLessThanOrEqualTo(TimeSpan.FromSeconds(30));
}
[Fact]
public void Description_IsNotEmpty()
{
// Assert
_check.Description.Should().NotBeNullOrEmpty();
}
private static DoctorPluginContext CreateContext()
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.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,294 @@
// -----------------------------------------------------------------------------
// DebuginfodAvailabilityCheckTests.cs
// Sprint: SPRINT_20260119_003_Doctor_binary_analysis_checks
// Task: DBIN-002 - Debuginfod Availability Check
// Description: Unit tests for DebuginfodAvailabilityCheck with mocked HTTP
// -----------------------------------------------------------------------------
using System.Net;
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Moq.Protected;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugin.BinaryAnalysis.Checks;
using StellaOps.Doctor.Plugins;
using Xunit;
namespace StellaOps.Doctor.Plugin.BinaryAnalysis.Tests.Checks;
[Trait("Category", "Unit")]
public class DebuginfodAvailabilityCheckTests
{
private readonly DebuginfodAvailabilityCheck _check = new();
[Fact]
public void CheckId_ReturnsExpectedValue()
{
// Assert
_check.CheckId.Should().Be("check.binaryanalysis.debuginfod.available");
}
[Fact]
public void Name_ReturnsDebuginfodAvailability()
{
// Assert
_check.Name.Should().Be("Debuginfod Availability");
}
[Fact]
public void DefaultSeverity_IsWarn()
{
// Assert
_check.DefaultSeverity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public void Tags_ContainsBinaryAnalysis()
{
// Assert
_check.Tags.Should().Contain("binaryanalysis");
_check.Tags.Should().Contain("debuginfod");
_check.Tags.Should().Contain("symbols");
}
[Fact]
public void CanRun_ReturnsTrue_Always()
{
// Arrange
var context = CreateContext();
// Act & Assert
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public async Task RunAsync_ReturnsPass_WhenDebuginfodReachable()
{
// Arrange
var mockHandler = CreateMockHttpHandler(HttpStatusCode.OK);
var context = CreateContextWithHttpClient(mockHandler);
// Set environment variable for test
var originalValue = Environment.GetEnvironmentVariable("DEBUGINFOD_URLS");
try
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", "https://debuginfod.example.com");
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("reachable");
}
finally
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", originalValue);
}
}
[Fact]
public async Task RunAsync_ReturnsInfo_WhenDefaultUrlReachableButEnvNotSet()
{
// Arrange
var mockHandler = CreateMockHttpHandler(HttpStatusCode.OK);
var context = CreateContextWithHttpClient(mockHandler);
var originalValue = Environment.GetEnvironmentVariable("DEBUGINFOD_URLS");
try
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", null);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Info);
result.Diagnosis.Should().Contain("not configured");
}
finally
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", originalValue);
}
}
[Fact]
public async Task RunAsync_ReturnsFail_WhenAllUrlsUnreachable()
{
// Arrange
var mockHandler = CreateMockHttpHandler(throwException: true);
var context = CreateContextWithHttpClient(mockHandler);
var originalValue = Environment.GetEnvironmentVariable("DEBUGINFOD_URLS");
try
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", "https://debuginfod.example.com");
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("None");
result.Diagnosis.Should().Contain("reachable");
}
finally
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", originalValue);
}
}
[Fact]
public async Task RunAsync_IncludesRemediationSteps_OnFailure()
{
// Arrange
var mockHandler = CreateMockHttpHandler(throwException: true);
var context = CreateContextWithHttpClient(mockHandler);
var originalValue = Environment.GetEnvironmentVariable("DEBUGINFOD_URLS");
try
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", "https://debuginfod.example.com");
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Remediation.Should().NotBeNull();
result.Remediation!.Steps.Should().NotBeEmpty();
}
finally
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", originalValue);
}
}
[Fact]
public async Task RunAsync_ParsesMultipleUrls()
{
// Arrange - all will return OK since we use the same mock
var mockHandler = CreateMockHttpHandler(HttpStatusCode.OK);
var context = CreateContextWithHttpClient(mockHandler);
var originalValue = Environment.GetEnvironmentVariable("DEBUGINFOD_URLS");
try
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS",
"https://debuginfod1.example.com https://debuginfod2.example.com");
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("2"); // Should mention 2 URLs
}
finally
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", originalValue);
}
}
[Fact]
public async Task RunAsync_IncludesVerificationCommand()
{
// Arrange
var mockHandler = CreateMockHttpHandler(HttpStatusCode.OK);
var context = CreateContextWithHttpClient(mockHandler);
var originalValue = Environment.GetEnvironmentVariable("DEBUGINFOD_URLS");
try
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", "https://debuginfod.example.com");
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.VerificationCommand.Should().NotBeNullOrEmpty();
result.VerificationCommand.Should().Contain("stella doctor --check");
}
finally
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", originalValue);
}
}
private static DoctorPluginContext CreateContext()
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
private static DoctorPluginContext CreateContextWithHttpClient(Mock<HttpMessageHandler> mockHandler)
{
var httpClient = new HttpClient(mockHandler.Object);
var mockFactory = new Mock<IHttpClientFactory>();
mockFactory.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient);
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.Build();
var services = new ServiceCollection()
.AddSingleton(mockFactory.Object)
.BuildServiceProvider();
return new DoctorPluginContext
{
Services = services,
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
private static Mock<HttpMessageHandler> CreateMockHttpHandler(
HttpStatusCode statusCode = HttpStatusCode.OK,
bool throwException = false)
{
var mockHandler = new Mock<HttpMessageHandler>();
if (throwException)
{
mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ThrowsAsync(new HttpRequestException("Connection refused"));
}
else
{
mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = statusCode
});
}
return mockHandler;
}
}

View File

@@ -0,0 +1,277 @@
// -----------------------------------------------------------------------------
// SymbolRecoveryFallbackCheckTests.cs
// Sprint: SPRINT_20260119_003_Doctor_binary_analysis_checks
// Task: DBIN-005 - Symbol Recovery Fallback Check
// Description: Unit tests for SymbolRecoveryFallbackCheck
// -----------------------------------------------------------------------------
using System.Net;
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Moq.Protected;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugin.BinaryAnalysis.Checks;
using StellaOps.Doctor.Plugins;
using Xunit;
namespace StellaOps.Doctor.Plugin.BinaryAnalysis.Tests.Checks;
[Trait("Category", "Unit")]
public class SymbolRecoveryFallbackCheckTests
{
private readonly SymbolRecoveryFallbackCheck _check = new();
[Fact]
public void CheckId_ReturnsExpectedValue()
{
// Assert
_check.CheckId.Should().Be("check.binaryanalysis.symbol.recovery.fallback");
}
[Fact]
public void Name_ReturnsSymbolRecoveryFallback()
{
// Assert
_check.Name.Should().Be("Symbol Recovery Fallback");
}
[Fact]
public void DefaultSeverity_IsWarn()
{
// Assert
_check.DefaultSeverity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public void Tags_ContainsMeta()
{
// Assert
_check.Tags.Should().Contain("meta");
_check.Tags.Should().Contain("fallback");
_check.Tags.Should().Contain("symbols");
}
[Fact]
public void CanRun_ReturnsTrue_Always()
{
// Arrange
var context = CreateContext();
// Act & Assert
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public async Task RunAsync_IncludesVerificationCommand()
{
// Arrange
var mockHandler = CreateMockHttpHandler(HttpStatusCode.OK);
var context = CreateContextWithHttpClient(mockHandler);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.VerificationCommand.Should().NotBeNullOrEmpty();
result.VerificationCommand.Should().Contain("stella doctor --check");
}
[Fact]
public async Task RunAsync_IncludesEvidence_WithSourceCounts()
{
// Arrange
var mockHandler = CreateMockHttpHandler(HttpStatusCode.OK);
var context = CreateContextWithHttpClient(mockHandler);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Evidence.Should().NotBeNull();
result.Evidence.Data.Should().ContainKey("total_sources_checked");
result.Evidence.Data.Should().ContainKey("available_sources");
}
[Fact]
public async Task RunAsync_AggregatesChildCheckResults()
{
// Arrange
var mockHandler = CreateMockHttpHandler(HttpStatusCode.OK);
var context = CreateContextWithHttpClient(mockHandler);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert - should have evidence for multiple sources
result.Evidence.Data.Keys.Should().Contain(k => k.StartsWith("source_"));
}
[Fact]
public async Task RunAsync_ReturnsPassOrInfo_WhenAtLeastOneSourceAvailable()
{
// Arrange - at least debuginfod should succeed with mocked HTTP
var mockHandler = CreateMockHttpHandler(HttpStatusCode.OK);
var context = CreateContextWithHttpClient(mockHandler);
// Set DEBUGINFOD_URLS to ensure at least one source is available
var originalValue = Environment.GetEnvironmentVariable("DEBUGINFOD_URLS");
try
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", "https://debuginfod.example.com");
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert - should be Pass or Info when at least one source is available
result.Severity.Should().BeOneOf(DoctorSeverity.Pass, DoctorSeverity.Info);
}
finally
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", originalValue);
}
}
[Fact]
public async Task RunAsync_ReturnsFail_WhenNoSourcesAvailable()
{
// Arrange - all HTTP calls will fail
var mockHandler = CreateMockHttpHandler(throwException: true);
var context = CreateContextWithHttpClient(mockHandler);
// Ensure no DEBUGINFOD_URLS fallback
var originalValue = Environment.GetEnvironmentVariable("DEBUGINFOD_URLS");
try
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", "https://unreachable.example.com");
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("No symbol recovery sources");
}
finally
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", originalValue);
}
}
[Fact]
public async Task RunAsync_IncludesRemediation_WhenNoSourcesAvailable()
{
// Arrange
var mockHandler = CreateMockHttpHandler(throwException: true);
var context = CreateContextWithHttpClient(mockHandler);
var originalValue = Environment.GetEnvironmentVariable("DEBUGINFOD_URLS");
try
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", "https://unreachable.example.com");
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Remediation.Should().NotBeNull();
result.Remediation!.Steps.Should().NotBeEmpty();
}
finally
{
Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", originalValue);
}
}
[Fact]
public void EstimatedDuration_IsLargerThanChildChecks()
{
// The fallback check runs multiple child checks, so should have larger duration
_check.EstimatedDuration.Should().BeGreaterThanOrEqualTo(TimeSpan.FromSeconds(30));
}
[Fact]
public void Description_IsNotEmpty()
{
// Assert
_check.Description.Should().NotBeNullOrEmpty();
_check.Description.Should().Contain("at least one");
}
private static DoctorPluginContext CreateContext()
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
private static DoctorPluginContext CreateContextWithHttpClient(Mock<HttpMessageHandler> mockHandler)
{
var httpClient = new HttpClient(mockHandler.Object);
var mockFactory = new Mock<IHttpClientFactory>();
mockFactory.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient);
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.Build();
var services = new ServiceCollection()
.AddSingleton(mockFactory.Object)
.BuildServiceProvider();
return new DoctorPluginContext
{
Services = services,
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
private static Mock<HttpMessageHandler> CreateMockHttpHandler(
HttpStatusCode statusCode = HttpStatusCode.OK,
bool throwException = false)
{
var mockHandler = new Mock<HttpMessageHandler>();
if (throwException)
{
mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ThrowsAsync(new HttpRequestException("Connection refused"));
}
else
{
mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = statusCode
});
}
return mockHandler;
}
}

View File

@@ -0,0 +1,238 @@
// -----------------------------------------------------------------------------
// BinaryAnalysisPluginIntegrationTests.cs
// Sprint: SPRINT_20260119_003_Doctor_binary_analysis_checks
// Task: DBIN-006, DBIN-007 - Integration tests for plugin discovery and CLI behavior
// Description: Verifies plugin integration with Doctor engine and CLI filtering
// -----------------------------------------------------------------------------
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugin.BinaryAnalysis.DependencyInjection;
using StellaOps.Doctor.Plugins;
using Xunit;
namespace StellaOps.Doctor.Plugin.BinaryAnalysis.Tests.Integration;
/// <summary>
/// Integration tests verifying plugin registration and discovery behavior.
/// </summary>
[Trait("Category", "Integration")]
public class BinaryAnalysisPluginIntegrationTests
{
[Fact]
public void AddDoctorBinaryAnalysisPlugin_RegistersPluginAsSingleton()
{
// Arrange
var services = new ServiceCollection();
// Act
services.AddDoctorBinaryAnalysisPlugin();
var provider = services.BuildServiceProvider();
// Assert
var plugins = provider.GetServices<IDoctorPlugin>().ToList();
plugins.Should().ContainSingle(p => p.PluginId == "stellaops.doctor.binaryanalysis");
}
[Fact]
public void AddDoctorBinaryAnalysisPlugin_PluginHasSecurityCategory()
{
// Arrange
var services = new ServiceCollection();
services.AddDoctorBinaryAnalysisPlugin();
var provider = services.BuildServiceProvider();
// Act
var plugin = provider.GetServices<IDoctorPlugin>()
.Single(p => p.PluginId == "stellaops.doctor.binaryanalysis");
// Assert
plugin.Category.Should().Be(DoctorCategory.Security);
}
[Fact]
public void GetChecks_ReturnsFourBinaryAnalysisChecks()
{
// Arrange
var services = new ServiceCollection();
services.AddDoctorBinaryAnalysisPlugin();
var provider = services.BuildServiceProvider();
var plugin = provider.GetServices<IDoctorPlugin>()
.Single(p => p.PluginId == "stellaops.doctor.binaryanalysis");
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.Build();
var context = new DoctorPluginContext
{
Services = provider,
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
// Act
var checks = plugin.GetChecks(context);
// Assert
checks.Should().HaveCount(4);
checks.Select(c => c.CheckId).Should().Contain("check.binaryanalysis.debuginfod.available");
checks.Select(c => c.CheckId).Should().Contain("check.binaryanalysis.ddeb.enabled");
checks.Select(c => c.CheckId).Should().Contain("check.binaryanalysis.buildinfo.cache");
checks.Select(c => c.CheckId).Should().Contain("check.binaryanalysis.symbol.recovery.fallback");
}
[Fact]
public void AllChecks_HaveBinaryanalysisTag()
{
// Arrange
var services = new ServiceCollection();
services.AddDoctorBinaryAnalysisPlugin();
var provider = services.BuildServiceProvider();
var plugin = provider.GetServices<IDoctorPlugin>()
.Single(p => p.PluginId == "stellaops.doctor.binaryanalysis");
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.Build();
var context = new DoctorPluginContext
{
Services = provider,
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
// Act
var checks = plugin.GetChecks(context);
// Assert
foreach (var check in checks)
{
check.Tags.Should().Contain("binaryanalysis",
because: $"check {check.CheckId} should have binaryanalysis tag for CLI filtering");
}
}
[Fact]
public void AllChecks_HaveSecurityTag()
{
// Arrange
var services = new ServiceCollection();
services.AddDoctorBinaryAnalysisPlugin();
var provider = services.BuildServiceProvider();
var plugin = provider.GetServices<IDoctorPlugin>()
.Single(p => p.PluginId == "stellaops.doctor.binaryanalysis");
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.Build();
var context = new DoctorPluginContext
{
Services = provider,
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
// Act
var checks = plugin.GetChecks(context);
// Assert
foreach (var check in checks)
{
check.Tags.Should().Contain("security",
because: $"check {check.CheckId} should have security tag for CLI filtering");
}
}
[Fact]
public void AllChecks_HaveValidCheckIdFormat()
{
// Arrange
var services = new ServiceCollection();
services.AddDoctorBinaryAnalysisPlugin();
var provider = services.BuildServiceProvider();
var plugin = provider.GetServices<IDoctorPlugin>()
.Single(p => p.PluginId == "stellaops.doctor.binaryanalysis");
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.Build();
var context = new DoctorPluginContext
{
Services = provider,
Configuration = config,
TimeProvider = TimeProvider.System,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
// Act
var checks = plugin.GetChecks(context);
// Assert
foreach (var check in checks)
{
check.CheckId.Should().StartWith("check.binaryanalysis.",
"check IDs should follow check.<category>.<name> convention");
check.CheckId.Should().NotContain(" ");
check.CheckId.Should().NotContain("\t");
check.CheckId.Should().NotContain("\n");
}
}
[Fact]
public void Plugin_CanFilterByCategory_Security()
{
// Arrange
var services = new ServiceCollection();
services.AddDoctorBinaryAnalysisPlugin();
var provider = services.BuildServiceProvider();
// Act - simulate CLI --category Security filter
var plugins = provider.GetServices<IDoctorPlugin>()
.Where(p => p.Category == DoctorCategory.Security)
.ToList();
// Assert
plugins.Should().Contain(p => p.PluginId == "stellaops.doctor.binaryanalysis");
}
[Fact]
public void Plugin_HasVersionInfo()
{
// Arrange
var services = new ServiceCollection();
services.AddDoctorBinaryAnalysisPlugin();
var provider = services.BuildServiceProvider();
// Act
var plugin = provider.GetServices<IDoctorPlugin>()
.Single(p => p.PluginId == "stellaops.doctor.binaryanalysis");
// Assert
plugin.Version.Should().NotBeNull();
plugin.Version.Major.Should().BeGreaterThanOrEqualTo(1);
plugin.MinEngineVersion.Should().NotBeNull();
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Plugins\StellaOps.Doctor.Plugin.BinaryAnalysis\StellaOps.Doctor.Plugin.BinaryAnalysis.csproj" />
</ItemGroup>
</Project>