Refactor code structure and optimize performance across multiple modules

This commit is contained in:
StellaOps Bot
2025-12-26 20:03:22 +02:00
parent c786faae84
commit b4fc66feb6
3353 changed files with 88254 additions and 1590657 deletions

View File

@@ -36,6 +36,7 @@ public static class BinaryIndexServiceExtensions
services.AddScoped<IBinaryVulnerabilityService, BinaryVulnerabilityService>();
services.AddScoped<IBinaryFeatureExtractor, ElfFeatureExtractor>();
services.AddScoped<BinaryVulnerabilityAnalyzer>();
services.AddScoped<Processing.BinaryFindingMapper>();
return services;
}
@@ -87,4 +88,40 @@ internal sealed class NullBinaryVulnerabilityService : IBinaryVulnerabilityServi
{
return Task.FromResult(System.Collections.Immutable.ImmutableDictionary<string, System.Collections.Immutable.ImmutableArray<BinaryVulnMatch>>.Empty);
}
public Task<StellaOps.BinaryIndex.FixIndex.Models.FixStatusResult?> GetFixStatusAsync(
string distro,
string release,
string sourcePkg,
string cveId,
CancellationToken ct = default)
{
return Task.FromResult<StellaOps.BinaryIndex.FixIndex.Models.FixStatusResult?>(null);
}
public Task<System.Collections.Immutable.ImmutableDictionary<string, StellaOps.BinaryIndex.FixIndex.Models.FixStatusResult>> GetFixStatusBatchAsync(
string distro,
string release,
string sourcePkg,
IEnumerable<string> cveIds,
CancellationToken ct = default)
{
return Task.FromResult(System.Collections.Immutable.ImmutableDictionary<string, StellaOps.BinaryIndex.FixIndex.Models.FixStatusResult>.Empty);
}
public Task<System.Collections.Immutable.ImmutableArray<BinaryVulnMatch>> LookupByFingerprintAsync(
byte[] fingerprint,
FingerprintLookupOptions? options = null,
CancellationToken ct = default)
{
return Task.FromResult(System.Collections.Immutable.ImmutableArray<BinaryVulnMatch>.Empty);
}
public Task<System.Collections.Immutable.ImmutableDictionary<string, System.Collections.Immutable.ImmutableArray<BinaryVulnMatch>>> LookupByFingerprintBatchAsync(
IEnumerable<(string Key, byte[] Fingerprint)> fingerprints,
FingerprintLookupOptions? options = null,
CancellationToken ct = default)
{
return Task.FromResult(System.Collections.Immutable.ImmutableDictionary<string, System.Collections.Immutable.ImmutableArray<BinaryVulnMatch>>.Empty);
}
}

View File

@@ -0,0 +1,288 @@
// -----------------------------------------------------------------------------
// BinaryFindingMapper.cs
// Sprint: SPRINT_20251226_014_BINIDX
// Task: SCANINT-08 — Create BinaryFindingMapper to convert matches to findings
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging;
using StellaOps.BinaryIndex.Core.Services;
using FixStatusResult = StellaOps.BinaryIndex.Core.Services.FixStatusResult;
namespace StellaOps.Scanner.Worker.Processing;
/// <summary>
/// Maps binary vulnerability findings to the standard scanner finding format.
/// Enables integration with the Findings Ledger and triage workflow.
/// </summary>
public sealed class BinaryFindingMapper
{
private readonly IBinaryVulnerabilityService _binaryVulnService;
private readonly ILogger<BinaryFindingMapper> _logger;
public BinaryFindingMapper(
IBinaryVulnerabilityService binaryVulnService,
ILogger<BinaryFindingMapper> logger)
{
_binaryVulnService = binaryVulnService ?? throw new ArgumentNullException(nameof(binaryVulnService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Maps a single binary finding to a standard finding.
/// </summary>
public Finding MapToFinding(BinaryVulnerabilityFinding finding, string? distro = null, string? release = null)
{
ArgumentNullException.ThrowIfNull(finding);
var findingId = GenerateFindingId(finding);
var severity = GetSeverityFromCve(finding.CveId);
return new Finding
{
Id = findingId,
Type = FindingType.BinaryVulnerability,
Severity = severity,
Title = $"Binary contains vulnerable code: {finding.CveId}",
Description = GenerateDescription(finding),
CveId = finding.CveId,
Purl = finding.VulnerablePurl,
Evidence = new BinaryFindingEvidence
{
BinaryKey = finding.BinaryKey,
LayerDigest = finding.LayerDigest,
MatchMethod = finding.MatchMethod,
Confidence = finding.Confidence,
Similarity = finding.Evidence?.Similarity,
MatchedFunction = finding.Evidence?.MatchedFunction,
BuildId = finding.Evidence?.BuildId
},
Remediation = GenerateRemediation(finding),
ScanId = finding.ScanId,
DetectedAt = DateTimeOffset.UtcNow
};
}
/// <summary>
/// Maps multiple binary findings to standard findings with fix status enrichment.
/// </summary>
public async Task<ImmutableArray<Finding>> MapToFindingsAsync(
IEnumerable<BinaryVulnerabilityFinding> findings,
string? distro,
string? release,
CancellationToken ct = default)
{
var result = new List<Finding>();
var findingsList = findings.ToList();
// Group by source package for batch fix status lookup
var groupedByPurl = findingsList
.GroupBy(f => ExtractSourcePackage(f.VulnerablePurl))
.Where(g => !string.IsNullOrEmpty(g.Key));
foreach (var group in groupedByPurl)
{
var sourcePkg = group.Key!;
var cveIds = group.Select(f => f.CveId).Distinct().ToList();
// Batch fix status lookup
ImmutableDictionary<string, FixStatusResult>? fixStatuses = null;
if (!string.IsNullOrEmpty(distro) && !string.IsNullOrEmpty(release))
{
try
{
fixStatuses = await _binaryVulnService.GetFixStatusBatchAsync(
distro, release, sourcePkg, cveIds, ct).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get fix status for {SourcePkg}", sourcePkg);
}
}
foreach (var finding in group)
{
var mapped = MapToFinding(finding, distro, release);
// Enrich with fix status if available
if (fixStatuses != null && fixStatuses.TryGetValue(finding.CveId, out var fixStatus))
{
mapped = mapped with
{
FixStatus = MapFixStatus(fixStatus),
FixedVersion = fixStatus.FixedVersion
};
}
result.Add(mapped);
}
}
// Handle findings without valid PURLs
foreach (var finding in findingsList.Where(f => string.IsNullOrEmpty(ExtractSourcePackage(f.VulnerablePurl))))
{
result.Add(MapToFinding(finding, distro, release));
}
_logger.LogInformation("Mapped {Count} binary findings", result.Count);
return result.ToImmutableArray();
}
private static Guid GenerateFindingId(BinaryVulnerabilityFinding finding)
{
// Generate deterministic ID based on scan, CVE, and binary key
var input = $"{finding.ScanId}:{finding.CveId}:{finding.BinaryKey}:{finding.LayerDigest}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return new Guid(hash.AsSpan()[..16]);
}
private static Severity GetSeverityFromCve(string cveId)
{
// In production, this would look up CVSS from advisory data
// For now, return Unknown and let downstream enrichment set it
return Severity.Unknown;
}
private static string GenerateDescription(BinaryVulnerabilityFinding finding)
{
var sb = new StringBuilder();
sb.AppendLine($"A binary file in the container image contains code affected by {finding.CveId}.");
sb.AppendLine();
sb.AppendLine($"**Detection Method:** {finding.MatchMethod}");
sb.AppendLine($"**Confidence:** {finding.Confidence:P0}");
if (finding.Evidence?.MatchedFunction is not null)
{
sb.AppendLine($"**Vulnerable Function:** {finding.Evidence.MatchedFunction}");
}
if (finding.Evidence?.BuildId is not null)
{
sb.AppendLine($"**Build-ID:** {finding.Evidence.BuildId}");
}
return sb.ToString();
}
private static string GenerateRemediation(BinaryVulnerabilityFinding finding)
{
return $"Update the package containing the binary to a version that includes the fix for {finding.CveId}. " +
$"If using a distro package, check if a backported security update is available.";
}
private static string? ExtractSourcePackage(string purl)
{
// Extract package name from PURL
// e.g., "pkg:deb/debian/openssl@1.1.1" -> "openssl"
if (string.IsNullOrEmpty(purl))
return null;
var atIndex = purl.IndexOf('@');
var slashIndex = purl.LastIndexOf('/', atIndex > 0 ? atIndex : purl.Length);
if (slashIndex >= 0)
{
var endIndex = atIndex > slashIndex ? atIndex : purl.Length;
return purl[(slashIndex + 1)..endIndex];
}
return null;
}
private static FindingFixStatus MapFixStatus(FixStatusResult status)
{
return status.State switch
{
FixState.Fixed => FindingFixStatus.Fixed,
FixState.Vulnerable => FindingFixStatus.Vulnerable,
FixState.NotAffected => FindingFixStatus.NotAffected,
FixState.WontFix => FindingFixStatus.WontFix,
_ => FindingFixStatus.Unknown
};
}
}
/// <summary>
/// Standard scanner finding.
/// </summary>
public sealed record Finding
{
public required Guid Id { get; init; }
public required FindingType Type { get; init; }
public required Severity Severity { get; init; }
public required string Title { get; init; }
public required string Description { get; init; }
public string? CveId { get; init; }
public string? Purl { get; init; }
public required BinaryFindingEvidence Evidence { get; init; }
public required string Remediation { get; init; }
public Guid ScanId { get; init; }
public DateTimeOffset DetectedAt { get; init; }
public FindingFixStatus FixStatus { get; init; } = FindingFixStatus.Unknown;
public string? FixedVersion { get; init; }
}
/// <summary>
/// Evidence specific to binary vulnerability findings.
/// </summary>
public sealed record BinaryFindingEvidence
{
public required string BinaryKey { get; init; }
public required string LayerDigest { get; init; }
public required string MatchMethod { get; init; }
public required decimal Confidence { get; init; }
public decimal? Similarity { get; init; }
public string? MatchedFunction { get; init; }
public string? BuildId { get; init; }
}
/// <summary>
/// Finding type enumeration.
/// </summary>
public enum FindingType
{
PackageVulnerability,
BinaryVulnerability,
PolicyViolation,
SecretExposure,
MisconfigurationDebian
}
/// <summary>
/// Severity levels for findings.
/// </summary>
public enum Severity
{
Unknown,
None,
Low,
Medium,
High,
Critical
}
/// <summary>
/// Fix status for findings.
/// </summary>
public enum FindingFixStatus
{
Unknown,
Vulnerable,
Fixed,
NotAffected,
WontFix
}
/// <summary>
/// Fix state from the binary index.
/// </summary>
public enum FixState
{
Unknown,
Vulnerable,
Fixed,
NotAffected,
WontFix
}

View File

@@ -0,0 +1,219 @@
// -----------------------------------------------------------------------------
// BinaryLookupStageExecutor.cs
// Sprint: SPRINT_20251226_014_BINIDX
// Task: SCANINT-02 — Create IBinaryLookupStep in scan pipeline
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using StellaOps.BinaryIndex.Core.Models;
using StellaOps.BinaryIndex.Core.Services;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Worker.Extensions;
namespace StellaOps.Scanner.Worker.Processing;
/// <summary>
/// Scan pipeline stage that performs binary vulnerability lookups.
/// Runs after analyzers to correlate binary identities with known vulnerabilities.
/// </summary>
public sealed class BinaryLookupStageExecutor : IScanStageExecutor
{
private readonly BinaryVulnerabilityAnalyzer _analyzer;
private readonly BinaryIndexOptions _options;
private readonly ILogger<BinaryLookupStageExecutor> _logger;
public BinaryLookupStageExecutor(
BinaryVulnerabilityAnalyzer analyzer,
BinaryIndexOptions options,
ILogger<BinaryLookupStageExecutor> logger)
{
_analyzer = analyzer ?? throw new ArgumentNullException(nameof(analyzer));
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public string StageName => ScanStageNames.BinaryLookup;
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
{
if (!_options.Enabled)
{
_logger.LogDebug("Binary vulnerability analysis disabled, skipping");
return;
}
_logger.LogInformation(
"Starting binary vulnerability lookup for scan {ScanId}",
context.ScanId);
var allFindings = new List<BinaryVulnerabilityFinding>();
var layerContexts = BuildLayerContexts(context);
foreach (var layerContext in layerContexts)
{
try
{
var result = await _analyzer.AnalyzeLayerAsync(layerContext, cancellationToken)
.ConfigureAwait(false);
if (result.Findings.Length > 0)
{
allFindings.AddRange(result.Findings);
_logger.LogInformation(
"Found {Count} binary vulnerabilities in layer {Layer}",
result.Findings.Length,
layerContext.LayerDigest);
}
}
catch (Exception ex)
{
_logger.LogWarning(
ex,
"Failed to analyze layer {Layer} for binary vulnerabilities",
layerContext.LayerDigest);
}
}
// Store findings in analysis context for downstream stages
context.Analysis.SetBinaryFindings(allFindings.ToImmutableArray());
_logger.LogInformation(
"Binary vulnerability lookup complete for scan {ScanId}: {Count} findings",
context.ScanId,
allFindings.Count);
}
private IReadOnlyList<BinaryLayerContext> BuildLayerContexts(ScanJobContext context)
{
var contexts = new List<BinaryLayerContext>();
// Get layer information from the scan context
var layers = context.Analysis.GetLayers();
if (layers == null || layers.Count == 0)
{
_logger.LogDebug("No layers found in scan context");
return contexts;
}
var distro = context.Analysis.GetDetectedDistro();
var release = context.Analysis.GetDetectedRelease();
foreach (var layer in layers)
{
var binaryPaths = context.Analysis.GetBinaryPathsForLayer(layer.Digest);
if (binaryPaths == null || binaryPaths.Count == 0)
{
continue;
}
contexts.Add(new BinaryLayerContext
{
ScanId = Guid.Parse(context.ScanId),
LayerDigest = layer.Digest,
BinaryPaths = binaryPaths,
DetectedDistro = distro,
DetectedRelease = release,
OpenFile = path => context.Analysis.OpenLayerFile(layer.Digest, path)
});
}
return contexts;
}
}
/// <summary>
/// Extension methods for ScanAnalysisStore to support binary analysis.
/// </summary>
public static class BinaryScanAnalysisStoreExtensions
{
private const string BinaryFindingsKey = "binary_findings";
private const string LayersKey = "layers";
private const string DistroKey = "detected_distro";
private const string ReleaseKey = "detected_release";
public static void SetBinaryFindings(
this ScanAnalysisStore store,
ImmutableArray<BinaryVulnerabilityFinding> findings)
{
ArgumentNullException.ThrowIfNull(store);
store.Set(BinaryFindingsKey, findings);
}
public static ImmutableArray<BinaryVulnerabilityFinding> GetBinaryFindings(
this ScanAnalysisStore store)
{
ArgumentNullException.ThrowIfNull(store);
if (store.TryGet<ImmutableArray<BinaryVulnerabilityFinding>>(BinaryFindingsKey, out var findings) && !findings.IsDefault)
{
return findings;
}
return ImmutableArray<BinaryVulnerabilityFinding>.Empty;
}
public static IReadOnlyList<LayerInfo>? GetLayers(this ScanAnalysisStore store)
{
ArgumentNullException.ThrowIfNull(store);
if (store.TryGet<IReadOnlyList<LayerInfo>>(LayersKey, out var layers))
{
return layers;
}
return null;
}
public static string? GetDetectedDistro(this ScanAnalysisStore store)
{
ArgumentNullException.ThrowIfNull(store);
if (store.TryGet<string>(DistroKey, out var distro))
{
return distro;
}
return null;
}
public static string? GetDetectedRelease(this ScanAnalysisStore store)
{
ArgumentNullException.ThrowIfNull(store);
if (store.TryGet<string>(ReleaseKey, out var release))
{
return release;
}
return null;
}
public static IReadOnlyList<string>? GetBinaryPathsForLayer(
this ScanAnalysisStore store,
string layerDigest)
{
ArgumentNullException.ThrowIfNull(store);
var key = $"binary_paths_{layerDigest}";
if (store.TryGet<IReadOnlyList<string>>(key, out var paths))
{
return paths;
}
return null;
}
public static Stream? OpenLayerFile(
this ScanAnalysisStore store,
string layerDigest,
string path)
{
ArgumentNullException.ThrowIfNull(store);
if (store.TryGet<Func<string, string, Stream?>>("layer_file_opener", out var opener))
{
return opener?.Invoke(layerDigest, path);
}
return null;
}
}
/// <summary>
/// Layer metadata for binary analysis.
/// </summary>
public sealed record LayerInfo
{
public required string Digest { get; init; }
public required string MediaType { get; init; }
public long Size { get; init; }
}

View File

@@ -20,6 +20,9 @@ public static class ScanStageNames
// Sprint: SPRINT_3500_0001_0001 - Proof of Exposure
public const string GeneratePoE = "generate-poe";
// Sprint: SPRINT_20251226_014_BINIDX - Binary Vulnerability Lookup
public const string BinaryLookup = "binary-lookup";
public static readonly IReadOnlyList<string> Ordered = new[]
{
IngestReplay,
@@ -27,6 +30,7 @@ public static class ScanStageNames
PullLayers,
BuildFilesystem,
ExecuteAnalyzers,
BinaryLookup,
EpssEnrichment,
ComposeArtifacts,
Entropy,

View File

@@ -27,6 +27,7 @@ using StellaOps.Scanner.Worker.Options;
using StellaOps.Scanner.Worker.Processing;
using StellaOps.Scanner.Worker.Processing.Entropy;
using StellaOps.Scanner.Worker.Determinism;
using StellaOps.Scanner.Worker.Extensions;
using StellaOps.Scanner.Worker.Processing.Surface;
using StellaOps.Scanner.Storage.Extensions;
using StellaOps.Scanner.Storage;
@@ -93,6 +94,10 @@ builder.Services.AddSingleton<IDelayScheduler, SystemDelayScheduler>();
builder.Services.AddEntryTraceAnalyzer();
builder.Services.AddSingleton<IEntryTraceExecutionService, EntryTraceExecutionService>();
// BinaryIndex integration for binary vulnerability detection (Sprint: SPRINT_20251226_014_BINIDX)
builder.Services.AddBinaryIndexIntegration(builder.Configuration);
builder.Services.AddSingleton<ReachabilityUnionWriter>();
builder.Services.AddSingleton<ReachabilityUnionPublisher>();
builder.Services.AddSingleton<IReachabilityUnionPublisherService, ReachabilityUnionPublisherService>();
@@ -156,6 +161,7 @@ builder.Services.AddSingleton<NativeAnalyzerExecutor>();
builder.Services.AddSingleton<IScanAnalyzerDispatcher, CompositeScanAnalyzerDispatcher>();
builder.Services.AddSingleton<IScanStageExecutor, RegistrySecretStageExecutor>();
builder.Services.AddSingleton<IScanStageExecutor, AnalyzerStageExecutor>();
builder.Services.AddSingleton<IScanStageExecutor, BinaryLookupStageExecutor>();
builder.Services.AddSingleton<IScanStageExecutor, EpssEnrichmentStageExecutor>();
builder.Services.AddSingleton<IScanStageExecutor, Reachability.ReachabilityBuildStageExecutor>();
builder.Services.AddSingleton<IScanStageExecutor, Reachability.ReachabilityPublishStageExecutor>();

View File

@@ -9,7 +9,7 @@
<Description>Ground-truth corpus benchmarking infrastructure for reachability analysis</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="10.0.0-preview.1.25105.2" />
<PackageReference Include="System.Text.Json" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj" />

View File

@@ -15,5 +15,8 @@
<ProjectReference Include="../../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Auth.Security/StellaOps.Auth.Security.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Replay.Core/StellaOps.Replay.Core.csproj" />
<ProjectReference Include="../StellaOps.Scanner.ProofSpine/StellaOps.Scanner.ProofSpine.csproj" />
<ProjectReference Include="../../../Attestor/StellaOps.Attestor.Envelope/StellaOps.Attestor.Envelope.csproj" />
</ItemGroup>
</Project>

View File

@@ -9,6 +9,6 @@
<RootNamespace>StellaOps.Scanner.Orchestration</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-preview.7.24407.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
</ItemGroup>
</Project>

View File

@@ -13,5 +13,8 @@
Use SliceDataDto and JsonElement instead of ReachabilitySlice type. -->
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Replay.Core\StellaOps.Replay.Core.csproj" />
<ProjectReference Include="..\StellaOps.Scanner.Evidence\StellaOps.Scanner.Evidence.csproj" />
<ProjectReference Include="..\..\..\Attestor\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj" />
</ItemGroup>
</Project>

View File

@@ -8,6 +8,8 @@ using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.VulnSurfaces.Fingerprint;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.VulnSurfaces.Tests;
public class CecilMethodFingerprinterTests
@@ -20,20 +22,23 @@ public class CecilMethodFingerprinterTests
NullLogger<CecilMethodFingerprinter>.Instance);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Ecosystem_ReturnsNuget()
{
Assert.Equal("nuget", _fingerprinter.Ecosystem);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FingerprintAsync_WithNullRequest_ThrowsArgumentNullException()
{
await Assert.ThrowsAsync<ArgumentNullException>(
() => _fingerprinter.FingerprintAsync(null!));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FingerprintAsync_WithNonExistentPath_ReturnsEmptyResult()
{
// Arrange
@@ -53,7 +58,8 @@ public class CecilMethodFingerprinterTests
Assert.Empty(result.Methods);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FingerprintAsync_WithOwnAssembly_FindsMethods()
{
// Arrange - use the test assembly itself
@@ -80,7 +86,8 @@ public class CecilMethodFingerprinterTests
Assert.True(result.Methods.Count > 0, "Should find at least some methods");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FingerprintAsync_ComputesDeterministicHashes()
{
// Arrange - fingerprint twice
@@ -109,7 +116,8 @@ public class CecilMethodFingerprinterTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FingerprintAsync_WithCancellation_RespectsCancellation()
{
// Arrange
@@ -142,7 +150,8 @@ public class CecilMethodFingerprinterTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FingerprintAsync_MethodKeyFormat_IsValid()
{
// Arrange
@@ -172,7 +181,8 @@ public class CecilMethodFingerprinterTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FingerprintAsync_IncludesSignature()
{
// Arrange

View File

@@ -8,11 +8,13 @@ using StellaOps.Scanner.VulnSurfaces.CallGraph;
using StellaOps.Scanner.VulnSurfaces.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.VulnSurfaces.Tests;
public class InternalCallGraphTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddMethod_StoresMethod()
{
// Arrange
@@ -38,7 +40,8 @@ public class InternalCallGraphTests
Assert.Equal(1, graph.MethodCount);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddEdge_CreatesForwardAndReverseMapping()
{
// Arrange
@@ -63,7 +66,8 @@ public class InternalCallGraphTests
Assert.Equal(1, graph.EdgeCount);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetPublicMethods_ReturnsOnlyPublic()
{
// Arrange
@@ -97,7 +101,8 @@ public class InternalCallGraphTests
Assert.Equal("A::Public()", publicMethods[0].MethodKey);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetCallees_EmptyForUnknownMethod()
{
// Arrange
@@ -114,7 +119,8 @@ public class InternalCallGraphTests
Assert.Empty(callees);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetMethod_ReturnsNullForUnknown()
{
// Arrange

View File

@@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.VulnSurfaces.Fingerprint;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.VulnSurfaces.Tests;
public class MethodDiffEngineTests
@@ -20,14 +21,16 @@ public class MethodDiffEngineTests
NullLogger<MethodDiffEngine>.Instance);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DiffAsync_WithNullRequest_ThrowsArgumentNullException()
{
await Assert.ThrowsAsync<ArgumentNullException>(
() => _diffEngine.DiffAsync(null!));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DiffAsync_WithIdenticalFingerprints_ReturnsNoChanges()
{
// Arrange
@@ -68,7 +71,8 @@ public class MethodDiffEngineTests
Assert.Equal(0, diff.TotalChanges);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DiffAsync_WithModifiedMethod_ReturnsModified()
{
// Arrange
@@ -112,7 +116,8 @@ public class MethodDiffEngineTests
Assert.Empty(diff.Removed);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DiffAsync_WithAddedMethod_ReturnsAdded()
{
// Arrange
@@ -155,7 +160,8 @@ public class MethodDiffEngineTests
Assert.Empty(diff.Removed);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DiffAsync_WithRemovedMethod_ReturnsRemoved()
{
// Arrange
@@ -198,7 +204,8 @@ public class MethodDiffEngineTests
Assert.Equal("Test.Class::RemovedMethod", diff.Removed[0].MethodKey);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DiffAsync_WithMultipleChanges_ReturnsAllChanges()
{
// Arrange - simulate a fix that modifies one method, adds one, removes one
@@ -247,7 +254,8 @@ public class MethodDiffEngineTests
Assert.Equal(3, diff.TotalChanges);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DiffAsync_TriggerMethods_AreModifiedOrRemoved()
{
// This test validates the key insight:
@@ -298,7 +306,8 @@ public class MethodDiffEngineTests
Assert.Empty(diff.Removed);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DiffAsync_WithEmptyFingerprints_ReturnsNoChanges()
{
// Arrange

View File

@@ -14,6 +14,8 @@ using Moq.Protected;
using StellaOps.Scanner.VulnSurfaces.Download;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.VulnSurfaces.Tests;
public class NuGetPackageDownloaderTests : IDisposable
@@ -35,7 +37,8 @@ public class NuGetPackageDownloaderTests : IDisposable
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Ecosystem_ReturnsNuget()
{
// Arrange
@@ -45,7 +48,8 @@ public class NuGetPackageDownloaderTests : IDisposable
Assert.Equal("nuget", downloader.Ecosystem);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DownloadAsync_WithNullRequest_ThrowsArgumentNullException()
{
// Arrange
@@ -56,7 +60,8 @@ public class NuGetPackageDownloaderTests : IDisposable
() => downloader.DownloadAsync(null!));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DownloadAsync_WithHttpError_ReturnsFailResult()
{
// Arrange
@@ -93,7 +98,8 @@ public class NuGetPackageDownloaderTests : IDisposable
Assert.Null(result.ExtractedPath);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DownloadAsync_WithValidNupkg_ReturnsSuccessResult()
{
// Arrange - create a mock .nupkg (which is just a zip file)
@@ -135,7 +141,8 @@ public class NuGetPackageDownloaderTests : IDisposable
Assert.False(result.FromCache);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DownloadAsync_WithCachedPackage_ReturnsCachedResult()
{
// Arrange - pre-create the cached directory
@@ -162,7 +169,8 @@ public class NuGetPackageDownloaderTests : IDisposable
Assert.Equal(packageDir, result.ExtractedPath);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DownloadAsync_WithCacheFalse_BypassesCache()
{
// Arrange - pre-create the cached directory
@@ -210,7 +218,8 @@ public class NuGetPackageDownloaderTests : IDisposable
ItExpr.IsAny<CancellationToken>());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DownloadAsync_UsesCorrectUrl()
{
// Arrange
@@ -250,7 +259,8 @@ public class NuGetPackageDownloaderTests : IDisposable
Assert.EndsWith(".nupkg", capturedRequest.RequestUri!.ToString());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DownloadAsync_WithCustomRegistry_UsesCustomUrl()
{
// Arrange
@@ -289,7 +299,8 @@ public class NuGetPackageDownloaderTests : IDisposable
Assert.StartsWith("https://custom.nuget.feed.example.com/v3", capturedRequest.RequestUri!.ToString());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DownloadAsync_WithCancellation_HonorsCancellation()
{
// Arrange

View File

@@ -20,5 +20,6 @@
<ItemGroup>
<ProjectReference Include="..\StellaOps.Scanner.VulnSurfaces\StellaOps.Scanner.VulnSurfaces.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -10,6 +10,7 @@ using StellaOps.Scanner.VulnSurfaces.Models;
using StellaOps.Scanner.VulnSurfaces.Triggers;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.VulnSurfaces.Tests;
public class TriggerMethodExtractorTests
@@ -21,7 +22,8 @@ public class TriggerMethodExtractorTests
_extractor = new TriggerMethodExtractor(NullLogger<TriggerMethodExtractor>.Instance);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractAsync_DirectPath_FindsTrigger()
{
// Arrange
@@ -85,7 +87,8 @@ public class TriggerMethodExtractorTests
Assert.False(trigger.IsInterfaceExpansion);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractAsync_NoPath_ReturnsEmpty()
{
// Arrange
@@ -124,7 +127,8 @@ public class TriggerMethodExtractorTests
Assert.Empty(result.Triggers);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractAsync_MultiplePublicMethods_FindsAllTriggers()
{
// Arrange
@@ -174,7 +178,8 @@ public class TriggerMethodExtractorTests
Assert.Contains(result.Triggers, t => t.TriggerMethodKey == "Class::Api2()");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractAsync_MaxDepthExceeded_DoesNotFindTrigger()
{
// Arrange
@@ -231,7 +236,8 @@ public class TriggerMethodExtractorTests
Assert.Empty(result.Triggers);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractAsync_VirtualMethod_ReducesConfidence()
{
// Arrange

View File

@@ -4,10 +4,12 @@ using StellaOps.Scanner.VulnSurfaces.Services;
using StellaOps.Scanner.VulnSurfaces.Storage;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.VulnSurfaces.Tests;
public sealed class VulnSurfaceServiceTests
{
[Trait("Category", TestCategories.Unit)]
[Fact(DisplayName = "GetAffectedSymbolsAsync returns sinks when surface exists")]
public async Task GetAffectedSymbolsAsync_ReturnsSurfaceSinks()
{
@@ -50,6 +52,7 @@ public sealed class VulnSurfaceServiceTests
Assert.Equal(surfaceGuid, repository.LastSurfaceId);
}
[Trait("Category", TestCategories.Unit)]
[Fact(DisplayName = "GetAffectedSymbolsAsync falls back to package symbol provider")]
public async Task GetAffectedSymbolsAsync_FallsBackToPackageSymbols()
{
@@ -64,6 +67,7 @@ public sealed class VulnSurfaceServiceTests
Assert.Single(result.Symbols);
}
[Trait("Category", TestCategories.Unit)]
[Fact(DisplayName = "GetAffectedSymbolsAsync returns heuristic when no data")]
public async Task GetAffectedSymbolsAsync_ReturnsHeuristicWhenEmpty()
{

View File

@@ -1,15 +1,17 @@
using System.Net;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Scanner.Advisory.Tests;
public sealed class AdvisoryClientTests
{
[Trait("Category", TestCategories.Unit)]
[Fact(DisplayName = "GetCveSymbolsAsync uses Concelier response and caches results")]
public async Task GetCveSymbolsAsync_UsesConcelierAndCaches()
{
@@ -57,6 +59,7 @@ public sealed class AdvisoryClientTests
Assert.NotNull(mapping2);
}
[Trait("Category", TestCategories.Unit)]
[Fact(DisplayName = "GetCveSymbolsAsync falls back to bundle store on HTTP failure")]
public async Task GetCveSymbolsAsync_FallsBackToBundle()
{

View File

@@ -1,10 +1,12 @@
using System.Text.Json;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Scanner.Advisory.Tests;
public sealed class FileAdvisoryBundleStoreTests
{
[Trait("Category", TestCategories.Unit)]
[Fact(DisplayName = "FileAdvisoryBundleStore resolves CVE IDs case-insensitively")]
public async Task TryGetAsync_ResolvesCaseInsensitive()
{

View File

@@ -16,5 +16,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.Advisory\StellaOps.Scanner.Advisory.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -2,11 +2,13 @@ using System.IO;
using StellaOps.Scanner.Analyzers.Lang.Node;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests;
public class Phase22SmokeTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Phase22_Fixture_Matches_Golden()
{
var cancellationToken = TestContext.Current.CancellationToken;

View File

@@ -22,6 +22,7 @@
<!-- Keep graph tight: only Lang.Node tests + core contracts. Reuse compiled binaries to avoid dragging full solution build. -->
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang.Node/StellaOps.Scanner.Analyzers.Lang.Node.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Core/StellaOps.Scanner.Core.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -5,11 +5,13 @@ using System.Threading.Tasks;
using StellaOps.Scanner.Analyzers.Lang.Node.Internal.Phase22;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.Lang.Node.Tests;
public class NodePhase22SampleLoaderTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TryLoadAsync_ReadsComponentsFromNdjson()
{
var root = Path.Combine("Fixtures");

View File

@@ -35,6 +35,7 @@
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang.Node/StellaOps.Scanner.Analyzers.Lang.Node.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Core/StellaOps.Scanner.Core.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -4,6 +4,7 @@ using StellaOps.Scanner.Analyzers.Lang;
using StellaOps.Scanner.Analyzers.Lang.Ruby;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.Lang.Ruby.Tests;
/// <summary>
@@ -18,7 +19,8 @@ public sealed class RubyBenchmarks
private const int BenchmarkIterations = 10;
private const int MaxAnalysisTimeMs = 1000;
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SimpleApp_MeetsPerformanceTargetAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");
@@ -47,7 +49,8 @@ public sealed class RubyBenchmarks
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Simple app analysis should complete in <{MaxAnalysisTimeMs}ms including policy scanning (actual: {avgMs:F2}ms)");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ComplexApp_MeetsPerformanceTargetAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "complex-app");
@@ -76,7 +79,8 @@ public sealed class RubyBenchmarks
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Complex app analysis should complete in <{MaxAnalysisTimeMs}ms including policy scanning (actual: {avgMs:F2}ms)");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RailsApp_MeetsPerformanceTargetAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "rails-app");
@@ -105,7 +109,8 @@ public sealed class RubyBenchmarks
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Rails app analysis should complete in <{MaxAnalysisTimeMs}ms including policy scanning (actual: {avgMs:F2}ms)");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SinatraApp_MeetsPerformanceTargetAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "sinatra-app");
@@ -134,7 +139,8 @@ public sealed class RubyBenchmarks
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Sinatra app analysis should complete in <{MaxAnalysisTimeMs}ms including policy scanning (actual: {avgMs:F2}ms)");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ContainerApp_MeetsPerformanceTargetAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "container-app");
@@ -163,7 +169,8 @@ public sealed class RubyBenchmarks
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Container app analysis should complete in <{MaxAnalysisTimeMs}ms including policy scanning (actual: {avgMs:F2}ms)");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LegacyApp_MeetsPerformanceTargetAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "legacy-app");
@@ -192,7 +199,8 @@ public sealed class RubyBenchmarks
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Legacy app analysis should complete in <{MaxAnalysisTimeMs}ms including policy scanning (actual: {avgMs:F2}ms)");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CliApp_MeetsPerformanceTargetAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "cli-app");
@@ -221,7 +229,8 @@ public sealed class RubyBenchmarks
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"CLI app analysis should complete in <{MaxAnalysisTimeMs}ms including policy scanning (actual: {avgMs:F2}ms)");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task MultipleRuns_ProduceDeterministicResultsAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");

View File

@@ -5,11 +5,14 @@ using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.Lang.Ruby.Tests;
public sealed class RubyLanguageAnalyzerTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SimpleWorkspaceProducesDeterministicOutputAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");
@@ -23,7 +26,8 @@ public sealed class RubyLanguageAnalyzerTests
TestContext.Current.CancellationToken);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzerEmitsObservationPayloadWithSummaryAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");
@@ -73,7 +77,8 @@ public sealed class RubyLanguageAnalyzerTests
Assert.Equal("2.4.22", root.GetProperty("bundledWith").GetString());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ComplexWorkspaceProducesDeterministicOutputAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "complex-app");
@@ -87,7 +92,8 @@ public sealed class RubyLanguageAnalyzerTests
TestContext.Current.CancellationToken);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CliWorkspaceProducesDeterministicOutputAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "cli-app");
@@ -101,7 +107,8 @@ public sealed class RubyLanguageAnalyzerTests
TestContext.Current.CancellationToken);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RailsWorkspaceProducesDeterministicOutputAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "rails-app");
@@ -115,7 +122,8 @@ public sealed class RubyLanguageAnalyzerTests
TestContext.Current.CancellationToken);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SinatraWorkspaceProducesDeterministicOutputAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "sinatra-app");
@@ -129,7 +137,8 @@ public sealed class RubyLanguageAnalyzerTests
TestContext.Current.CancellationToken);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ContainerWorkspaceProducesDeterministicOutputAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "container-app");
@@ -143,7 +152,8 @@ public sealed class RubyLanguageAnalyzerTests
TestContext.Current.CancellationToken);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ContainerWorkspaceDetectsRubyVersionAndNativeExtensionsAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "container-app");
@@ -185,7 +195,8 @@ public sealed class RubyLanguageAnalyzerTests
Assert.True(nativeExtensions.GetArrayLength() >= 2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LegacyWorkspaceProducesDeterministicOutputAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "legacy-app");
@@ -199,7 +210,8 @@ public sealed class RubyLanguageAnalyzerTests
TestContext.Current.CancellationToken);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LegacyWorkspaceDetectsCapabilitiesWithoutBundlerAsync()
{
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "legacy-app");

View File

@@ -35,6 +35,7 @@
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang.Ruby/StellaOps.Scanner.Analyzers.Lang.Ruby.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Core/StellaOps.Scanner.Core.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -3,11 +3,14 @@ using StellaOps.Scanner.Analyzers.Native;
using StellaOps.Scanner.Analyzers.Native.Tests.Fixtures;
using StellaOps.Scanner.Analyzers.Native.Tests.TestUtilities;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.Native.Tests;
public class ElfDynamicSectionParserTests : NativeTestBase
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesMinimalElfWithNoDynamicSection()
{
// Minimal ELF64 with no dependencies (static binary scenario)
@@ -21,7 +24,8 @@ public class ElfDynamicSectionParserTests : NativeTestBase
info.Runpath.Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesElfWithDtNeeded()
{
// Build ELF with DT_NEEDED entries using the builder
@@ -38,7 +42,8 @@ public class ElfDynamicSectionParserTests : NativeTestBase
info.Dependencies[2].Soname.Should().Be("libpthread.so.0");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesElfWithRpathAndRunpath()
{
// Build ELF with rpath and runpath using the builder
@@ -53,7 +58,8 @@ public class ElfDynamicSectionParserTests : NativeTestBase
info.Runpath.Should().BeEquivalentTo(["$ORIGIN/../lib"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesElfWithInterpreterAndBuildId()
{
// Build ELF with interpreter and build ID using the builder
@@ -67,7 +73,8 @@ public class ElfDynamicSectionParserTests : NativeTestBase
info.BinaryId.Should().Be("deadbeef0102030405060708090a0b0c");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeduplicatesDtNeededEntries()
{
// ElfBuilder deduplicates internally, so add "duplicates" via builder
@@ -85,7 +92,8 @@ public class ElfDynamicSectionParserTests : NativeTestBase
info.Dependencies[0].Soname.Should().Be("libc.so.6");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReturnsFalseForNonElfData()
{
var buffer = new byte[] { 0x00, 0x01, 0x02, 0x03 };
@@ -96,7 +104,8 @@ public class ElfDynamicSectionParserTests : NativeTestBase
result.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReturnsFalseForPeFile()
{
var buffer = new byte[256];
@@ -109,7 +118,8 @@ public class ElfDynamicSectionParserTests : NativeTestBase
result.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesElfWithVersionNeeds()
{
// Test that version needs (GLIBC_2.17, etc.) are properly extracted
@@ -128,7 +138,8 @@ public class ElfDynamicSectionParserTests : NativeTestBase
info.Dependencies[0].VersionNeeds.Should().Contain(v => v.Version == "GLIBC_2.28");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesElfWithWeakVersionNeeds()
{
// Test that weak version requirements (VER_FLG_WEAK) are properly detected

View File

@@ -2,11 +2,14 @@ using System.Text;
using FluentAssertions;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.Native.Tests;
public class HeuristicScannerTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_DetectsElfSonamePattern()
{
// Arrange - binary containing soname strings
@@ -26,7 +29,8 @@ public class HeuristicScannerTests
result.Edges.Should().OnlyContain(e => e.ReasonCode == HeuristicReasonCodes.StringDlopen);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_DetectsWindowsDllPattern()
{
// Arrange
@@ -46,7 +50,8 @@ public class HeuristicScannerTests
result.Edges.Should().OnlyContain(e => e.ReasonCode == HeuristicReasonCodes.StringLoadLibrary);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_DetectsMachODylibPattern()
{
// Arrange
@@ -66,7 +71,8 @@ public class HeuristicScannerTests
result.Edges.Should().Contain(e => e.LibraryName == "@loader_path/libbaz.dylib");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_AssignsHighConfidenceToPathLikeStrings()
{
// Arrange
@@ -86,7 +92,8 @@ public class HeuristicScannerTests
simpleSoname.Confidence.Should().Be(HeuristicConfidence.Medium);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_DetectsPluginConfigReferences()
{
// Arrange
@@ -106,7 +113,8 @@ public class HeuristicScannerTests
result.PluginConfigs.Should().Contain("modules.conf");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_DetectsGoCgoImportDirective()
{
// Arrange - simulate Go binary with cgo import
@@ -126,7 +134,8 @@ public class HeuristicScannerTests
e.Confidence == HeuristicConfidence.High);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_DetectsGoCgoStaticImport()
{
// Arrange
@@ -145,7 +154,8 @@ public class HeuristicScannerTests
e.ReasonCode == HeuristicReasonCodes.GoCgoImport);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_DeduplicatesEdgesByLibraryName()
{
// Arrange - same library mentioned multiple times
@@ -164,7 +174,8 @@ public class HeuristicScannerTests
result.Edges.Should().ContainSingle(e => e.LibraryName == "libfoo.so");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_IncludesFileOffsetInEdge()
{
// Arrange
@@ -182,7 +193,8 @@ public class HeuristicScannerTests
edge.FileOffset.Should().Be(100);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ScanForDynamicLoading_ReturnsOnlyLibraryEdges()
{
// Arrange
@@ -200,7 +212,8 @@ public class HeuristicScannerTests
e.ReasonCode == HeuristicReasonCodes.StringDlopen);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ScanForPluginConfigs_ReturnsOnlyConfigReferences()
{
// Arrange
@@ -219,7 +232,8 @@ public class HeuristicScannerTests
configs.Should().Contain("plugin.json");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_EmptyStream_ReturnsEmptyResult()
{
// Arrange
@@ -233,7 +247,8 @@ public class HeuristicScannerTests
result.PluginConfigs.Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_NoValidStrings_ReturnsEmptyResult()
{
// Arrange - binary data with no printable strings
@@ -247,7 +262,8 @@ public class HeuristicScannerTests
result.Edges.Should().BeEmpty();
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("libfoo.so.1", true)]
[InlineData("libbar.so", true)]
[InlineData("lib-baz_qux.so.2.3", true)]

View File

@@ -7,7 +7,8 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests;
public class MachOLoadCommandParserTests : NativeTestBase
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesMinimalMachO64LittleEndian()
{
// Build minimal Mach-O 64-bit little-endian using builder
@@ -20,7 +21,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
info.Slices[0].CpuType.Should().Be("x86_64");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesMinimalMachO64BigEndian()
{
// Build minimal Mach-O 64-bit big-endian using builder
@@ -37,7 +39,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
info.Slices[0].CpuType.Should().Be("x86_64");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesMachOWithDylibs()
{
// Build Mach-O with dylib dependencies using builder
@@ -55,7 +58,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
info.Slices[0].Dependencies[1].Path.Should().Be("/usr/lib/libc++.1.dylib");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesMachOWithRpath()
{
// Build Mach-O with rpaths using builder
@@ -71,7 +75,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
info.Slices[0].Rpaths[1].Should().Be("@loader_path/../lib");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesMachOWithUuid()
{
// Build Mach-O with UUID using builder
@@ -86,7 +91,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
info.Slices[0].Uuid.Should().MatchRegex(@"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesFatBinary()
{
// Build universal (fat) binary using builder
@@ -100,7 +106,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
info.Slices[1].CpuType.Should().Be("arm64");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesWeakAndReexportDylibs()
{
// Build Mach-O with weak and reexport dylibs using builder
@@ -115,7 +122,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
info.Slices[0].Dependencies.Should().Contain(d => d.ReasonCode == "macho-reexport");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeduplicatesDylibs()
{
// Build Mach-O with duplicate dylibs - builder or parser should deduplicate
@@ -129,7 +137,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
info.Slices[0].Dependencies.Should().HaveCount(1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReturnsFalseForNonMachO()
{
var buffer = new byte[] { (byte)'M', (byte)'Z', 0x00, 0x00 };
@@ -140,7 +149,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
result.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReturnsFalseForElf()
{
var buffer = new byte[] { 0x7F, (byte)'E', (byte)'L', (byte)'F' };
@@ -151,7 +161,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
result.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesVersionNumbers()
{
// Build Mach-O with versioned dylib using builder
@@ -159,6 +170,7 @@ public class MachOLoadCommandParserTests : NativeTestBase
.AddDylib("/usr/lib/libfoo.dylib", "1.2.3", "1.0.0")
.Build();
using StellaOps.TestKit;
var info = ParseMachO(macho);
info.Slices[0].Dependencies[0].CurrentVersion.Should().Be("1.2.3");

View File

@@ -3,6 +3,8 @@ using System.Security.Cryptography;
using System.Text;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.Native.Tests;
/// <summary>
@@ -391,7 +393,8 @@ public sealed class MachOReaderTests
#region Magic Detection Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_Returns_Null_For_Empty_Stream()
{
using var stream = new MemoryStream([]);
@@ -399,7 +402,8 @@ public sealed class MachOReaderTests
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_Returns_Null_For_Invalid_Magic()
{
var data = new byte[] { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77 };
@@ -408,7 +412,8 @@ public sealed class MachOReaderTests
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_Detects_64Bit_LittleEndian_MachO()
{
var data = BuildMachO64();
@@ -421,7 +426,8 @@ public sealed class MachOReaderTests
Assert.False(result.Identities[0].IsFatBinary);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_Detects_32Bit_MachO()
{
var data = BuildMachO32(cpuType: 7); // x86
@@ -437,7 +443,8 @@ public sealed class MachOReaderTests
#region LC_UUID Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_Extracts_LC_UUID()
{
var uuid = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10 };
@@ -450,7 +457,8 @@ public sealed class MachOReaderTests
Assert.Equal("0123456789abcdeffedcba9876543210", result.Identities[0].Uuid);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_Returns_Null_Uuid_When_Not_Present()
{
var data = BuildMachO64(uuid: null);
@@ -462,7 +470,8 @@ public sealed class MachOReaderTests
Assert.Null(result.Identities[0].Uuid);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_UUID_Is_Lowercase_Hex_No_Dashes()
{
var uuid = new byte[] { 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x12, 0x34, 0x56, 0x78, 0x9A };
@@ -482,7 +491,8 @@ public sealed class MachOReaderTests
#region Export Trie Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_Extracts_Exports_From_LC_DYLD_INFO_ONLY()
{
var data = BuildMachO64(exports: new[] { "_main", "_printf" }, exportsViaDyldInfoOnly: true);
@@ -494,7 +504,8 @@ public sealed class MachOReaderTests
Assert.Equal(new[] { "_main", "_printf" }, result.Identities[0].Exports);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_Extracts_Exports_From_LC_DYLD_EXPORTS_TRIE()
{
var data = BuildMachO64(exports: new[] { "_zeta", "_alpha" }, exportsViaDyldInfoOnly: false);
@@ -510,7 +521,8 @@ public sealed class MachOReaderTests
#region Platform Detection Tests
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(MachOPlatform.MacOS)]
[InlineData(MachOPlatform.iOS)]
[InlineData(MachOPlatform.TvOS)]
@@ -528,7 +540,8 @@ public sealed class MachOReaderTests
Assert.Equal(expectedPlatform, result.Identities[0].Platform);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_Extracts_MinOs_Version()
{
var data = BuildMachO64(minOs: 0x000E0500); // 14.5.0
@@ -539,7 +552,8 @@ public sealed class MachOReaderTests
Assert.Equal("14.5", result.Identities[0].MinOsVersion);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_Extracts_SDK_Version()
{
var data = BuildMachO64(sdk: 0x000F0000); // 15.0.0
@@ -550,7 +564,8 @@ public sealed class MachOReaderTests
Assert.Equal("15.0", result.Identities[0].SdkVersion);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_Version_With_Patch()
{
var data = BuildMachO64(minOs: 0x000E0501); // 14.5.1
@@ -565,7 +580,8 @@ public sealed class MachOReaderTests
#region Code Signature Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_UnsignedBinary_HasNull_CodeSignature()
{
var data = BuildMachO64();
@@ -577,7 +593,8 @@ public sealed class MachOReaderTests
Assert.Null(result.Identities[0].CodeSignature);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_SignedBinary_Extracts_SigningId_TeamId_CdHash_Entitlements_And_HardenedRuntime()
{
var signingId = "com.stellaops.demo";
@@ -615,7 +632,8 @@ public sealed class MachOReaderTests
#region CPU Type Tests
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(0x00000007, "i386")] // CPU_TYPE_X86
[InlineData(0x01000007, "x86_64")] // CPU_TYPE_X86_64
[InlineData(0x0000000C, "arm")] // CPU_TYPE_ARM
@@ -634,7 +652,8 @@ public sealed class MachOReaderTests
#region Fat Binary Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_Handles_Fat_Binary()
{
var arm64Slice = BuildMachO64(cpuType: 0x0100000C, uuid: new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 });
@@ -655,7 +674,8 @@ public sealed class MachOReaderTests
Assert.NotEqual(result.Identities[0].Uuid, result.Identities[1].Uuid);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParseFatBinary_Returns_Multiple_Identities()
{
var arm64Slice = BuildMachO64(cpuType: 0x0100000C);
@@ -672,7 +692,8 @@ public sealed class MachOReaderTests
#region TryExtractIdentity Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_Returns_True_For_Valid_MachO()
{
var data = BuildMachO64();
@@ -685,7 +706,8 @@ public sealed class MachOReaderTests
Assert.Equal("arm64", identity.CpuType);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_Returns_False_For_Invalid_Data()
{
var data = new byte[] { 0x00, 0x00, 0x00, 0x00 };
@@ -697,7 +719,8 @@ public sealed class MachOReaderTests
Assert.Null(identity);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_Returns_First_Slice_For_Fat_Binary()
{
var arm64Slice = BuildMachO64(cpuType: 0x0100000C);
@@ -718,7 +741,8 @@ public sealed class MachOReaderTests
#region Path and LayerDigest Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_Preserves_Path_And_LayerDigest()
{
var data = BuildMachO64();

View File

@@ -3,6 +3,7 @@ using StellaOps.Scanner.Analyzers.Native;
using StellaOps.Scanner.Analyzers.Native.Tests.Fixtures;
using StellaOps.Scanner.Analyzers.Native.Tests.TestUtilities;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.Native.Tests;
/// <summary>
@@ -13,7 +14,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
{
#region ELF Parameterized Tests
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(true, false)] // 64-bit, little-endian
[InlineData(true, true)] // 64-bit, big-endian
public void ElfBuilder_ParsesDependencies_AllFormats(bool is64Bit, bool isBigEndian)
@@ -34,7 +36,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
info.Dependencies[1].Soname.Should().Be("libm.so.6");
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("GLIBC_2.17", false)]
[InlineData("GLIBC_2.28", false)]
[InlineData("GLIBC_2.34", true)]
@@ -58,7 +61,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
dep.VersionNeeds[0].IsWeak.Should().Be(isWeak);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ElfBuilder_LinuxX64Factory_CreatesValidElf()
{
// Arrange
@@ -82,7 +86,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
#region PE Parameterized Tests
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(false)] // PE32 with 4-byte thunks
[InlineData(true)] // PE32+ with 8-byte thunks
public void PeBuilder_ParsesImports_CorrectBitness(bool is64Bit)
@@ -104,7 +109,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
info.Dependencies[0].ImportedFunctions.Should().Contain("LoadLibraryA");
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(PeSubsystem.WindowsConsole)]
[InlineData(PeSubsystem.WindowsGui)]
public void PeBuilder_SetsSubsystem_Correctly(PeSubsystem subsystem)
@@ -121,7 +127,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
info.Subsystem.Should().Be(subsystem);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PeBuilder_Console64Factory_CreatesValidPe()
{
// Arrange
@@ -140,7 +147,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
info.DelayLoadDependencies.Should().HaveCount(1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PeBuilder_WithManifest_CreatesValidPe()
{
// Arrange
@@ -160,7 +168,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
#region Mach-O Parameterized Tests
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(MachODylibKind.Load, "macho-loadlib")]
[InlineData(MachODylibKind.Weak, "macho-weaklib")]
[InlineData(MachODylibKind.Reexport, "macho-reexport")]
@@ -181,7 +190,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
info.Slices[0].Dependencies[0].ReasonCode.Should().Be(expectedReason);
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(MachOCpuType.X86_64, "x86_64")]
[InlineData(MachOCpuType.Arm64, "arm64")]
public void MachOBuilder_SetsCpuType_Correctly(MachOCpuType cpuType, string expectedName)
@@ -201,7 +211,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
info.Slices[0].CpuType.Should().Be(expectedName);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void MachOBuilder_MacOSArm64Factory_CreatesValidMachO()
{
// Arrange
@@ -225,7 +236,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
info.Slices[0].Uuid.Should().NotBeNullOrEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void MachOBuilder_Universal_CreatesFatBinary()
{
// Arrange
@@ -241,7 +253,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
info.Slices.Should().HaveCount(2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void MachOBuilder_WithVersion_ParsesVersionNumbers()
{
// Arrange
@@ -261,7 +274,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
#region Cross-Format Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AllBuilders_ProduceParseable_Binaries()
{
// Arrange
@@ -275,7 +289,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
TryParseMachO(macho, out _).Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AllBuilders_RejectWrongFormat()
{
// Arrange

View File

@@ -3,11 +3,14 @@ using System.IO;
using System.Linq;
using StellaOps.Scanner.Analyzers.Native;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.Native.Tests;
public class NativeFormatDetectorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DetectsElf64LittleEndian()
{
var bytes = new byte[64];
@@ -28,7 +31,8 @@ public class NativeFormatDetectorTests
Assert.Equal("le", id.Endianness);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DetectsElfInterpreterAndBuildId()
{
// Minimal ELF64 with two program headers: PT_INTERP and PT_NOTE (GNU build-id)
@@ -93,7 +97,8 @@ public class NativeFormatDetectorTests
Assert.Equal("gnu-build-id:0102030405060708090a0b0c0d0e0f10", id.BuildId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DetectsPe()
{
var bytes = new byte[256];
@@ -116,7 +121,8 @@ public class NativeFormatDetectorTests
Assert.Equal("le", id.Endianness);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DetectsMachO64()
{
var bytes = new byte[32];
@@ -134,7 +140,8 @@ public class NativeFormatDetectorTests
Assert.Equal("darwin", id.OperatingSystem);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ExtractsMachOUuid()
{
var buffer = new byte[128];
@@ -161,7 +168,8 @@ public class NativeFormatDetectorTests
Assert.Equal($"macho-uuid:{Convert.ToHexString(uuid.ToByteArray()).ToLowerInvariant()}", id.Uuid);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReturnsUnknownForUnsupported()
{
var bytes = new byte[] { 0x00, 0x01, 0x02, 0x03 };

View File

@@ -3,11 +3,14 @@ using FluentAssertions;
using StellaOps.Scanner.Analyzers.Native.Observations;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.Native.Tests;
public class NativeObservationSerializerTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Serialize_ProducesValidJson()
{
// Arrange
@@ -22,7 +25,8 @@ public class NativeObservationSerializerTests
parsed.RootElement.GetProperty("$schema").GetString().Should().Be("stellaops.native.observation@1");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Serialize_OmitsNullProperties()
{
// Arrange
@@ -36,7 +40,8 @@ public class NativeObservationSerializerTests
json.Should().NotContain("\"build_id\"");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SerializePretty_ProducesFormattedJson()
{
// Arrange
@@ -50,7 +55,8 @@ public class NativeObservationSerializerTests
json.Should().Contain(" ");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Deserialize_RestoresDocument()
{
// Arrange
@@ -68,7 +74,8 @@ public class NativeObservationSerializerTests
restored.HeuristicEdges.Should().HaveCount(original.HeuristicEdges.Count);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeSha256_ProducesConsistentHash()
{
// Arrange
@@ -84,7 +91,8 @@ public class NativeObservationSerializerTests
hash1.Should().MatchRegex("^[a-f0-9]+$");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SerializeToBytes_ProducesUtf8()
{
// Arrange
@@ -99,7 +107,8 @@ public class NativeObservationSerializerTests
json.Should().StartWith("{");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task WriteAsync_WritesToStream()
{
// Arrange
@@ -116,7 +125,8 @@ public class NativeObservationSerializerTests
json.Should().Contain("stellaops.native.observation@1");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ReadAsync_ReadsFromStream()
{
// Arrange
@@ -132,7 +142,8 @@ public class NativeObservationSerializerTests
doc!.Binary.Path.Should().Be(original.Binary.Path);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Deserialize_EmptyString_ReturnsNull()
{
// Act
@@ -230,7 +241,8 @@ public class NativeObservationSerializerTests
public class NativeObservationBuilderTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithBinary_CreatesDocument()
{
// Arrange & Act
@@ -243,7 +255,8 @@ public class NativeObservationBuilderTests
doc.Binary.Format.Should().Be("elf");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithoutBinary_ThrowsException()
{
// Arrange
@@ -255,7 +268,8 @@ public class NativeObservationBuilderTests
.WithMessage("*Binary*");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddEntrypoint_AddsToList()
{
// Arrange & Act
@@ -272,7 +286,8 @@ public class NativeObservationBuilderTests
doc.Entrypoints[0].Conditions.Should().Contain("linux");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddElfDependencies_AddsEdgesAndEnvironment()
{
// Arrange
@@ -302,7 +317,8 @@ public class NativeObservationBuilderTests
doc.Environment.Runpath.Should().Contain("/app/lib");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddPeDependencies_AddsEdgesAndSxs()
{
// Arrange
@@ -339,7 +355,8 @@ public class NativeObservationBuilderTests
doc.Environment.SxsDependencies![0].Name.Should().Be("Microsoft.VC90.CRT");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddMachODependencies_AddsEdgesAndRpaths()
{
// Arrange
@@ -372,7 +389,8 @@ public class NativeObservationBuilderTests
doc.Environment.MachORpaths.Should().Contain("@loader_path/../Frameworks");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddHeuristicResults_AddsEdgesAndPluginConfigs()
{
// Arrange
@@ -396,7 +414,8 @@ public class NativeObservationBuilderTests
doc.Environment.PluginConfigs.Should().HaveCount(2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddResolution_AddsExplainTrace()
{
// Arrange
@@ -424,7 +443,8 @@ public class NativeObservationBuilderTests
doc.Resolution[0].Steps[1].Found.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FullIntegration_BuildsCompleteDocument()
{
// Arrange & Act

View File

@@ -1,11 +1,13 @@
using FluentAssertions;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.Native.Tests;
public class ElfResolverTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_WithRpath_FindsLibraryInRpathDirectory()
{
// Arrange
@@ -26,7 +28,8 @@ public class ElfResolverTests
s.Found == true);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_WithRunpath_IgnoresRpath()
{
// Arrange - library exists in rpath but not runpath
@@ -47,7 +50,8 @@ public class ElfResolverTests
result.Steps.Should().Contain(s => s.SearchReason == "runpath");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_WithLdLibraryPath_SearchesBeforeRunpath()
{
// Arrange
@@ -68,7 +72,8 @@ public class ElfResolverTests
result.Steps.First().SearchReason.Should().Be("ld_library_path");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_WithOriginExpansion_ExpandsOriginVariable()
{
// Arrange
@@ -84,7 +89,8 @@ public class ElfResolverTests
result.ResolvedPath.Should().Be("/app/bin/../lib/libfoo.so.1");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_WithOriginBraceSyntax_ExpandsOriginVariable()
{
// Arrange
@@ -100,7 +106,8 @@ public class ElfResolverTests
result.ResolvedPath.Should().Be("/app/bin/../lib/libbar.so.2");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_NotFound_ReturnsUnresolvedWithSteps()
{
// Arrange
@@ -119,7 +126,8 @@ public class ElfResolverTests
result.Steps.Should().Contain(s => s.SearchReason == "default" && !s.Found);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_WithDefaultPaths_SearchesSystemDirectories()
{
// Arrange
@@ -134,7 +142,8 @@ public class ElfResolverTests
result.Steps.Should().Contain(s => s.SearchReason == "default");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_SearchOrder_FollowsCorrectPriority()
{
// Arrange - library exists in all locations
@@ -158,7 +167,8 @@ public class ElfResolverTests
public class PeResolverTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_InApplicationDirectory_FindsDll()
{
// Arrange
@@ -174,7 +184,8 @@ public class PeResolverTests
.Which.SearchReason.Should().Be("application_directory");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_InSystem32_FindsDll()
{
// Arrange
@@ -189,7 +200,8 @@ public class PeResolverTests
result.Steps.Should().Contain(s => s.SearchReason == "system_directory");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_InSysWOW64_FindsDll()
{
// Arrange
@@ -203,7 +215,8 @@ public class PeResolverTests
result.ResolvedPath.Should().Be("C:/Windows/SysWOW64/wow64.dll");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_InCurrentDirectory_FindsDll()
{
// Arrange
@@ -218,7 +231,8 @@ public class PeResolverTests
result.Steps.Should().Contain(s => s.SearchReason == "current_directory" && s.Found);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_InPathEnvironment_FindsDll()
{
// Arrange
@@ -234,7 +248,8 @@ public class PeResolverTests
result.Steps.Should().Contain(s => s.SearchReason == "path_environment" && s.Found);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_SafeDllSearchOrder_ApplicationBeforeSystem()
{
// Arrange - DLL exists in both app dir and system32
@@ -252,7 +267,8 @@ public class PeResolverTests
result.Steps.First().SearchReason.Should().Be("application_directory");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_NotFound_ReturnsAllSearchedPaths()
{
// Arrange
@@ -272,7 +288,8 @@ public class PeResolverTests
result.Steps.Should().Contain(s => s.SearchReason == "path_environment");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_WithNullApplicationDirectory_SkipsAppDirSearch()
{
// Arrange
@@ -289,7 +306,8 @@ public class PeResolverTests
public class MachOResolverTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_WithRpath_ExpandsAndFindsLibrary()
{
// Arrange
@@ -306,7 +324,8 @@ public class MachOResolverTests
.Which.SearchReason.Should().Be("rpath");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_WithMultipleRpaths_SearchesInOrder()
{
// Arrange
@@ -324,7 +343,8 @@ public class MachOResolverTests
result.Steps[1].Found.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_WithLoaderPath_ExpandsPlaceholder()
{
// Arrange
@@ -345,7 +365,8 @@ public class MachOResolverTests
.Which.SearchReason.Should().Be("loader_path");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_WithExecutablePath_ExpandsPlaceholder()
{
// Arrange
@@ -365,7 +386,8 @@ public class MachOResolverTests
.Which.SearchReason.Should().Be("executable_path");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_WithRpathContainingLoaderPath_ExpandsBoth()
{
// Arrange
@@ -380,7 +402,8 @@ public class MachOResolverTests
result.ResolvedPath.Should().Be("/app/bin/../lib/libfoo.dylib");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_AbsolutePath_ChecksDirectly()
{
// Arrange
@@ -396,7 +419,8 @@ public class MachOResolverTests
.Which.SearchReason.Should().Be("absolute_path");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_RelativePath_SearchesDefaultPaths()
{
// Arrange
@@ -411,7 +435,8 @@ public class MachOResolverTests
result.Steps.Should().Contain(s => s.SearchReason == "default_library_path");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_RpathNotFound_FallsBackToDefaultPaths()
{
// Arrange - library not in rpath but in default path
@@ -428,7 +453,8 @@ public class MachOResolverTests
result.Steps.Should().Contain(s => s.SearchReason == "default_library_path" && s.Found);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_NotFound_ReturnsAllSearchedPaths()
{
// Arrange
@@ -445,7 +471,8 @@ public class MachOResolverTests
result.Steps.Should().OnlyContain(s => !s.Found);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_LoaderPathNotFound_ReturnsFalse()
{
// Arrange
@@ -463,7 +490,8 @@ public class MachOResolverTests
public class VirtualFileSystemTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FileExists_WithExistingFile_ReturnsTrue()
{
// Arrange
@@ -473,7 +501,8 @@ public class VirtualFileSystemTests
fs.FileExists("/usr/lib/libc.so.6").Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FileExists_WithNonExistingFile_ReturnsFalse()
{
// Arrange
@@ -483,7 +512,8 @@ public class VirtualFileSystemTests
fs.FileExists("/usr/lib/missing.so").Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FileExists_IsCaseInsensitive()
{
// Arrange
@@ -493,7 +523,8 @@ public class VirtualFileSystemTests
fs.FileExists("/usr/lib/LIBC.SO.6").Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DirectoryExists_WithExistingDirectory_ReturnsTrue()
{
// Arrange
@@ -504,7 +535,8 @@ public class VirtualFileSystemTests
fs.DirectoryExists("/usr/lib/x86_64-linux-gnu").Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void NormalizePath_HandlesBackslashes()
{
// Arrange
@@ -514,7 +546,8 @@ public class VirtualFileSystemTests
fs.FileExists("C:\\Windows\\System32\\kernel32.dll").Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void EnumerateFiles_ReturnsFilesInDirectory()
{
// Arrange

View File

@@ -7,7 +7,8 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests;
public class PeImportParserTests : NativeTestBase
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesMinimalPe32()
{
// Build minimal PE32 using builder
@@ -23,7 +24,8 @@ public class PeImportParserTests : NativeTestBase
info.Subsystem.Should().Be(PeSubsystem.WindowsConsole);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesMinimalPe32Plus()
{
// Build minimal PE32+ using builder
@@ -35,7 +37,8 @@ public class PeImportParserTests : NativeTestBase
info.Machine.Should().Be("x86_64");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesPeWithImports()
{
// Build PE with imports using builder
@@ -52,7 +55,8 @@ public class PeImportParserTests : NativeTestBase
info.Dependencies[1].DllName.Should().Be("user32.dll");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeduplicatesImports()
{
// Build PE with duplicate imports - builder or parser should deduplicate
@@ -67,7 +71,8 @@ public class PeImportParserTests : NativeTestBase
info.Dependencies[0].DllName.Should().Be("kernel32.dll");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesDelayLoadImports()
{
// Build PE with delay imports using builder
@@ -82,7 +87,8 @@ public class PeImportParserTests : NativeTestBase
info.DelayLoadDependencies[0].ReasonCode.Should().Be("pe-delayimport");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesSubsystem()
{
// Build PE with GUI subsystem using builder
@@ -95,7 +101,8 @@ public class PeImportParserTests : NativeTestBase
info.Subsystem.Should().Be(PeSubsystem.WindowsGui);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReturnsFalseForNonPe()
{
var buffer = new byte[] { 0x7F, (byte)'E', (byte)'L', (byte)'F' };
@@ -106,7 +113,8 @@ public class PeImportParserTests : NativeTestBase
result.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReturnsFalseForTruncatedPe()
{
var buffer = new byte[] { (byte)'M', (byte)'Z' };
@@ -117,7 +125,8 @@ public class PeImportParserTests : NativeTestBase
result.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesEmbeddedManifest()
{
// Build PE with SxS dependency manifest using builder
@@ -126,13 +135,15 @@ public class PeImportParserTests : NativeTestBase
"6595b64144ccf1df", "*")
.Build();
using StellaOps.TestKit;
var info = ParsePe(pe);
info.SxsDependencies.Should().HaveCountGreaterOrEqualTo(1);
info.SxsDependencies[0].Name.Should().Be("Microsoft.Windows.Common-Controls");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesPe32PlusWithImportThunks()
{
// Test that 64-bit PE files correctly parse 8-byte import thunks
@@ -150,7 +161,8 @@ public class PeImportParserTests : NativeTestBase
info.Dependencies[0].ImportedFunctions.Should().Contain("LoadLibraryA");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParsesPeWithEmbeddedResourceManifest()
{
// Test that manifest is properly extracted from PE resources

View File

@@ -3,6 +3,8 @@ using StellaOps.Scanner.Analyzers.Native;
using StellaOps.Scanner.Analyzers.Native.Tests.Fixtures;
using StellaOps.Scanner.Analyzers.Native.Tests.TestUtilities;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.Native.Tests;
/// <summary>
@@ -12,7 +14,8 @@ public class PeReaderTests : NativeTestBase
{
#region Basic Parsing
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_InvalidData_ReturnsFalse()
{
// Arrange
@@ -26,7 +29,8 @@ public class PeReaderTests : NativeTestBase
identity.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_TooShort_ReturnsFalse()
{
// Arrange
@@ -39,7 +43,8 @@ public class PeReaderTests : NativeTestBase
result.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_MissingMzSignature_ReturnsFalse()
{
// Arrange
@@ -54,7 +59,8 @@ public class PeReaderTests : NativeTestBase
result.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_ValidMinimalPe64_ReturnsTrue()
{
// Arrange
@@ -71,7 +77,8 @@ public class PeReaderTests : NativeTestBase
identity.Subsystem.Should().Be(PeSubsystem.WindowsConsole);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_ValidMinimalPe32_ReturnsTrue()
{
// Arrange
@@ -90,7 +97,8 @@ public class PeReaderTests : NativeTestBase
identity.Machine.Should().Be("x86");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_GuiSubsystem_ParsesCorrectly()
{
// Arrange
@@ -112,7 +120,8 @@ public class PeReaderTests : NativeTestBase
#region Parse Method
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_ValidPeStream_ReturnsPeParseResult()
{
// Arrange
@@ -128,7 +137,8 @@ public class PeReaderTests : NativeTestBase
result.Identity.Is64Bit.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_InvalidStream_ReturnsNull()
{
// Arrange
@@ -142,7 +152,8 @@ public class PeReaderTests : NativeTestBase
result.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_ThrowsOnNullStream()
{
// Act & Assert
@@ -154,7 +165,8 @@ public class PeReaderTests : NativeTestBase
#region Machine Architecture
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(PeMachine.I386, "x86", false)]
[InlineData(PeMachine.Amd64, "x86_64", true)]
[InlineData(PeMachine.Arm64, "arm64", true)]
@@ -179,7 +191,8 @@ public class PeReaderTests : NativeTestBase
#region Exports
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_NoExports_ReturnsEmptyList()
{
// Arrange - standard console app has no exports
@@ -198,7 +211,8 @@ public class PeReaderTests : NativeTestBase
#region Compiler Hints (Rich Header)
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_NoRichHeader_ReturnsEmptyHints()
{
// Arrange - builder-generated PEs don't have rich header
@@ -214,7 +228,8 @@ public class PeReaderTests : NativeTestBase
identity.RichHeaderHash.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_RichHeader_ExtractsCompilerHints()
{
// Arrange
@@ -236,7 +251,8 @@ public class PeReaderTests : NativeTestBase
#region CodeView Debug Info
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_NoDebugDirectory_ReturnsNullCodeView()
{
// Arrange - builder-generated PEs don't have debug directory
@@ -253,7 +269,8 @@ public class PeReaderTests : NativeTestBase
identity.PdbPath.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_CodeViewDebugInfo_ExtractsGuidAgeAndPdbPath()
{
// Arrange
@@ -274,7 +291,8 @@ public class PeReaderTests : NativeTestBase
#region Version Resources
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_NoVersionResource_ReturnsNullVersions()
{
// Arrange - builder-generated PEs don't have version resources
@@ -293,7 +311,8 @@ public class PeReaderTests : NativeTestBase
identity.OriginalFilename.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_VersionResource_ExtractsStrings()
{
// Arrange
@@ -316,7 +335,8 @@ public class PeReaderTests : NativeTestBase
#region Golden Fixtures
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_Exports_ExtractsExportNames()
{
// Arrange
@@ -331,7 +351,8 @@ public class PeReaderTests : NativeTestBase
identity!.Exports.Should().ContainSingle().Which.Should().Be("mingw_export");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_MingwFixture_HasNoRichOrCodeView()
{
// Arrange
@@ -354,7 +375,8 @@ public class PeReaderTests : NativeTestBase
#region Determinism
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_SameInput_ReturnsSameOutput()
{
// Arrange
@@ -368,7 +390,8 @@ public class PeReaderTests : NativeTestBase
identity1.Should().BeEquivalentTo(identity2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_DifferentInputs_ReturnsDifferentOutput()
{
// Arrange
@@ -387,7 +410,8 @@ public class PeReaderTests : NativeTestBase
#region Edge Cases
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_InvalidPeOffset_ReturnsFalse()
{
// Arrange - Create data with MZ signature but invalid PE offset
@@ -407,7 +431,8 @@ public class PeReaderTests : NativeTestBase
result.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_MissingPeSignature_ReturnsFalse()
{
// Arrange - Create data with MZ but missing PE signature
@@ -424,7 +449,8 @@ public class PeReaderTests : NativeTestBase
result.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryExtractIdentity_InvalidMagic_ReturnsFalse()
{
// Arrange - Create data with PE signature but invalid magic

View File

@@ -7,6 +7,8 @@ using StellaOps.Scanner.Analyzers.Native;
using StellaOps.Scanner.Analyzers.Native.Observations;
using StellaOps.Scanner.Analyzers.Native.Plugin;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.Native.Tests;
/// <summary>
@@ -16,7 +18,8 @@ public sealed class PluginPackagingTests
{
#region INativeAnalyzerPlugin Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void NativeAnalyzerPlugin_Properties_AreConfigured()
{
var plugin = new NativeAnalyzerPlugin();
@@ -26,7 +29,8 @@ public sealed class PluginPackagingTests
plugin.Version.Should().Be("1.0.0");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void NativeAnalyzerPlugin_SupportedFormats_ContainsAllFormats()
{
var plugin = new NativeAnalyzerPlugin();
@@ -36,7 +40,8 @@ public sealed class PluginPackagingTests
plugin.SupportedFormats.Should().Contain(NativeFormat.MachO);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void NativeAnalyzerPlugin_IsAvailable_ReturnsTrue()
{
var plugin = new NativeAnalyzerPlugin();
@@ -47,7 +52,8 @@ public sealed class PluginPackagingTests
available.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void NativeAnalyzerPlugin_CreateAnalyzer_ReturnsAnalyzer()
{
var plugin = new NativeAnalyzerPlugin();
@@ -65,7 +71,8 @@ public sealed class PluginPackagingTests
#region NativeAnalyzerPluginCatalog Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PluginCatalog_Constructor_RegistersBuiltInPlugin()
{
var logger = NullLogger<NativeAnalyzerPluginCatalog>.Instance;
@@ -75,7 +82,8 @@ public sealed class PluginPackagingTests
catalog.Plugins[0].Name.Should().Be("Native Binary Analyzer");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PluginCatalog_Register_AddsPlugin()
{
var logger = NullLogger<NativeAnalyzerPluginCatalog>.Instance;
@@ -88,7 +96,8 @@ public sealed class PluginPackagingTests
catalog.Plugins.Should().Contain(p => p.Name == "Test Plugin");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PluginCatalog_Register_IgnoresDuplicates()
{
var logger = NullLogger<NativeAnalyzerPluginCatalog>.Instance;
@@ -102,7 +111,8 @@ public sealed class PluginPackagingTests
catalog.Plugins.Count(p => p.Name == "Test Plugin").Should().Be(1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PluginCatalog_Seal_PreventsModification()
{
var logger = NullLogger<NativeAnalyzerPluginCatalog>.Instance;
@@ -115,7 +125,8 @@ public sealed class PluginPackagingTests
.WithMessage("*sealed*");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PluginCatalog_LoadFromDirectory_DoesNotFailForMissingDirectory()
{
var logger = NullLogger<NativeAnalyzerPluginCatalog>.Instance;
@@ -126,7 +137,8 @@ public sealed class PluginPackagingTests
act.Should().NotThrow();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PluginCatalog_CreateAnalyzers_CreatesFromAvailablePlugins()
{
var logger = NullLogger<NativeAnalyzerPluginCatalog>.Instance;
@@ -140,7 +152,8 @@ public sealed class PluginPackagingTests
analyzers.Should().HaveCount(1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PluginCatalog_CreateAnalyzers_SkipsUnavailablePlugins()
{
var logger = NullLogger<NativeAnalyzerPluginCatalog>.Instance;
@@ -162,7 +175,8 @@ public sealed class PluginPackagingTests
#region ServiceCollectionExtensions Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddNativeAnalyzer_RegistersServices()
{
var services = new ServiceCollection();
@@ -173,7 +187,8 @@ public sealed class PluginPackagingTests
services.Should().Contain(s => s.ServiceType == typeof(INativeAnalyzer));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddNativeAnalyzer_WithOptions_ConfiguresOptions()
{
var services = new ServiceCollection();
@@ -191,7 +206,8 @@ public sealed class PluginPackagingTests
options.Value.DefaultTimeout.Should().Be(TimeSpan.FromMinutes(5));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void NativeAnalyzerServiceOptions_DefaultValues()
{
var options = new NativeAnalyzerServiceOptions();
@@ -202,7 +218,8 @@ public sealed class PluginPackagingTests
options.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(30));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void NativeAnalyzerServiceOptions_GetDefaultSearchPathsForFormat_ReturnsCorrectPaths()
{
var options = new NativeAnalyzerServiceOptions();
@@ -218,7 +235,8 @@ public sealed class PluginPackagingTests
unknownPaths.Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddNativeRuntimeCapture_RegistersAdapter()
{
var services = new ServiceCollection();
@@ -233,7 +251,8 @@ public sealed class PluginPackagingTests
#region NativeAnalyzerOptions Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void NativeAnalyzerOptions_DefaultValues()
{
var options = new NativeAnalyzerOptions();
@@ -249,7 +268,8 @@ public sealed class PluginPackagingTests
#region INativeAnalyzer Integration Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task NativeAnalyzer_AnalyzeAsync_ThrowsForUnknownFormat()
{
var logger = NullLogger<NativeAnalyzer>.Instance;
@@ -264,7 +284,8 @@ public sealed class PluginPackagingTests
.WithMessage("*Unknown or unsupported*");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task NativeAnalyzer_AnalyzeBatchAsync_YieldsResults()
{
var logger = NullLogger<NativeAnalyzer>.Instance;
@@ -294,7 +315,8 @@ public sealed class PluginPackagingTests
results[0].Binary.Format.Should().Be("elf");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task NativeAnalyzer_AnalyzeAsync_ParsesElfBinary()
{
var logger = NullLogger<NativeAnalyzer>.Instance;

View File

@@ -6,7 +6,8 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests;
public class RuntimeCaptureOptionsTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_DefaultOptions_ReturnsNoErrors()
{
// Arrange
@@ -19,7 +20,8 @@ public class RuntimeCaptureOptionsTests
errors.Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_InvalidBufferSize_ReturnsError()
{
// Arrange
@@ -32,7 +34,8 @@ public class RuntimeCaptureOptionsTests
errors.Should().Contain(e => e.Contains("BufferSize"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_NegativeCaptureDuration_ReturnsError()
{
// Arrange
@@ -45,7 +48,8 @@ public class RuntimeCaptureOptionsTests
errors.Should().Contain(e => e.Contains("MaxCaptureDuration"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_ExcessiveCaptureDuration_ReturnsError()
{
// Arrange
@@ -58,7 +62,8 @@ public class RuntimeCaptureOptionsTests
errors.Should().Contain(e => e.Contains("MaxCaptureDuration"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_SandboxWithoutRoot_ReturnsError()
{
// Arrange
@@ -79,7 +84,8 @@ public class RuntimeCaptureOptionsTests
errors.Should().Contain(e => e.Contains("SandboxRoot"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_SandboxWithRoot_ReturnsNoSandboxErrors()
{
// Arrange
@@ -102,7 +108,8 @@ public class RuntimeCaptureOptionsTests
public class RedactionOptionsTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ApplyRedaction_HomePath_IsRedacted()
{
// Arrange
@@ -116,7 +123,8 @@ public class RedactionOptionsTests
result.Should().Contain("[REDACTED]");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ApplyRedaction_WindowsUserPath_IsRedacted()
{
// Arrange
@@ -130,7 +138,8 @@ public class RedactionOptionsTests
result.Should().Contain("[REDACTED]");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ApplyRedaction_SystemPath_NotRedacted()
{
// Arrange
@@ -144,7 +153,8 @@ public class RedactionOptionsTests
result.Should().Be("/usr/lib/libc.so.6");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ApplyRedaction_DisabledRedaction_NotRedacted()
{
// Arrange
@@ -158,7 +168,8 @@ public class RedactionOptionsTests
result.Should().Be("/home/testuser/secret.so");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ApplyRedaction_SshPath_IsRedacted()
{
// Arrange
@@ -172,7 +183,8 @@ public class RedactionOptionsTests
result.Should().Contain("[REDACTED]");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ApplyRedaction_KeyFile_IsRedacted()
{
// Arrange
@@ -186,7 +198,8 @@ public class RedactionOptionsTests
result.Should().Contain("[REDACTED]");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_InvalidRegex_ReturnsError()
{
// Arrange
@@ -202,7 +215,8 @@ public class RedactionOptionsTests
errors.Should().Contain(e => e.Contains("Invalid redaction regex"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_EmptyReplacement_ReturnsError()
{
// Arrange
@@ -222,7 +236,8 @@ public class RedactionOptionsTests
public class RuntimeEvidenceAggregatorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Aggregate_EmptySessions_ReturnsEmptyEvidence()
{
// Arrange
@@ -237,7 +252,8 @@ public class RuntimeEvidenceAggregatorTests
evidence.RuntimeEdges.Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Aggregate_SingleSession_ReturnsCorrectSummary()
{
// Arrange
@@ -293,7 +309,8 @@ public class RuntimeEvidenceAggregatorTests
libfoo.CallerModules.Should().Contain("myapp");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Aggregate_DuplicateLoads_AggregatesCorrectly()
{
// Arrange
@@ -316,7 +333,8 @@ public class RuntimeEvidenceAggregatorTests
evidence.UniqueLibraries[0].FirstSeen.Should().Be(baseTime);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Aggregate_FailedLoads_NotIncludedInSummary()
{
// Arrange
@@ -335,7 +353,8 @@ public class RuntimeEvidenceAggregatorTests
evidence.RuntimeEdges.Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Aggregate_MultipleSessions_MergesCorrectly()
{
// Arrange
@@ -363,7 +382,8 @@ public class RuntimeEvidenceAggregatorTests
public class RuntimeCaptureAdapterFactoryTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateForCurrentPlatform_ReturnsAdapter()
{
// Act
@@ -381,7 +401,8 @@ public class RuntimeCaptureAdapterFactoryTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetAvailableAdapters_ReturnsAdaptersForCurrentPlatform()
{
// Act
@@ -402,7 +423,8 @@ public class RuntimeCaptureAdapterFactoryTests
public class SandboxCaptureTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SandboxCapture_WithMockEvents_CapturesEvents()
{
// Arrange
@@ -448,7 +470,8 @@ public class SandboxCaptureTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SandboxCapture_StateTransitions_AreCorrect()
{
// Arrange
@@ -488,7 +511,8 @@ public class SandboxCaptureTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SandboxCapture_CannotStartWhileRunning()
{
// Arrange
@@ -517,6 +541,7 @@ public class SandboxCaptureTests
{
await adapter.StartCaptureAsync(options);
using StellaOps.TestKit;
// Act & Assert
var act = async () => await adapter.StartCaptureAsync(options);
await act.Should().ThrowAsync<InvalidOperationException>();
@@ -528,7 +553,8 @@ public class SandboxCaptureTests
public class RuntimeEvidenceModelTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RuntimeLoadEvent_RecordEquality_Works()
{
// Arrange
@@ -542,7 +568,8 @@ public class RuntimeEvidenceModelTests
event1.Should().NotBe(event3);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RuntimeLoadType_AllTypesHaveReasonCodes()
{
// Arrange

View File

@@ -11,6 +11,7 @@
<ItemGroup>
<ProjectReference Include="../../StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Analyzers.OS.Homebrew;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.OS.Homebrew.Tests;
public sealed class HomebrewPackageAnalyzerTests
@@ -29,13 +30,15 @@ public sealed class HomebrewPackageAnalyzerTests
_logger);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AnalyzerId_ReturnsHomebrew()
{
Assert.Equal("homebrew", _analyzer.AnalyzerId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithValidCellar_ReturnsPackages()
{
// Arrange
@@ -50,7 +53,8 @@ public sealed class HomebrewPackageAnalyzerTests
Assert.True(result.Packages.Count > 0, "Expected at least one package");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_FindsIntelCellarPackages()
{
// Arrange
@@ -67,7 +71,8 @@ public sealed class HomebrewPackageAnalyzerTests
Assert.Contains("pkg:brew/homebrew%2Fcore/openssl%403@3.1.0", openssl.PackageUrl);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_FindsAppleSiliconCellarPackages()
{
// Arrange
@@ -83,7 +88,8 @@ public sealed class HomebrewPackageAnalyzerTests
Assert.Equal("arm64", jq.Architecture);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_PackageWithRevision_IncludesRevisionInPurl()
{
// Arrange
@@ -99,7 +105,8 @@ public sealed class HomebrewPackageAnalyzerTests
Assert.Equal("1", wget.Release);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ExtractsDependencies()
{
// Arrange
@@ -115,7 +122,8 @@ public sealed class HomebrewPackageAnalyzerTests
Assert.Contains("gettext", wget.Depends);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ExtractsVendorMetadata()
{
// Arrange
@@ -133,7 +141,8 @@ public sealed class HomebrewPackageAnalyzerTests
Assert.Equal("https://openssl.org/", openssl.VendorMetadata["homepage"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_SetsEvidenceSourceToHomebrewCellar()
{
// Arrange
@@ -149,7 +158,8 @@ public sealed class HomebrewPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_DiscoversBinFiles()
{
// Arrange
@@ -164,7 +174,8 @@ public sealed class HomebrewPackageAnalyzerTests
Assert.Contains(wget.Files, f => f.Path.Contains("wget"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ResultsAreDeterministicallySorted()
{
// Arrange
@@ -182,7 +193,8 @@ public sealed class HomebrewPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_NoCellar_ReturnsEmptyPackages()
{
// Arrange - use temp directory without Cellar structure
@@ -205,7 +217,8 @@ public sealed class HomebrewPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_PopulatesTelemetry()
{
// Arrange

View File

@@ -2,13 +2,16 @@ using System.Text;
using StellaOps.Scanner.Analyzers.OS.Homebrew;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.OS.Homebrew.Tests;
public sealed class HomebrewReceiptParserTests
{
private readonly HomebrewReceiptParser _parser = new();
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_ValidReceipt_ReturnsExpectedValues()
{
// Arrange
@@ -51,7 +54,8 @@ public sealed class HomebrewReceiptParserTests
Assert.Equal("x86_64", receipt.Architecture);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithRevision_ReturnsCorrectRevision()
{
// Arrange
@@ -77,7 +81,8 @@ public sealed class HomebrewReceiptParserTests
Assert.Equal(1, receipt.Revision);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_AppleSilicon_ReturnsArm64Architecture()
{
// Arrange
@@ -100,7 +105,8 @@ public sealed class HomebrewReceiptParserTests
Assert.Equal("arm64", receipt.Architecture);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithSourceInfo_ExtractsSourceUrlAndChecksum()
{
// Arrange
@@ -126,7 +132,8 @@ public sealed class HomebrewReceiptParserTests
Assert.Equal("sha256:abcdef123456", receipt.SourceChecksum);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_MultipleDependencies_SortsAlphabetically()
{
// Arrange
@@ -155,7 +162,8 @@ public sealed class HomebrewReceiptParserTests
Assert.Equal("zlib", receipt.RuntimeDependencies[2]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_InvalidJson_ReturnsNull()
{
// Arrange
@@ -169,7 +177,8 @@ public sealed class HomebrewReceiptParserTests
Assert.Null(receipt);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_EmptyJson_ReturnsNull()
{
// Arrange
@@ -183,7 +192,8 @@ public sealed class HomebrewReceiptParserTests
Assert.Null(receipt);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_MissingName_ReturnsNull()
{
// Arrange
@@ -202,7 +212,8 @@ public sealed class HomebrewReceiptParserTests
Assert.Null(receipt);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_TappedFrom_UsesTappedFromOverTap()
{
// Arrange
@@ -224,7 +235,8 @@ public sealed class HomebrewReceiptParserTests
Assert.Equal("custom/tap", receipt.Tap);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_FallbackVersion_UsesVersionFieldWhenVersionsStableMissing()
{
// Arrange - older receipt format uses version field directly
@@ -245,7 +257,8 @@ public sealed class HomebrewReceiptParserTests
Assert.Equal("2.0.0", receipt.Version);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_NormalizesArchitecture_AArch64ToArm64()
{
// Arrange

View File

@@ -20,6 +20,7 @@
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS/StellaOps.Scanner.Analyzers.OS.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS.Homebrew/StellaOps.Scanner.Analyzers.OS.Homebrew.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />

View File

@@ -1,6 +1,7 @@
using StellaOps.Scanner.Analyzers.OS.MacOsBundle;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests;
public sealed class EntitlementsParserTests
@@ -11,7 +12,8 @@ public sealed class EntitlementsParserTests
private readonly EntitlementsParser _parser = new();
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_ValidEntitlements_ReturnsEntitlements()
{
// Arrange
@@ -25,7 +27,8 @@ public sealed class EntitlementsParserTests
Assert.True(result.IsSandboxed);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_DetectsHighRiskEntitlements()
{
// Arrange
@@ -40,7 +43,8 @@ public sealed class EntitlementsParserTests
Assert.Contains("com.apple.security.device.microphone", result.HighRiskEntitlements);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_CategorizeEntitlements()
{
// Arrange
@@ -57,7 +61,8 @@ public sealed class EntitlementsParserTests
Assert.Contains("sandbox", result.Categories);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_NonExistentFile_ReturnsEmpty()
{
// Arrange
@@ -70,7 +75,8 @@ public sealed class EntitlementsParserTests
Assert.Same(BundleEntitlements.Empty, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FindEntitlementsFile_FindsXcentFile()
{
// Arrange
@@ -84,7 +90,8 @@ public sealed class EntitlementsParserTests
Assert.EndsWith(".xcent", result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FindEntitlementsFile_NoBundlePath_ReturnsNull()
{
// Act
@@ -94,7 +101,8 @@ public sealed class EntitlementsParserTests
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FindEntitlementsFile_NoEntitlements_ReturnsNull()
{
// Arrange - bundle without entitlements
@@ -107,7 +115,8 @@ public sealed class EntitlementsParserTests
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void HasEntitlement_ReturnsTrueForExistingEntitlement()
{
// Arrange
@@ -119,7 +128,8 @@ public sealed class EntitlementsParserTests
Assert.True(result.HasEntitlement("com.apple.security.device.camera"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void HasEntitlement_ReturnsFalseForMissingEntitlement()
{
// Arrange

View File

@@ -1,6 +1,7 @@
using StellaOps.Scanner.Analyzers.OS.MacOsBundle;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests;
public sealed class InfoPlistParserTests
@@ -11,7 +12,8 @@ public sealed class InfoPlistParserTests
private readonly InfoPlistParser _parser = new();
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_ValidInfoPlist_ReturnsBundleInfo()
{
// Arrange
@@ -29,7 +31,8 @@ public sealed class InfoPlistParserTests
Assert.Equal("1.2.3", result.ShortVersion);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_ExtractsMinimumSystemVersion()
{
// Arrange
@@ -43,7 +46,8 @@ public sealed class InfoPlistParserTests
Assert.Equal("12.0", result.MinimumSystemVersion);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_ExtractsExecutable()
{
// Arrange
@@ -57,7 +61,8 @@ public sealed class InfoPlistParserTests
Assert.Equal("TestApp", result.Executable);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_ExtractsSupportedPlatforms()
{
// Arrange
@@ -72,7 +77,8 @@ public sealed class InfoPlistParserTests
Assert.Contains("MacOSX", result.SupportedPlatforms);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_NonExistentFile_ReturnsNull()
{
// Arrange
@@ -85,7 +91,8 @@ public sealed class InfoPlistParserTests
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_MissingBundleIdentifier_ReturnsNull()
{
// Arrange - Create a temp file without CFBundleIdentifier

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Analyzers.OS.MacOsBundle;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests;
public sealed class MacOsBundleAnalyzerTests
@@ -29,13 +30,15 @@ public sealed class MacOsBundleAnalyzerTests
_logger);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AnalyzerId_ReturnsMacosBundleIdentifier()
{
Assert.Equal("macos-bundle", _analyzer.AnalyzerId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithValidBundles_ReturnsPackages()
{
// Arrange
@@ -50,7 +53,8 @@ public sealed class MacOsBundleAnalyzerTests
Assert.True(result.Packages.Count > 0, "Expected at least one bundle");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_FindsTestApp()
{
// Arrange
@@ -68,7 +72,8 @@ public sealed class MacOsBundleAnalyzerTests
Assert.Equal("Test Application", testApp.Name);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ExtractsVersionCorrectly()
{
// Arrange
@@ -88,7 +93,8 @@ public sealed class MacOsBundleAnalyzerTests
Assert.Equal("123", testApp.Release);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_BuildsCorrectPurl()
{
// Arrange
@@ -105,7 +111,8 @@ public sealed class MacOsBundleAnalyzerTests
Assert.Contains("pkg:generic/macos-app/com.stellaops.testapp@1.2.3", testApp.PackageUrl);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ExtractsVendorFromBundleId()
{
// Arrange
@@ -122,7 +129,8 @@ public sealed class MacOsBundleAnalyzerTests
Assert.Equal("stellaops", testApp.SourcePackage);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_SetsEvidenceSourceToMacOsBundle()
{
// Arrange
@@ -138,7 +146,8 @@ public sealed class MacOsBundleAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ExtractsVendorMetadata()
{
// Arrange
@@ -159,7 +168,8 @@ public sealed class MacOsBundleAnalyzerTests
Assert.Equal("MacOSX", testApp.VendorMetadata["macos:platforms"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_IncludesCodeResourcesHash()
{
// Arrange
@@ -178,7 +188,8 @@ public sealed class MacOsBundleAnalyzerTests
Assert.StartsWith("sha256:", hash);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_DetectsSandboxedApp()
{
// Arrange
@@ -195,7 +206,8 @@ public sealed class MacOsBundleAnalyzerTests
Assert.Equal("true", sandboxedApp.VendorMetadata["macos:sandboxed"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_DetectsHighRiskEntitlements()
{
// Arrange
@@ -216,7 +228,8 @@ public sealed class MacOsBundleAnalyzerTests
Assert.Contains("com.apple.security.device.microphone", highRisk);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_DetectsCapabilityCategories()
{
// Arrange
@@ -239,7 +252,8 @@ public sealed class MacOsBundleAnalyzerTests
Assert.Contains("sandbox", categories);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_IncludesFileEvidence()
{
// Arrange
@@ -264,7 +278,8 @@ public sealed class MacOsBundleAnalyzerTests
Assert.True(infoPlist.IsConfigFile);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ResultsAreDeterministicallySorted()
{
// Arrange
@@ -282,7 +297,8 @@ public sealed class MacOsBundleAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_NoApplicationsDirectory_ReturnsEmptyPackages()
{
// Arrange - use temp directory without Applications
@@ -305,7 +321,8 @@ public sealed class MacOsBundleAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_PopulatesTelemetry()
{
// Arrange

View File

@@ -20,6 +20,7 @@
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS/StellaOps.Scanner.Analyzers.OS.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS.MacOsBundle/StellaOps.Scanner.Analyzers.OS.MacOsBundle.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Analyzers.OS.Pkgutil;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.OS.Pkgutil.Tests;
public sealed class PkgutilPackageAnalyzerTests
@@ -29,13 +30,15 @@ public sealed class PkgutilPackageAnalyzerTests
_logger);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AnalyzerId_ReturnsPkgutil()
{
Assert.Equal("pkgutil", _analyzer.AnalyzerId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithValidReceipts_ReturnsPackages()
{
// Arrange
@@ -50,7 +53,8 @@ public sealed class PkgutilPackageAnalyzerTests
Assert.True(result.Packages.Count > 0, "Expected at least one package");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_FindsSafariPackage()
{
// Arrange
@@ -66,7 +70,8 @@ public sealed class PkgutilPackageAnalyzerTests
Assert.Contains("pkg:generic/apple/com.apple.pkg.Safari@17.1", safari.PackageUrl);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ExtractsVendorFromIdentifier()
{
// Arrange
@@ -81,7 +86,8 @@ public sealed class PkgutilPackageAnalyzerTests
Assert.Equal("apple", safari.SourcePackage);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_SetsEvidenceSourceToPkgutilReceipt()
{
// Arrange
@@ -97,7 +103,8 @@ public sealed class PkgutilPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ExtractsVendorMetadata()
{
// Arrange
@@ -113,7 +120,8 @@ public sealed class PkgutilPackageAnalyzerTests
Assert.Equal("/", safari.VendorMetadata["pkgutil:volume"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ResultsAreDeterministicallySorted()
{
// Arrange
@@ -131,7 +139,8 @@ public sealed class PkgutilPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_NoReceiptsDirectory_ReturnsEmptyPackages()
{
// Arrange - use temp directory without receipts
@@ -154,7 +163,8 @@ public sealed class PkgutilPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_PopulatesTelemetry()
{
// Arrange

View File

@@ -20,6 +20,7 @@
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS/StellaOps.Scanner.Analyzers.OS.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS.Pkgutil/StellaOps.Scanner.Analyzers.OS.Pkgutil.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />

View File

@@ -12,11 +12,14 @@ using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Analyzers.OS.Tests.TestUtilities;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.OS.Tests;
public sealed class OsAnalyzerDeterminismTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ApkAnalyzerMatchesGolden()
{
using var fixture = FixtureManager.UseFixture("apk", out var rootPath);
@@ -28,7 +31,8 @@ public sealed class OsAnalyzerDeterminismTests
GoldenAssert.MatchSnapshot(snapshot, FixtureManager.GetGoldenPath("apk.json"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DpkgAnalyzerMatchesGolden()
{
using var fixture = FixtureManager.UseFixture("dpkg", out var rootPath);
@@ -40,7 +44,8 @@ public sealed class OsAnalyzerDeterminismTests
GoldenAssert.MatchSnapshot(snapshot, FixtureManager.GetGoldenPath("dpkg.json"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RpmAnalyzerMatchesGolden()
{
var headers = new[]

View File

@@ -22,6 +22,7 @@
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS.Apk/StellaOps.Scanner.Analyzers.OS.Apk.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS.Dpkg/StellaOps.Scanner.Analyzers.OS.Dpkg.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS.Rpm/StellaOps.Scanner.Analyzers.OS.Rpm.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />

View File

@@ -4,11 +4,13 @@ using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests;
public class ChocolateyAnalyzerPluginTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Name_ReturnsCorrectPluginName()
{
// Arrange
@@ -21,7 +23,8 @@ public class ChocolateyAnalyzerPluginTests
Assert.Equal("StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey", name);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void IsAvailable_WithValidServiceProvider_ReturnsTrue()
{
// Arrange
@@ -37,7 +40,8 @@ public class ChocolateyAnalyzerPluginTests
Assert.True(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void IsAvailable_WithNullServiceProvider_ReturnsFalse()
{
// Arrange
@@ -50,7 +54,8 @@ public class ChocolateyAnalyzerPluginTests
Assert.False(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateAnalyzer_WithValidServiceProvider_ReturnsAnalyzer()
{
// Arrange
@@ -68,7 +73,8 @@ public class ChocolateyAnalyzerPluginTests
Assert.Equal("windows-chocolatey", analyzer.AnalyzerId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateAnalyzer_WithNullServiceProvider_ThrowsArgumentNullException()
{
// Arrange

View File

@@ -3,6 +3,8 @@ using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests;
public class ChocolateyPackageAnalyzerTests
@@ -25,13 +27,15 @@ public class ChocolateyPackageAnalyzerTests
_logger);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AnalyzerId_ReturnsCorrectValue()
{
Assert.Equal("windows-chocolatey", _analyzer.AnalyzerId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithNoChocolateyDirectory_ReturnsEmptyList()
{
// Arrange
@@ -55,7 +59,8 @@ public class ChocolateyPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithEmptyChocolateyLib_ReturnsEmptyList()
{
// Arrange
@@ -80,7 +85,8 @@ public class ChocolateyPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithNuspecFile_ReturnsPackageRecord()
{
// Arrange
@@ -113,7 +119,8 @@ public class ChocolateyPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithMultiplePackages_ReturnsAllRecords()
{
// Arrange
@@ -160,7 +167,8 @@ public class ChocolateyPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ExtractsVendorMetadata()
{
// Arrange
@@ -198,7 +206,8 @@ public class ChocolateyPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithInstallScript_ComputesHash()
{
// Arrange
@@ -231,7 +240,8 @@ public class ChocolateyPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_FallsBackToDirectoryParsing_WhenNoNuspec()
{
// Arrange
@@ -264,7 +274,8 @@ public class ChocolateyPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_IncludesFileEvidence()
{
// Arrange
@@ -307,7 +318,8 @@ public class ChocolateyPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ResultsAreSortedDeterministically()
{
// Arrange
@@ -346,7 +358,8 @@ public class ChocolateyPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_SkipsHiddenDirectories()
{
// Arrange
@@ -379,7 +392,8 @@ public class ChocolateyPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_HandlesLowerCaseChocolateyPath()
{
// Arrange
@@ -407,7 +421,8 @@ public class ChocolateyPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_TruncatesLongDescription()
{
// Arrange
@@ -440,7 +455,8 @@ public class ChocolateyPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithCancellation_ThrowsOperationCanceledException()
{
// Arrange

View File

@@ -1,13 +1,15 @@
using StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests;
public class NuspecParserTests
{
private readonly NuspecParser _parser = new();
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithValidNuspec_ReturnsMetadata()
{
// Arrange
@@ -49,7 +51,8 @@ public class NuspecParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithOldNamespace_ReturnsMetadata()
{
// Arrange
@@ -81,7 +84,8 @@ public class NuspecParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithOld2011Namespace_ReturnsMetadata()
{
// Arrange
@@ -113,7 +117,8 @@ public class NuspecParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithNoNamespace_ReturnsMetadata()
{
// Arrange
@@ -145,7 +150,8 @@ public class NuspecParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithMissingId_ReturnsNull()
{
// Arrange
@@ -174,7 +180,8 @@ public class NuspecParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithMissingVersion_ReturnsNull()
{
// Arrange
@@ -203,7 +210,8 @@ public class NuspecParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithInvalidXml_ReturnsNull()
{
// Arrange
@@ -227,7 +235,8 @@ public class NuspecParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithNonExistentFile_ReturnsNull()
{
// Act
@@ -237,7 +246,8 @@ public class NuspecParserTests
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithNullPath_ReturnsNull()
{
// Act
@@ -247,7 +257,8 @@ public class NuspecParserTests
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithEmptyPath_ReturnsNull()
{
// Act
@@ -257,7 +268,8 @@ public class NuspecParserTests
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithWhitespacePath_ReturnsNull()
{
// Act
@@ -267,7 +279,8 @@ public class NuspecParserTests
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_ComputesInstallScriptHash_FromToolsDirectory()
{
// Arrange
@@ -302,7 +315,8 @@ public class NuspecParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_ComputesInstallScriptHash_FromRootDirectory()
{
// Arrange
@@ -336,7 +350,8 @@ public class NuspecParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_EnumeratesInstalledFiles()
{
// Arrange
@@ -373,7 +388,8 @@ public class NuspecParserTests
}
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("git.2.42.0", "git", "2.42.0")]
[InlineData("nodejs.20.10.0", "nodejs", "20.10.0")]
[InlineData("7zip.23.01", "7zip", "23.01")]
@@ -391,7 +407,8 @@ public class NuspecParserTests
Assert.Equal(expectedVersion, result.Value.Version);
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]

View File

@@ -20,6 +20,7 @@
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS/StellaOps.Scanner.Analyzers.OS.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey/StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />

View File

@@ -1,12 +1,14 @@
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests;
public class MsiDatabaseParserTests
{
private readonly MsiDatabaseParser _parser = new();
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithValidMsiFile_ExtractsMetadata()
{
// Arrange - Create a minimal valid OLE compound document
@@ -34,7 +36,8 @@ public class MsiDatabaseParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithVersionedFilename_ExtractsVersionFromName()
{
// Arrange
@@ -59,7 +62,8 @@ public class MsiDatabaseParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithSpaceVersionedFilename_ExtractsVersionFromName()
{
// Arrange
@@ -84,7 +88,8 @@ public class MsiDatabaseParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithUnversionedFilename_UsesDefaultVersion()
{
// Arrange
@@ -109,7 +114,8 @@ public class MsiDatabaseParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithNonExistentFile_ReturnsNull()
{
// Act
@@ -119,7 +125,8 @@ public class MsiDatabaseParserTests
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithInvalidMsiFile_ReturnsNull()
{
// Arrange - Create a file with invalid content
@@ -140,7 +147,8 @@ public class MsiDatabaseParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithEmptyPath_ReturnsNull()
{
// Act
@@ -150,7 +158,8 @@ public class MsiDatabaseParserTests
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithNullPath_ReturnsNull()
{
// Act
@@ -160,7 +169,8 @@ public class MsiDatabaseParserTests
Assert.Null(result);
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("Product-1.0.msi", "Product", "1.0")]
[InlineData("Product-1.0.0.msi", "Product", "1.0.0")]
[InlineData("Product-1.0.0.1.msi", "Product", "1.0.0.1")]

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Analyzers.OS.Windows.Msi;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests;
public class MsiPackageAnalyzerTests
@@ -25,13 +26,15 @@ public class MsiPackageAnalyzerTests
_logger);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AnalyzerId_ReturnsCorrectValue()
{
Assert.Equal("windows-msi", _analyzer.AnalyzerId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithNoMsiDirectory_ReturnsEmptyList()
{
// Arrange
@@ -55,7 +58,8 @@ public class MsiPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithMsiFiles_ReturnsPackageRecords()
{
// Arrange
@@ -94,7 +98,8 @@ public class MsiPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithNestedMsiFiles_DiscoversMsisRecursively()
{
// Arrange
@@ -122,7 +127,8 @@ public class MsiPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithUserAppDataCache_ScansMsisInUserDirectories()
{
// Arrange
@@ -149,7 +155,8 @@ public class MsiPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithInvalidMsiFile_SkipsInvalidFile()
{
// Arrange
@@ -180,7 +187,8 @@ public class MsiPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ResultsAreSortedDeterministically()
{
// Arrange
@@ -212,7 +220,8 @@ public class MsiPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithDuplicateMsiFiles_DeduplicatesByPath()
{
// Arrange
@@ -241,7 +250,8 @@ public class MsiPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_SetsCorrectFileEvidence()
{
// Arrange

View File

@@ -20,6 +20,7 @@
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS/StellaOps.Scanner.Analyzers.OS.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS.Windows.Msi/StellaOps.Scanner.Analyzers.OS.Windows.Msi.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />

View File

@@ -20,6 +20,7 @@
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS/StellaOps.Scanner.Analyzers.OS.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS.Windows.WinSxS/StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />

View File

@@ -1,12 +1,14 @@
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests;
public class WinSxSManifestParserTests
{
private readonly WinSxSManifestParser _parser = new();
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithValidManifest_ExtractsMetadata()
{
// Arrange
@@ -44,7 +46,8 @@ public class WinSxSManifestParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithAmd64Architecture_ExtractsCorrectly()
{
// Arrange
@@ -74,7 +77,8 @@ public class WinSxSManifestParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithMultipleFiles_ExtractsAllFiles()
{
// Arrange
@@ -104,7 +108,8 @@ public class WinSxSManifestParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithKbReferenceInFilename_ExtractsKbReference()
{
// Arrange
@@ -130,7 +135,8 @@ public class WinSxSManifestParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithNonExistentFile_ReturnsNull()
{
// Act
@@ -140,7 +146,8 @@ public class WinSxSManifestParserTests
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithInvalidXml_ReturnsNull()
{
// Arrange
@@ -160,7 +167,8 @@ public class WinSxSManifestParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithMissingAssemblyIdentity_ReturnsNull()
{
// Arrange
@@ -183,7 +191,8 @@ public class WinSxSManifestParserTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_WithEmptyPath_ReturnsNull()
{
// Act
@@ -193,7 +202,8 @@ public class WinSxSManifestParserTests
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BuildAssemblyIdentityString_BuildsCorrectFormat()
{
// Arrange
@@ -218,7 +228,8 @@ public class WinSxSManifestParserTests
Assert.Equal("microsoft.windows.common-controls_6.0.0.0_x86_6595b64144ccf1df_en-us", result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BuildAssemblyIdentityString_WithNeutralLanguage_OmitsLanguage()
{
// Arrange

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Analyzers.OS.Windows.WinSxS;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests;
public class WinSxSPackageAnalyzerTests
@@ -25,13 +26,15 @@ public class WinSxSPackageAnalyzerTests
_logger);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AnalyzerId_ReturnsCorrectValue()
{
Assert.Equal("windows-winsxs", _analyzer.AnalyzerId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithNoWinSxSDirectory_ReturnsEmptyList()
{
// Arrange
@@ -55,7 +58,8 @@ public class WinSxSPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithManifestFiles_ReturnsPackageRecords()
{
// Arrange
@@ -97,7 +101,8 @@ public class WinSxSPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ExtractsVendorMetadata()
{
// Arrange
@@ -132,7 +137,8 @@ public class WinSxSPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ExtractsPublisherFromAssemblyName()
{
// Arrange
@@ -160,7 +166,8 @@ public class WinSxSPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_IncludesFileEvidence()
{
// Arrange
@@ -201,7 +208,8 @@ public class WinSxSPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_WithInvalidManifest_SkipsAndContinues()
{
// Arrange
@@ -233,7 +241,8 @@ public class WinSxSPackageAnalyzerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnalyzeAsync_ResultsAreSortedDeterministically()
{
// Arrange

View File

@@ -9,6 +9,8 @@ using StellaOps.Scanner.Cache.FileCas;
using StellaOps.Scanner.Cache.LayerCache;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Cache.Tests;
public sealed class LayerCacheRoundTripTests : IAsyncLifetime
@@ -42,7 +44,8 @@ public sealed class LayerCacheRoundTripTests : IAsyncLifetime
_fileCas = new FileContentAddressableStore(_options, NullLogger<FileContentAddressableStore>.Instance, _timeProvider);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RoundTrip_Succeeds_And_Respects_Ttl_And_ImportExport()
{
var layerDigest = "sha256:abcd1234";

View File

@@ -16,6 +16,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Remove="..\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<Using Remove="StellaOps.Concelier.Testing" />

View File

@@ -2,11 +2,13 @@ using StellaOps.Scanner.CallGraph;
using StellaOps.Scanner.CallGraph.Node;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.CallGraph.Tests;
public class BenchmarkIntegrationTests
{
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("unsafe-eval", true)]
[InlineData("guarded-eval", false)]
public async Task NodeTraceExtractor_AlignsWithBenchmarkReachability(string caseName, bool expectSinkReachable)

View File

@@ -8,11 +8,13 @@ using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.CallGraph.Binary;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.CallGraph.Tests;
public class BinaryCallGraphExtractorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BinaryEntrypointClassifier_ClassifiesMainFunction()
{
// Arrange
@@ -34,7 +36,8 @@ public class BinaryCallGraphExtractorTests
Assert.Equal(EntrypointType.CliCommand, result.Value);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BinaryEntrypointClassifier_ClassifiesInitArray()
{
// Arrange
@@ -56,7 +59,8 @@ public class BinaryCallGraphExtractorTests
Assert.Equal(EntrypointType.InitFunction, result.Value);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BinaryEntrypointClassifier_ReturnsNullForInternalFunction()
{
// Arrange
@@ -77,7 +81,8 @@ public class BinaryCallGraphExtractorTests
Assert.False(result.HasValue);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DwarfDebugReader_HandlesNonExistentFile()
{
// Arrange
@@ -89,7 +94,8 @@ public class BinaryCallGraphExtractorTests
await reader.ReadAsync("/nonexistent/binary", default));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BinaryRelocation_HasCorrectProperties()
{
// Arrange & Act
@@ -110,7 +116,8 @@ public class BinaryCallGraphExtractorTests
Assert.True(relocation.IsExternal);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DwarfFunction_RecordsCorrectInfo()
{
// Arrange & Act
@@ -135,7 +142,8 @@ public class BinaryCallGraphExtractorTests
Assert.True(func.IsExternal);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BinarySymbol_TracksVisibility()
{
// Arrange & Act

View File

@@ -2,11 +2,13 @@ using StellaOps.Scanner.CallGraph.Binary;
using StellaOps.Scanner.CallGraph.Binary.Disassembly;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.CallGraph.Tests;
public class BinaryDisassemblyTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void X86Disassembler_Extracts_Call_And_Jmp()
{
var disassembler = new X86Disassembler();
@@ -26,7 +28,8 @@ public class BinaryDisassemblyTests
Assert.Equal(0x100CUL, calls[1].TargetAddress);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DirectCallExtractor_Maps_Targets_To_Symbols()
{
var extractor = new DirectCallExtractor();

View File

@@ -5,11 +5,13 @@ using StellaOps.Scanner.CallGraph.Binary.Disassembly;
using StellaOps.Scanner.CallGraph.Binary.Analysis;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.CallGraph.Tests;
public class BinaryTextSectionReaderTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ReadsElfTextSection()
{
var textBytes = new byte[] { 0x90, 0x90, 0xC3, 0x90 };
@@ -32,7 +34,8 @@ public class BinaryTextSectionReaderTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ReadsPeTextSection()
{
var textBytes = new byte[] { 0x90, 0x90, 0xC3, 0x90 };
@@ -54,7 +57,8 @@ public class BinaryTextSectionReaderTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ReadsMachOTextSection()
{
var textBytes = new byte[] { 0x1F, 0x20, 0x03, 0xD5 };
@@ -76,7 +80,8 @@ public class BinaryTextSectionReaderTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task StringScannerExtractsLibraryCandidates()
{
var textBytes = new byte[] { 0x90, 0x90, 0xC3, 0x90 };

View File

@@ -1,11 +1,13 @@
using StellaOps.Scanner.CallGraph.Caching;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.CallGraph.Tests;
public class CircuitBreakerStateTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RecordFailure_TripsOpen_AfterThreshold()
{
var config = new CircuitBreakerConfig
@@ -26,7 +28,8 @@ public class CircuitBreakerStateTests
Assert.True(cb.IsOpen);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RecordSuccess_ResetsToClosed()
{
var config = new CircuitBreakerConfig { FailureThreshold = 1, TimeoutSeconds = 60, HalfOpenTimeout = 10 };

View File

@@ -2,11 +2,14 @@ using StellaOps.Scanner.CallGraph;
using StellaOps.Scanner.CallGraph.DotNet;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.CallGraph.Tests;
public class DotNetCallGraphExtractorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractAsync_SimpleProject_ProducesEntrypointAndSink()
{
await using var temp = await TempDirectory.CreateAsync();
@@ -71,7 +74,8 @@ public class DotNetCallGraphExtractorTests
Assert.NotEmpty(snapshot.EntrypointIds);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractAsync_IsDeterministic_ForSameInputs()
{
await using var temp = await TempDirectory.CreateAsync();

View File

@@ -9,11 +9,13 @@ using StellaOps.Scanner.CallGraph.Go;
using StellaOps.Scanner.Reachability;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.CallGraph.Tests;
public class GoCallGraphExtractorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BuildFunctionId_CreatesCorrectFormat()
{
// Arrange & Act
@@ -23,7 +25,8 @@ public class GoCallGraphExtractorTests
Assert.Equal("go:github.com/example/pkg.HandleRequest", id);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BuildMethodId_CreatesCorrectFormat()
{
// Arrange & Act
@@ -33,7 +36,8 @@ public class GoCallGraphExtractorTests
Assert.Equal("go:github.com/example/pkg.Server.Start", id);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BuildExternalId_CreatesCorrectFormat()
{
// Arrange & Act
@@ -43,7 +47,8 @@ public class GoCallGraphExtractorTests
Assert.Equal("go:external/fmt.Println", id);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_ParsesFunctionId()
{
// Arrange
@@ -61,7 +66,8 @@ public class GoCallGraphExtractorTests
Assert.False(result.IsExternal);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_ParsesExternalId()
{
// Arrange
@@ -77,7 +83,8 @@ public class GoCallGraphExtractorTests
Assert.True(result.IsExternal);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void IsStdLib_ReturnsTrueForStandardLibrary()
{
// Arrange & Act & Assert
@@ -86,7 +93,8 @@ public class GoCallGraphExtractorTests
Assert.False(GoSymbolIdBuilder.IsStdLib("go:external/github.com/gin-gonic/gin.New"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GoSsaResultParser_ParsesValidJson()
{
// Arrange
@@ -120,7 +128,8 @@ public class GoCallGraphExtractorTests
Assert.Single(result.Entrypoints);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GoSsaResultParser_ThrowsOnEmptyInput()
{
// Arrange & Act & Assert
@@ -128,7 +137,8 @@ public class GoCallGraphExtractorTests
Assert.Throws<ArgumentException>(() => GoSsaResultParser.Parse(" "));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GoEntrypointClassifier_ClassifiesHttpHandler()
{
// Arrange
@@ -164,7 +174,8 @@ public class GoCallGraphExtractorTests
Assert.Equal(EntrypointType.HttpHandler, result.Value);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GoSinkMatcher_MatchesExecCommand()
{
// Arrange
@@ -177,7 +188,8 @@ public class GoCallGraphExtractorTests
Assert.Equal(SinkCategory.CmdExec, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GoSinkMatcher_MatchesSqlQuery()
{
// Arrange

View File

@@ -10,6 +10,8 @@ using StellaOps.Scanner.Reachability;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.CallGraph.Tests;
/// <summary>
@@ -31,7 +33,8 @@ public class JavaCallGraphExtractorTests
#region JavaEntrypointClassifier Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaEntrypointClassifier_SpringRequestMapping_DetectedAsHttpHandler()
{
var classifier = new JavaEntrypointClassifier();
@@ -61,7 +64,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(EntrypointType.HttpHandler, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaEntrypointClassifier_SpringRestController_PublicMethodDetectedAsHttpHandler()
{
var classifier = new JavaEntrypointClassifier();
@@ -91,7 +95,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(EntrypointType.HttpHandler, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaEntrypointClassifier_JaxRsPath_DetectedAsHttpHandler()
{
var classifier = new JavaEntrypointClassifier();
@@ -121,7 +126,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(EntrypointType.HttpHandler, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaEntrypointClassifier_SpringScheduled_DetectedAsScheduledJob()
{
var classifier = new JavaEntrypointClassifier();
@@ -151,7 +157,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(EntrypointType.ScheduledJob, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaEntrypointClassifier_KafkaListener_DetectedAsMessageHandler()
{
var classifier = new JavaEntrypointClassifier();
@@ -181,7 +188,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(EntrypointType.MessageHandler, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaEntrypointClassifier_GrpcService_DetectedAsGrpcMethod()
{
var classifier = new JavaEntrypointClassifier();
@@ -212,7 +220,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(EntrypointType.GrpcMethod, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaEntrypointClassifier_MainMethod_DetectedAsCliCommand()
{
var classifier = new JavaEntrypointClassifier();
@@ -242,7 +251,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(EntrypointType.CliCommand, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaEntrypointClassifier_PrivateMethod_NotDetectedAsEntrypoint()
{
var classifier = new JavaEntrypointClassifier();
@@ -276,7 +286,8 @@ public class JavaCallGraphExtractorTests
#region JavaSinkMatcher Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaSinkMatcher_RuntimeExec_DetectedAsCmdExec()
{
var matcher = new JavaSinkMatcher();
@@ -286,7 +297,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(SinkCategory.CmdExec, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaSinkMatcher_ProcessBuilderInit_DetectedAsCmdExec()
{
var matcher = new JavaSinkMatcher();
@@ -296,7 +308,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(SinkCategory.CmdExec, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaSinkMatcher_StatementExecute_DetectedAsSqlRaw()
{
var matcher = new JavaSinkMatcher();
@@ -306,7 +319,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(SinkCategory.SqlRaw, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaSinkMatcher_ObjectInputStream_DetectedAsUnsafeDeser()
{
var matcher = new JavaSinkMatcher();
@@ -316,7 +330,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(SinkCategory.UnsafeDeser, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaSinkMatcher_HttpClientExecute_DetectedAsSsrf()
{
var matcher = new JavaSinkMatcher();
@@ -326,7 +341,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(SinkCategory.Ssrf, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaSinkMatcher_FileWriter_DetectedAsPathTraversal()
{
var matcher = new JavaSinkMatcher();
@@ -336,7 +352,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(SinkCategory.PathTraversal, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaSinkMatcher_UnknownMethod_ReturnsNull()
{
var matcher = new JavaSinkMatcher();
@@ -346,7 +363,8 @@ public class JavaCallGraphExtractorTests
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaSinkMatcher_XxeVulnerableParsing_DetectedAsXxe()
{
var matcher = new JavaSinkMatcher();
@@ -356,7 +374,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(SinkCategory.XxeInjection, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaSinkMatcher_ScriptEngineEval_DetectedAsCodeInjection()
{
var matcher = new JavaSinkMatcher();
@@ -370,7 +389,8 @@ public class JavaCallGraphExtractorTests
#region JavaBytecodeAnalyzer Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaBytecodeAnalyzer_ValidClassHeader_Parsed()
{
var analyzer = new JavaBytecodeAnalyzer(NullLogger<JavaCallGraphExtractor>.Instance);
@@ -394,7 +414,8 @@ public class JavaCallGraphExtractorTests
// The important thing is it doesn't throw
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaBytecodeAnalyzer_InvalidMagic_ReturnsNull()
{
var analyzer = new JavaBytecodeAnalyzer(NullLogger<JavaCallGraphExtractor>.Instance);
@@ -406,7 +427,8 @@ public class JavaCallGraphExtractorTests
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaBytecodeAnalyzer_EmptyArray_ReturnsNull()
{
var analyzer = new JavaBytecodeAnalyzer(NullLogger<JavaCallGraphExtractor>.Instance);
@@ -420,7 +442,8 @@ public class JavaCallGraphExtractorTests
#region Integration Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractAsync_InvalidPath_ThrowsFileNotFound()
{
var request = new CallGraphExtractionRequest(
@@ -432,7 +455,8 @@ public class JavaCallGraphExtractorTests
() => _extractor.ExtractAsync(request));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractAsync_WrongLanguage_ThrowsArgumentException()
{
await using var temp = await TempDirectory.CreateAsync();
@@ -446,7 +470,8 @@ public class JavaCallGraphExtractorTests
() => _extractor.ExtractAsync(request));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractAsync_EmptyDirectory_ProducesEmptySnapshot()
{
await using var temp = await TempDirectory.CreateAsync();
@@ -468,7 +493,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(_fixedTime, snapshot.ExtractedAt);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extractor_Language_IsJava()
{
Assert.Equal("java", _extractor.Language);
@@ -478,7 +504,8 @@ public class JavaCallGraphExtractorTests
#region Determinism Verification Tests (JCG-020)
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractAsync_SamePath_ProducesSameDigest()
{
// Arrange: Create a temp directory
@@ -497,7 +524,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(snapshot1.GraphDigest, snapshot2.GraphDigest);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractAsync_DifferentScanId_SameNodesAndEdges()
{
// Arrange: Create a temp directory
@@ -523,7 +551,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(snapshot1.GraphDigest, snapshot2.GraphDigest);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BuildNodeId_SameInputs_ProducesIdenticalIds()
{
// Act: Build node IDs multiple times with same inputs
@@ -534,7 +563,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(id1, id2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BuildNodeId_DifferentDescriptors_ProducesDifferentIds()
{
// Act: Build node IDs with different descriptors (overloaded methods)
@@ -545,7 +575,8 @@ public class JavaCallGraphExtractorTests
Assert.NotEqual(id1, id2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaEntrypointClassifier_SameInput_AlwaysSameResult()
{
var classifier = new JavaEntrypointClassifier();
@@ -581,7 +612,8 @@ public class JavaCallGraphExtractorTests
Assert.Equal(result2, result3);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JavaSinkMatcher_SameInput_AlwaysSameResult()
{
var matcher = new JavaSinkMatcher();

View File

@@ -12,6 +12,8 @@ using StellaOps.Scanner.CallGraph.JavaScript;
using StellaOps.Scanner.Reachability;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.CallGraph.Tests;
/// <summary>
@@ -35,7 +37,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
#region Entrypoint Classifier Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsEntrypointClassifier_ExpressHandler_ReturnsHttpHandler()
{
var classifier = new JsEntrypointClassifier();
@@ -56,7 +59,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(EntrypointType.HttpHandler, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsEntrypointClassifier_FastifyRoute_ReturnsHttpHandler()
{
var classifier = new JsEntrypointClassifier();
@@ -79,7 +83,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(EntrypointType.HttpHandler, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsEntrypointClassifier_LambdaHandler_ReturnsLambda()
{
var classifier = new JsEntrypointClassifier();
@@ -99,7 +104,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(EntrypointType.Lambda, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsEntrypointClassifier_AzureFunction_WithHandler_ReturnsLambda()
{
var classifier = new JsEntrypointClassifier();
@@ -119,7 +125,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(EntrypointType.Lambda, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsEntrypointClassifier_CliWithRunName_ReturnsCliCommand()
{
var classifier = new JsEntrypointClassifier();
@@ -139,7 +146,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(EntrypointType.CliCommand, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsEntrypointClassifier_UnknownSocket_ReturnsNull()
{
var classifier = new JsEntrypointClassifier();
@@ -160,7 +168,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsEntrypointClassifier_NestHandler_ReturnsMessageHandler()
{
var classifier = new JsEntrypointClassifier();
@@ -181,7 +190,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(EntrypointType.MessageHandler, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsEntrypointClassifier_UnknownCron_ReturnsNull()
{
var classifier = new JsEntrypointClassifier();
@@ -202,7 +212,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Null(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsEntrypointClassifier_GraphQL_ReturnsHttpHandler()
{
var classifier = new JsEntrypointClassifier();
@@ -223,7 +234,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(EntrypointType.HttpHandler, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsEntrypointClassifier_NoMatch_ReturnsNull()
{
var classifier = new JsEntrypointClassifier();
@@ -247,7 +259,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
#region Sink Matcher Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_ChildProcessExec_ReturnsCmdExec()
{
var matcher = new JsSinkMatcher();
@@ -257,7 +270,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(SinkCategory.CmdExec, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_ChildProcessSpawn_ReturnsCmdExec()
{
var matcher = new JsSinkMatcher();
@@ -267,7 +281,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(SinkCategory.CmdExec, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_ChildProcessFork_ReturnsCmdExec()
{
var matcher = new JsSinkMatcher();
@@ -277,7 +292,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(SinkCategory.CmdExec, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_MysqlQuery_ReturnsSqlRaw()
{
var matcher = new JsSinkMatcher();
@@ -287,7 +303,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(SinkCategory.SqlRaw, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_PgQuery_ReturnsSqlRaw()
{
var matcher = new JsSinkMatcher();
@@ -297,7 +314,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(SinkCategory.SqlRaw, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_KnexRaw_ReturnsSqlRaw()
{
var matcher = new JsSinkMatcher();
@@ -307,7 +325,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(SinkCategory.SqlRaw, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_FsReadFile_ReturnsPathTraversal()
{
var matcher = new JsSinkMatcher();
@@ -317,7 +336,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(SinkCategory.PathTraversal, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_FsWriteFile_ReturnsPathTraversal()
{
var matcher = new JsSinkMatcher();
@@ -327,7 +347,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(SinkCategory.PathTraversal, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_AxiosGet_ReturnsSsrf()
{
var matcher = new JsSinkMatcher();
@@ -337,7 +358,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(SinkCategory.Ssrf, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_HttpRequest_ReturnsSsrf()
{
var matcher = new JsSinkMatcher();
@@ -347,7 +369,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(SinkCategory.Ssrf, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_JsYamlLoad_ReturnsUnsafeDeser()
{
var matcher = new JsSinkMatcher();
@@ -357,7 +380,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(SinkCategory.UnsafeDeser, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_Eval_ReturnsCodeInjection()
{
var matcher = new JsSinkMatcher();
@@ -367,7 +391,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(SinkCategory.CodeInjection, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_VmRunInContext_ReturnsCodeInjection()
{
var matcher = new JsSinkMatcher();
@@ -377,7 +402,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(SinkCategory.CodeInjection, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_EjsRender_ReturnsTemplateInjection()
{
var matcher = new JsSinkMatcher();
@@ -387,7 +413,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(SinkCategory.TemplateInjection, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_NoMatch_ReturnsNull()
{
var matcher = new JsSinkMatcher();
@@ -401,7 +428,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
#region Extractor Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extractor_Language_IsJavascript()
{
Assert.Equal("javascript", _extractor.Language);
@@ -475,7 +503,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(snapshot1.GraphDigest, snapshot2.GraphDigest);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsEntrypointClassifier_SameInput_AlwaysSameResult()
{
var classifier = new JsEntrypointClassifier();
@@ -499,7 +528,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
Assert.Equal(result2, result3);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsSinkMatcher_SameInput_AlwaysSameResult()
{
var matcher = new JsSinkMatcher();

View File

@@ -7,11 +7,13 @@
using StellaOps.Scanner.CallGraph.Node;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.CallGraph.Tests;
public class NodeCallGraphExtractorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BabelResultParser_ParsesValidJson()
{
// Arrange
@@ -56,7 +58,8 @@ public class NodeCallGraphExtractorTests
Assert.Single(result.Entrypoints);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BabelResultParser_ParsesNodeWithPosition()
{
// Arrange
@@ -92,7 +95,8 @@ public class NodeCallGraphExtractorTests
Assert.Equal(5, node.Position.Column);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BabelResultParser_ParsesEdgeWithSite()
{
// Arrange
@@ -127,7 +131,8 @@ public class NodeCallGraphExtractorTests
Assert.Equal(25, edge.Site.Line);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BabelResultParser_ThrowsOnEmptyInput()
{
// Arrange & Act & Assert
@@ -135,7 +140,8 @@ public class NodeCallGraphExtractorTests
Assert.Throws<ArgumentNullException>(() => BabelResultParser.Parse(null!));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BabelResultParser_ParsesNdjson()
{
// Arrange
@@ -152,7 +158,8 @@ public class NodeCallGraphExtractorTests
Assert.Equal("app", result.Module);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void JsEntrypointInfo_HasCorrectProperties()
{
// Arrange
@@ -184,7 +191,8 @@ public class NodeCallGraphExtractorTests
Assert.Equal("GET", ep.Method);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BabelResultParser_ParsesSinks()
{
// Arrange
@@ -229,7 +237,8 @@ public class NodeCallGraphExtractorTests
Assert.Equal(42, sink.Site.Line);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BabelResultParser_ParsesMultipleSinkCategories()
{
// Arrange
@@ -269,7 +278,8 @@ public class NodeCallGraphExtractorTests
Assert.Contains(result.Sinks, s => s.Category == "file_write");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BabelResultParser_ParsesEmptySinks()
{
// Arrange
@@ -290,7 +300,8 @@ public class NodeCallGraphExtractorTests
Assert.Empty(result.Sinks);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BabelResultParser_ParsesMissingSinks()
{
// Arrange - sinks field omitted entirely
@@ -310,7 +321,8 @@ public class NodeCallGraphExtractorTests
Assert.Empty(result.Sinks);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BabelResultParser_ParsesSinkWithoutSite()
{
// Arrange

View File

@@ -9,11 +9,13 @@ using StellaOps.Scanner.CallGraph.Python;
using StellaOps.Scanner.Reachability;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.CallGraph.Tests;
public class PythonCallGraphExtractorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PythonEntrypointClassifier_ClassifiesFlaskRoute()
{
// Arrange
@@ -40,7 +42,8 @@ public class PythonCallGraphExtractorTests
Assert.Equal(EntrypointType.HttpHandler, result.Value);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PythonEntrypointClassifier_ClassifiesFastApiRoute()
{
// Arrange
@@ -67,7 +70,8 @@ public class PythonCallGraphExtractorTests
Assert.Equal(EntrypointType.HttpHandler, result.Value);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PythonEntrypointClassifier_ClassifiesCeleryTask()
{
// Arrange
@@ -94,7 +98,8 @@ public class PythonCallGraphExtractorTests
Assert.Equal(EntrypointType.BackgroundJob, result.Value);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PythonEntrypointClassifier_ClassifiesClickCommand()
{
// Arrange
@@ -121,7 +126,8 @@ public class PythonCallGraphExtractorTests
Assert.Equal(EntrypointType.CliCommand, result.Value);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PythonSinkMatcher_MatchesSubprocessCall()
{
// Arrange
@@ -134,7 +140,8 @@ public class PythonCallGraphExtractorTests
Assert.Equal(SinkCategory.CmdExec, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PythonSinkMatcher_MatchesEval()
{
// Arrange
@@ -147,7 +154,8 @@ public class PythonCallGraphExtractorTests
Assert.Equal(SinkCategory.CodeInjection, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PythonSinkMatcher_MatchesPickleLoads()
{
// Arrange
@@ -160,7 +168,8 @@ public class PythonCallGraphExtractorTests
Assert.Equal(SinkCategory.UnsafeDeser, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PythonSinkMatcher_MatchesSqlAlchemyExecute()
{
// Arrange
@@ -173,7 +182,8 @@ public class PythonCallGraphExtractorTests
Assert.Equal(SinkCategory.SqlRaw, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PythonSinkMatcher_ReturnsNullForSafeFunction()
{
// Arrange

View File

@@ -2,6 +2,7 @@ using System.Collections.Immutable;
using StellaOps.Scanner.CallGraph;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.CallGraph.Tests;
/// <summary>
@@ -10,7 +11,8 @@ namespace StellaOps.Scanner.CallGraph.Tests;
/// </summary>
public class ReachabilityAnalyzerTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Analyze_WhenSinkReachable_ReturnsShortestPath()
{
var entry = CallGraphNodeIds.Compute("dotnet:test:entry");
@@ -46,7 +48,8 @@ public class ReachabilityAnalyzerTests
Assert.Equal(new[] { entry, mid, sink }, result.Paths[0].NodeIds);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Analyze_WhenNoEntrypoints_ReturnsEmpty()
{
var snapshot = new CallGraphSnapshot(
@@ -71,7 +74,8 @@ public class ReachabilityAnalyzerTests
/// <summary>
/// WIT-007A: Verify deterministic path ordering (SinkId ASC, EntrypointId ASC, PathLength ASC).
/// </summary>
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Analyze_PathsAreDeterministicallyOrdered_BySinkIdThenEntrypointIdThenLength()
{
// Arrange: create graph with multiple entrypoints and sinks
@@ -121,7 +125,8 @@ public class ReachabilityAnalyzerTests
/// <summary>
/// WIT-007A: Verify that multiple runs produce identical results (determinism).
/// </summary>
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Analyze_ProducesIdenticalResults_OnMultipleRuns()
{
var entry = "entry:test";
@@ -163,7 +168,8 @@ public class ReachabilityAnalyzerTests
/// <summary>
/// WIT-007A: Verify MaxTotalPaths limit is enforced.
/// </summary>
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Analyze_WithOptions_RespectsMaxTotalPathsLimit()
{
// Arrange: create graph with 5 sinks reachable from 1 entrypoint
@@ -206,7 +212,8 @@ public class ReachabilityAnalyzerTests
/// <summary>
/// WIT-007A: Verify MaxDepth limit is enforced.
/// </summary>
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Analyze_WithOptions_RespectsMaxDepthLimit()
{
// Arrange: create a chain of 10 nodes
@@ -250,7 +257,8 @@ public class ReachabilityAnalyzerTests
/// <summary>
/// WIT-007A: Verify node IDs in paths are ordered from entrypoint to sink.
/// </summary>
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Analyze_PathNodeIds_AreOrderedFromEntrypointToSink()
{
var entry = "entry:start";
@@ -297,7 +305,8 @@ public class ReachabilityAnalyzerTests
/// <summary>
/// WIT-007B: Verify ExplicitSinks option allows targeting specific sinks not in snapshot.SinkIds.
/// </summary>
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Analyze_WithExplicitSinks_FindsPathsToSpecifiedSinksOnly()
{
// Arrange: graph with 3 reachable nodes, only 1 is in snapshot.SinkIds
@@ -347,7 +356,8 @@ public class ReachabilityAnalyzerTests
/// <summary>
/// WIT-007B: Verify ExplicitSinks with empty array falls back to snapshot sinks.
/// </summary>
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Analyze_WithEmptyExplicitSinks_UsesSnapshotSinks()
{
var entry = "entry:start";

View File

@@ -17,6 +17,7 @@
<ItemGroup>
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Scanner.CallGraph\\StellaOps.Scanner.CallGraph.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -6,6 +6,7 @@ using StellaOps.Scanner.CallGraph;
using StellaOps.Scanner.CallGraph.Caching;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.CallGraph.Tests;
public class ValkeyCallGraphCacheServiceTests : IAsyncLifetime
@@ -76,7 +77,8 @@ public class ValkeyCallGraphCacheServiceTests : IAsyncLifetime
await _cache.DisposeAsync();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SetThenGet_CallGraph_RoundTrips()
{
var nodeId = CallGraphNodeIds.Compute("dotnet:test:entry");
@@ -99,7 +101,8 @@ public class ValkeyCallGraphCacheServiceTests : IAsyncLifetime
Assert.Equal(snapshot.GraphDigest, loaded.GraphDigest);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SetThenGet_ReachabilityResult_RoundTrips()
{
var result = new ReachabilityAnalysisResult(

View File

@@ -2,11 +2,14 @@ using System.Threading.Tasks;
using StellaOps.Scanner.Reachability;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Core.Tests;
public class ReachabilityGraphBuilderUnionTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ConvertsBuilderToUnionGraphAndWritesNdjson()
{
var builder = new ReachabilityGraphBuilder()

View File

@@ -4,11 +4,14 @@ using StellaOps.Scanner.Core.Tests.Fakes;
using StellaOps.Scanner.Reachability;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Core.Tests;
public class ReachabilityUnionPublisherTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PublishesZipToCas()
{
var graph = new ReachabilityUnionGraph(

View File

@@ -11,7 +11,8 @@ namespace StellaOps.Scanner.Core.Tests;
public class ReachabilityUnionWriterTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task WritesDeterministicFilesAndHashes()
{
var writer = new ReachabilityUnionWriter();
@@ -62,6 +63,7 @@ public class ReachabilityUnionWriterTests
.EnumerateArray()
.Select(file => (Path: file.GetProperty("path").GetString(), Sha256: file.GetProperty("sha256").GetString()))
.ToList();
using StellaOps.TestKit;
}
Assert.Contains(files, file => file.Path == result.Nodes.Path && file.Sha256 == result.Nodes.Sha256);

View File

@@ -1,11 +1,13 @@
using System.Text.Json;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Core.Tests;
public class ScanManifestTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHash_SameManifest_ProducesSameHash()
{
var manifest1 = CreateSampleManifest();
@@ -18,7 +20,8 @@ public class ScanManifestTests
Assert.StartsWith("sha256:", hash1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHash_DifferentSeed_ProducesDifferentHash()
{
var seed1 = new byte[32];
@@ -32,7 +35,8 @@ public class ScanManifestTests
Assert.NotEqual(manifest1.ComputeHash(), manifest2.ComputeHash());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHash_DifferentArtifactDigest_ProducesDifferentHash()
{
var manifest1 = CreateSampleManifest(artifactDigest: "sha256:abc123");
@@ -41,7 +45,8 @@ public class ScanManifestTests
Assert.NotEqual(manifest1.ComputeHash(), manifest2.ComputeHash());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHash_HashIsLowercaseHex()
{
var manifest = CreateSampleManifest();
@@ -52,7 +57,8 @@ public class ScanManifestTests
Assert.Matches(@"^[0-9a-f]{64}$", hexPart);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Serialization_RoundTrip_PreservesAllFields()
{
var manifest = CreateSampleManifest();
@@ -71,7 +77,8 @@ public class ScanManifestTests
Assert.Equal(manifest.Seed, deserialized.Seed);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Serialization_JsonPropertyNames_AreCamelCase()
{
var manifest = CreateSampleManifest();
@@ -84,7 +91,8 @@ public class ScanManifestTests
Assert.Contains("\"concelierSnapshotHash\":", json);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ToCanonicalJson_ProducesDeterministicOutput()
{
var manifest = CreateSampleManifest();
@@ -95,7 +103,8 @@ public class ScanManifestTests
Assert.Equal(json1, json2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Builder_CreatesValidManifest()
{
var seed = new byte[32];
@@ -123,7 +132,8 @@ public class ScanManifestTests
Assert.Equal("10", manifest.Knobs["maxDepth"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Builder_WithKnobs_MergesMultipleKnobs()
{
var manifest = ScanManifest.CreateBuilder("scan-001", "sha256:abc123")
@@ -140,7 +150,8 @@ public class ScanManifestTests
Assert.Equal("value4", manifest.Knobs["key4"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Builder_SeedMustBe32Bytes()
{
var builder = ScanManifest.CreateBuilder("scan-001", "sha256:abc123");
@@ -149,7 +160,8 @@ public class ScanManifestTests
Assert.Contains("32 bytes", ex.Message);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Record_WithExpression_CreatesModifiedCopy()
{
var original = CreateSampleManifest();
@@ -160,7 +172,8 @@ public class ScanManifestTests
Assert.Equal(original.ScanId, modified.ScanId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ToJson_Indented_FormatsOutput()
{
var manifest = CreateSampleManifest();
@@ -170,7 +183,8 @@ public class ScanManifestTests
Assert.Contains(" ", json);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ToJson_NotIndented_CompactOutput()
{
var manifest = CreateSampleManifest();
@@ -179,7 +193,8 @@ public class ScanManifestTests
Assert.DoesNotContain("\n", json);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void KnobsCollection_IsImmutable()
{
var manifest = CreateSampleManifest();

View File

@@ -9,11 +9,14 @@ using StellaOps.Scanner.Diff;
using StellaOps.Scanner.Core.Utility;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Diff.Tests;
public sealed class ComponentDifferTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Compute_CapturesAddedRemovedAndChangedComponents()
{
var oldFragments = new[]
@@ -173,7 +176,8 @@ public sealed class ComponentDifferTests
Assert.False(removedJson.TryGetProperty("introducingLayer", out _));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Compute_UsageView_FiltersComponents()
{
var oldFragments = new[]
@@ -218,7 +222,8 @@ public sealed class ComponentDifferTests
Assert.False(parsed.RootElement.TryGetProperty("newImageDigest", out _));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Compute_MetadataChange_WhenEvidenceDiffers()
{
var oldFragments = new[]
@@ -277,7 +282,8 @@ public sealed class ComponentDifferTests
Assert.Equal(1, change.OldComponent!.Evidence.Length);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Compute_MetadataChange_WhenBuildIdDiffers()
{
var oldFragments = new[]

View File

@@ -8,5 +8,6 @@
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Diff/StellaOps.Scanner.Diff.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -5,13 +5,15 @@ using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Scanner.Emit.Lineage;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Emit.Lineage.Tests;
public class RebuildProofTests
{
#region RebuildProof Model Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RebuildProof_RequiredProperties_MustBeSet()
{
var proof = new RebuildProof
@@ -31,7 +33,8 @@ public class RebuildProofTests
proof.PolicyHash.Should().NotBeNullOrEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RebuildProof_WithFeedSnapshots_TracksAllFeeds()
{
var feeds = ImmutableArray.Create(
@@ -69,7 +72,8 @@ public class RebuildProofTests
proof.FeedSnapshots[1].EntryCount.Should().Be(15000);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RebuildProof_WithAnalyzerVersions_TracksAllAnalyzers()
{
var analyzers = ImmutableArray.Create(
@@ -103,7 +107,8 @@ public class RebuildProofTests
proof.AnalyzerVersions[0].AnalyzerId.Should().Be("npm-analyzer");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RebuildProof_OptionalDsseSignature_IsNullByDefault()
{
var proof = new RebuildProof
@@ -121,7 +126,8 @@ public class RebuildProofTests
proof.ProofHash.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RebuildProof_WithSignature_StoresSignature()
{
var proof = new RebuildProof
@@ -145,7 +151,8 @@ public class RebuildProofTests
#region FeedSnapshot Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FeedSnapshot_RequiredProperties_MustBeSet()
{
var snapshot = new FeedSnapshot
@@ -161,7 +168,8 @@ public class RebuildProofTests
snapshot.SnapshotHash.Should().NotBeNullOrEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FeedSnapshot_OptionalProperties_AreNullByDefault()
{
var snapshot = new FeedSnapshot
@@ -180,7 +188,8 @@ public class RebuildProofTests
#region AnalyzerVersion Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AnalyzerVersion_RequiredProperties_MustBeSet()
{
var analyzer = new AnalyzerVersion
@@ -195,7 +204,8 @@ public class RebuildProofTests
analyzer.Version.Should().Be("2.0.0");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AnalyzerVersion_OptionalHashes_AreNullByDefault()
{
var analyzer = new AnalyzerVersion
@@ -213,7 +223,8 @@ public class RebuildProofTests
#region RebuildVerification Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RebuildVerification_SuccessfulRebuild_HasMatchingHash()
{
var proof = new RebuildProof
@@ -242,7 +253,8 @@ public class RebuildProofTests
verification.ErrorMessage.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RebuildVerification_FailedRebuild_HasErrorMessage()
{
var proof = new RebuildProof
@@ -269,7 +281,8 @@ public class RebuildProofTests
verification.RebuiltSbomId.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RebuildVerification_MismatchRebuild_HasDifferences()
{
var proof = new RebuildProof

View File

@@ -5,6 +5,7 @@ using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Scanner.Emit.Lineage;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Emit.Lineage.Tests;
public class SbomDiffEngineTests
@@ -25,7 +26,8 @@ public class SbomDiffEngineTests
#region Basic Diff Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeDiff_IdenticalComponents_ReturnsNoDelta()
{
var fromId = SbomId.New();
@@ -46,7 +48,8 @@ public class SbomDiffEngineTests
diff.Summary.Unchanged.Should().Be(2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeDiff_AddedComponent_DetectsAddition()
{
var fromId = SbomId.New();
@@ -67,7 +70,8 @@ public class SbomDiffEngineTests
diff.Summary.Added.Should().Be(1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeDiff_RemovedComponent_DetectsRemoval()
{
var fromId = SbomId.New();
@@ -89,7 +93,8 @@ public class SbomDiffEngineTests
diff.Summary.IsBreaking.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeDiff_VersionUpgrade_DetectsVersionChange()
{
var fromId = SbomId.New();
@@ -107,7 +112,8 @@ public class SbomDiffEngineTests
diff.Summary.IsBreaking.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeDiff_VersionDowngrade_MarksAsBreaking()
{
var fromId = SbomId.New();
@@ -121,7 +127,8 @@ public class SbomDiffEngineTests
diff.Summary.IsBreaking.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeDiff_LicenseChange_DetectsLicenseChange()
{
var fromId = SbomId.New();
@@ -141,7 +148,8 @@ public class SbomDiffEngineTests
#region Complex Diff Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeDiff_MultipleChanges_TracksAll()
{
var fromId = SbomId.New();
@@ -170,7 +178,8 @@ public class SbomDiffEngineTests
diff.Summary.IsBreaking.Should().BeTrue(); // Due to removal
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeDiff_EmptyFrom_AllAdditions()
{
var fromId = SbomId.New();
@@ -190,7 +199,8 @@ public class SbomDiffEngineTests
diff.Summary.Unchanged.Should().Be(0);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeDiff_EmptyTo_AllRemovals()
{
var fromId = SbomId.New();
@@ -214,7 +224,8 @@ public class SbomDiffEngineTests
#region Determinism Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeDiff_SameInputs_ProducesSameOutput()
{
var fromId = SbomId.New();
@@ -239,7 +250,8 @@ public class SbomDiffEngineTests
diff1.Deltas.Should().HaveCount(diff2.Deltas.Length);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeDiff_DeltasAreSorted()
{
var fromId = SbomId.New();
@@ -267,7 +279,8 @@ public class SbomDiffEngineTests
#region CreatePointer Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreatePointer_SumsCorrectly()
{
var fromId = SbomId.New();
@@ -294,7 +307,8 @@ public class SbomDiffEngineTests
pointer.DiffHash.Should().NotBeNullOrEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreatePointer_DiffHashIsDeterministic()
{
var fromId = SbomId.New();
@@ -316,7 +330,8 @@ public class SbomDiffEngineTests
#region Summary Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DiffSummary_TotalComponents_CalculatesCorrectly()
{
var summary = new DiffSummary

View File

@@ -5,13 +5,15 @@ using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Scanner.Emit.Lineage;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Emit.Lineage.Tests;
public class SbomLineageTests
{
#region SbomId Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomId_New_CreatesUniqueId()
{
var id1 = SbomId.New();
@@ -20,7 +22,8 @@ public class SbomLineageTests
id1.Should().NotBe(id2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomId_Parse_RoundTrips()
{
var original = SbomId.New();
@@ -29,7 +32,8 @@ public class SbomLineageTests
parsed.Should().Be(original);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomId_ToString_ReturnsGuidString()
{
var id = SbomId.New();
@@ -42,7 +46,8 @@ public class SbomLineageTests
#region SbomLineage Model Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomLineage_RequiredProperties_MustBeSet()
{
var lineage = new SbomLineage
@@ -58,7 +63,8 @@ public class SbomLineageTests
lineage.ContentHash.Should().Be("sha256:def456");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomLineage_WithParent_TracksLineage()
{
var parentId = SbomId.New();
@@ -78,7 +84,8 @@ public class SbomLineageTests
child.Ancestors.Should().Contain(parentId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomLineage_WithDiffPointer_TracksChanges()
{
var diff = new SbomDiffPointer
@@ -103,7 +110,8 @@ public class SbomLineageTests
lineage.DiffFromParent!.TotalChanges.Should().Be(10);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomLineage_RootLineage_HasNoParent()
{
var root = new SbomLineage
@@ -123,7 +131,8 @@ public class SbomLineageTests
#region SbomDiffPointer Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomDiffPointer_TotalChanges_SumsAllCategories()
{
var pointer = new SbomDiffPointer
@@ -137,7 +146,8 @@ public class SbomLineageTests
pointer.TotalChanges.Should().Be(23);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomDiffPointer_EmptyDiff_HasZeroChanges()
{
var pointer = new SbomDiffPointer

View File

@@ -15,5 +15,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.Emit\StellaOps.Scanner.Emit.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -9,6 +9,8 @@ using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using StellaOps.TestKit;
namespace StellaOps.Scanner.EntryTrace.Tests;
public sealed class EntryTraceAnalyzerTests
@@ -20,7 +22,8 @@ public sealed class EntryTraceAnalyzerTests
_output = output;
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_CollapsesBundleExecWrapper()
{
var fs = new TestRootFileSystem();
@@ -55,7 +58,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Contains(result.Edges, edge => edge.Relationship == "wrapper" && edge.FromNodeId == bundleNode.Id);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_CollapsesDockerPhpEntrypointWrapper()
{
var fs = new TestRootFileSystem();
@@ -89,7 +93,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Equal("docker-php-entrypoint", wrapperNode.Metadata?["wrapper.name"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_CollapsesNpmExecWrapper()
{
var fs = new TestRootFileSystem();
@@ -136,7 +141,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Equal("npm", npmNode.Metadata?["wrapper.name"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_CollapsesYarnNodeWrapper()
{
var fs = new TestRootFileSystem();
@@ -175,7 +181,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Equal("yarn node", yarnNode.Metadata?["wrapper.name"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_CollapsesPipenvRunWrapper()
{
var fs = new TestRootFileSystem();
@@ -214,7 +221,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Equal("pipenv run", pipenvNode.Metadata?["wrapper.name"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_CollapsesPoetryRunWrapper()
{
var fs = new TestRootFileSystem();
@@ -263,7 +271,8 @@ public sealed class EntryTraceAnalyzerTests
return new EntryTraceAnalyzer(options, new EntryTraceMetrics(), NullLogger<EntryTraceAnalyzer>.Instance);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_FollowsShellIncludeAndPythonModule()
{
var fs = new TestRootFileSystem();
@@ -323,7 +332,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Contains(result.Edges, edge => edge.Relationship == "python-module" && edge.Metadata is { } metadata && metadata.TryGetValue("module", out var module) && module == "app.main");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_RecordsDiagnosticsForMissingInclude()
{
var fs = new TestRootFileSystem();
@@ -353,7 +363,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Equal(EntryTraceUnknownReason.MissingFile, result.Diagnostics[0].Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_IsDeterministic()
{
var fs = new TestRootFileSystem();
@@ -386,7 +397,8 @@ public sealed class EntryTraceAnalyzerTests
second.Edges.Select(e => (e.FromNodeId, e.ToNodeId, e.Relationship)).ToArray());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_HandlesCmdShellScript()
{
var fs = new TestRootFileSystem();
@@ -413,7 +425,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Contains(result.Diagnostics, diagnostic => diagnostic.Reason == EntryTraceUnknownReason.UnsupportedSyntax);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_ClassifiesGoBinaryWithPlan()
{
var fs = new TestRootFileSystem();
@@ -454,7 +467,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Equal("/", plan.WorkingDirectory);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_ExtractsJarManifestEvidence()
{
var fs = new TestRootFileSystem();
@@ -497,7 +511,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Equal(terminal.Confidence, plan.Confidence);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_UsesHistoryCandidateWhenEntrypointMissing()
{
var fs = new TestRootFileSystem();
@@ -533,7 +548,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Contains("/app/server.js", terminal.Arguments);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_DiscoversSupervisorCommand()
{
var fs = new TestRootFileSystem();
@@ -569,7 +585,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Contains("app:app", terminal.Arguments);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_DiscoversEntrypointScriptCandidate()
{
var fs = new TestRootFileSystem();
@@ -605,7 +622,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Contains("/srv/service.py", terminal.Arguments);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_DiscoversServiceRunScript()
{
var fs = new TestRootFileSystem();
@@ -654,7 +672,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Equal(EntryTraceTerminalType.Native, terminal.Type);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_PropagatesUserSwitchWrapper()
{
var fs = new TestRootFileSystem();
@@ -690,7 +709,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Equal("app", edge.Metadata?["user"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_PropagatesEnvWrapperIntoPlan()
{
var fs = new TestRootFileSystem();
@@ -725,7 +745,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Equal("true", edge.Metadata?["guarded"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_AccumulatesWorkingDirectoryFromShellCd()
{
var fs = new TestRootFileSystem();
@@ -756,7 +777,8 @@ public sealed class EntryTraceAnalyzerTests
Assert.Equal("/service", terminal.WorkingDirectory);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAsync_HandlesInitShimAndGuardsEdge()
{
var fs = new TestRootFileSystem();

View File

@@ -4,11 +4,13 @@ using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.EntryTrace.Tests;
public sealed class EntryTraceImageContextFactoryTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Create_UsesEnvironmentAndEntrypointFromConfig()
{
var json = """
@@ -52,7 +54,8 @@ public sealed class EntryTraceImageContextFactoryTests
Assert.Equal("/custom/bin:/usr/bin", string.Join(":", imageContext.Context.Path));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Create_FallsBackToDefaultPathWhenMissing()
{
var json = """

View File

@@ -9,11 +9,14 @@ using System.Text.Json;
using StellaOps.Scanner.EntryTrace;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.EntryTrace.Tests;
public sealed class EntryTraceNdjsonWriterTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Serialize_ProducesDeterministicNdjsonLines()
{
var (graph, metadata) = CreateSampleGraph();
@@ -58,7 +61,8 @@ public sealed class EntryTraceNdjsonWriterTests
Assert.Equal("gosu", capabilityJson.GetProperty("name").GetString());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Serialize_ProducesStableSha256Hash()
{
var (graph, metadata) = CreateSampleGraph();

View File

@@ -5,6 +5,8 @@ using System.Text;
using StellaOps.Scanner.EntryTrace.FileSystem;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.EntryTrace.Tests;
public sealed class LayeredRootFileSystemTests : IDisposable
@@ -17,7 +19,8 @@ public sealed class LayeredRootFileSystemTests : IDisposable
Directory.CreateDirectory(_tempRoot);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FromDirectories_HandlesWhiteoutsAndResolution()
{
var layer1 = CreateLayerDirectory("layer1");
@@ -65,7 +68,8 @@ public sealed class LayeredRootFileSystemTests : IDisposable
Assert.DoesNotContain(optEntries, entry => entry.Path.EndsWith("setup.sh", StringComparison.Ordinal));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryReadBytes_ReturnsLimitedPreview()
{
var layer = CreateLayerDirectory("layer-bytes");
@@ -84,7 +88,8 @@ public sealed class LayeredRootFileSystemTests : IDisposable
Assert.Equal("abcd", Encoding.UTF8.GetString(preview.Span));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FromArchives_ResolvesSymlinkAndWhiteout()
{
var layer1Path = Path.Combine(_tempRoot, "layer1.tar");
@@ -135,7 +140,8 @@ public sealed class LayeredRootFileSystemTests : IDisposable
Assert.False(fs.TryReadAllText("/opt/old.sh", out _, out _));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FromArchives_ResolvesHardLinkContent()
{
var baseLayer = Path.Combine(_tempRoot, "base.tar");

View File

@@ -1,11 +1,13 @@
using StellaOps.Scanner.EntryTrace.Parsing;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.EntryTrace.Tests;
public sealed class ShellParserTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parse_ProducesDeterministicNodes()
{
const string script = """

View File

@@ -23,6 +23,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.EntryTrace/StellaOps.Scanner.EntryTrace.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />

View File

@@ -9,11 +9,13 @@ using FluentAssertions;
using StellaOps.Scanner.Evidence.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Evidence.Tests;
public sealed class FuncProofBuilderTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithBinaryIdentity_SetsFileProperties()
{
// Arrange
@@ -33,7 +35,8 @@ public sealed class FuncProofBuilderTests
proof.SchemaVersion.Should().Be(FuncProofConstants.SchemaVersion);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithSection_AddsSectionToProof()
{
// Arrange
@@ -56,7 +59,8 @@ public sealed class FuncProofBuilderTests
proof.Sections![0].Hash.Should().Be(sectionHash);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithMultipleSections_AddsAllSections()
{
// Arrange & Act
@@ -74,7 +78,8 @@ public sealed class FuncProofBuilderTests
proof.Sections![2].Name.Should().Be(".data");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithFunction_AddsFunctionToProof()
{
// Arrange
@@ -99,7 +104,8 @@ public sealed class FuncProofBuilderTests
proof.Functions![0].Hash.Should().Be(functionHash);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithFunctionCallers_SetsCallersOnFunction()
{
// Arrange
@@ -115,7 +121,8 @@ public sealed class FuncProofBuilderTests
proof.Functions![0].Callers.Should().BeEquivalentTo(callers);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithTrace_AddsTraceToProof()
{
// Arrange
@@ -135,7 +142,8 @@ public sealed class FuncProofBuilderTests
proof.Traces![0].Truncated.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithTruncatedTrace_SetsTruncatedFlag()
{
// Arrange
@@ -151,7 +159,8 @@ public sealed class FuncProofBuilderTests
proof.Traces![0].Truncated.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_WithMetadata_SetsMetadataProperties()
{
// Arrange
@@ -172,7 +181,8 @@ public sealed class FuncProofBuilderTests
proof.Metadata.CreatedAt.Should().Be(timestamp);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_GeneratesProofId()
{
// Arrange & Act
@@ -186,7 +196,8 @@ public sealed class FuncProofBuilderTests
proof.ProofId.Should().HaveLength(64); // SHA-256 hex
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_SameInput_GeneratesSameProofId()
{
// Arrange
@@ -205,7 +216,8 @@ public sealed class FuncProofBuilderTests
proof1.ProofId.Should().Be(proof2.ProofId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_DifferentInput_GeneratesDifferentProofId()
{
// Arrange & Act
@@ -223,7 +235,8 @@ public sealed class FuncProofBuilderTests
proof1.ProofId.Should().NotBe(proof2.ProofId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeSymbolDigest_DeterministicForSameInput()
{
// Arrange
@@ -238,7 +251,8 @@ public sealed class FuncProofBuilderTests
digest1.Should().Be(digest2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeSymbolDigest_DifferentForDifferentOffset()
{
// Arrange
@@ -252,7 +266,8 @@ public sealed class FuncProofBuilderTests
digest1.Should().NotBe(digest2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeFunctionHash_DeterministicForSameInput()
{
// Arrange
@@ -266,7 +281,8 @@ public sealed class FuncProofBuilderTests
hash1.Should().Be(hash2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeFunctionHash_DifferentForDifferentInput()
{
// Arrange
@@ -281,7 +297,8 @@ public sealed class FuncProofBuilderTests
hash1.Should().NotBe(hash2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeProofId_DeterministicForSameProof()
{
// Arrange
@@ -298,7 +315,8 @@ public sealed class FuncProofBuilderTests
id1.Should().Be(id2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_FunctionOrdering_IsDeterministic()
{
// Arrange & Act

View File

@@ -15,6 +15,7 @@ using StellaOps.Scanner.Evidence.Models;
using StellaOps.Scanner.ProofSpine;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Evidence.Tests;
public sealed class FuncProofDsseServiceTests
@@ -34,7 +35,8 @@ public sealed class FuncProofDsseServiceTests
_logger = NullLogger<FuncProofDsseService>.Instance;
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SignAsync_WithValidProof_ReturnsSignedEnvelope()
{
// Arrange
@@ -64,7 +66,8 @@ public sealed class FuncProofDsseServiceTests
result.EnvelopeJson.Should().NotBeNullOrEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SignAsync_WithNullProofId_ThrowsArgumentException()
{
// Arrange
@@ -81,7 +84,8 @@ public sealed class FuncProofDsseServiceTests
await Assert.ThrowsAsync<ArgumentException>(() => service.SignAsync(proof));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SignAsync_CallsSigningServiceWithCorrectPayloadType()
{
// Arrange
@@ -109,7 +113,8 @@ public sealed class FuncProofDsseServiceTests
capturedPayloadType.Should().Be(FuncProofConstants.MediaType);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task VerifyAsync_WithValidEnvelope_ReturnsSuccessResult()
{
// Arrange
@@ -136,7 +141,8 @@ public sealed class FuncProofDsseServiceTests
result.FuncProof.Should().NotBeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task VerifyAsync_WithWrongPayloadType_ReturnsInvalidResult()
{
// Arrange
@@ -155,7 +161,8 @@ public sealed class FuncProofDsseServiceTests
result.FailureReason.Should().Contain("Invalid payload type");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task VerifyAsync_WithFailedSignature_ReturnsInvalidResult()
{
// Arrange
@@ -180,7 +187,8 @@ public sealed class FuncProofDsseServiceTests
result.FailureReason.Should().Be("dsse_sig_mismatch");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ExtractPayload_WithValidEnvelope_ReturnsFuncProof()
{
// Arrange
@@ -205,7 +213,8 @@ public sealed class FuncProofDsseServiceTests
extracted.BuildId.Should().Be(proof.BuildId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ExtractPayload_WithInvalidBase64_ReturnsNull()
{
// Arrange
@@ -223,7 +232,8 @@ public sealed class FuncProofDsseServiceTests
extracted.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ExtractPayload_WithInvalidJson_ReturnsNull()
{
// Arrange
@@ -241,7 +251,8 @@ public sealed class FuncProofDsseServiceTests
extracted.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ToUnsignedEnvelope_CreatesValidEnvelope()
{
// Arrange
@@ -257,7 +268,8 @@ public sealed class FuncProofDsseServiceTests
envelope.Payload.Should().NotBeNullOrEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParseEnvelope_WithValidJson_ReturnsEnvelope()
{
// Arrange
@@ -278,7 +290,8 @@ public sealed class FuncProofDsseServiceTests
parsed!.PayloadType.Should().Be("test-type");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParseEnvelope_WithInvalidJson_ReturnsNull()
{
// Act
@@ -288,7 +301,8 @@ public sealed class FuncProofDsseServiceTests
parsed.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParseEnvelope_WithEmptyString_ReturnsNull()
{
// Act

View File

@@ -14,6 +14,7 @@ using StellaOps.Scanner.Evidence;
using StellaOps.Scanner.Evidence.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Evidence.Tests;
public sealed class SbomFuncProofLinkerTests
@@ -25,7 +26,8 @@ public sealed class SbomFuncProofLinkerTests
_linker = new SbomFuncProofLinker(NullLogger<SbomFuncProofLinker>.Instance);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void LinkFuncProofEvidence_AddsEvidenceToComponent()
{
// Arrange
@@ -58,7 +60,8 @@ public sealed class SbomFuncProofLinkerTests
frames!.Count.Should().BeGreaterThan(0);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void LinkFuncProofEvidence_AddsExternalReference()
{
// Arrange
@@ -86,7 +89,8 @@ public sealed class SbomFuncProofLinkerTests
evidenceRef["url"]!.GetValue<string>().Should().Contain("oci://");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void LinkFuncProofEvidence_ThrowsForNonCycloneDx()
{
// Arrange
@@ -111,7 +115,8 @@ public sealed class SbomFuncProofLinkerTests
.WithMessage("*CycloneDX*");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void LinkFuncProofEvidence_ThrowsForMissingComponent()
{
// Arrange
@@ -130,7 +135,8 @@ public sealed class SbomFuncProofLinkerTests
.WithMessage("*not found*");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ExtractFuncProofReferences_ReturnsEmptyForNoEvidence()
{
// Arrange
@@ -143,7 +149,8 @@ public sealed class SbomFuncProofLinkerTests
refs.Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ExtractFuncProofReferences_FindsLinkedEvidence()
{
// Arrange
@@ -167,7 +174,8 @@ public sealed class SbomFuncProofLinkerTests
refs[0].FunctionCount.Should().Be(funcProof.Functions.Length);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateEvidenceRef_PopulatesAllFields()
{
// Arrange
@@ -189,7 +197,8 @@ public sealed class SbomFuncProofLinkerTests
evidenceRef.TraceCount.Should().Be(1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void LinkFuncProofEvidence_IncludesProofProperties()
{
// Arrange
@@ -224,7 +233,8 @@ public sealed class SbomFuncProofLinkerTests
proofIdProperty!["value"]!.GetValue<string>().Should().Be(funcProof.ProofId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void LinkFuncProofEvidence_MergesWithExistingEvidence()
{
// Arrange

View File

@@ -27,5 +27,6 @@
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Evidence/StellaOps.Scanner.Evidence.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.ProofSpine/StellaOps.Scanner.ProofSpine.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -6,6 +6,7 @@ using StellaOps.Scanner.Explainability.Assumptions;
using StellaOps.Scanner.Explainability.Confidence;
using StellaOps.Scanner.Explainability.Falsifiability;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Explainability.Tests;
public class RiskReportTests
@@ -17,7 +18,8 @@ public class RiskReportTests
_generator = new RiskReportGenerator(new EvidenceDensityScorer());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Generate_MinimalInput_CreatesReport()
{
var input = new RiskReportInput
@@ -39,7 +41,8 @@ public class RiskReportTests
result.GeneratedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Generate_WithSeverity_IncludesInExplanation()
{
var input = new RiskReportInput
@@ -56,7 +59,8 @@ public class RiskReportTests
result.Explanation.Should().Contain("CRITICAL");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Generate_WithFixedVersion_RecommendsUpdate()
{
var input = new RiskReportInput
@@ -74,7 +78,8 @@ public class RiskReportTests
a.Action.Contains("Update") && a.Action.Contains("1.0.1"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Generate_WithoutFixedVersion_RecommendsMonitoring()
{
var input = new RiskReportInput
@@ -91,7 +96,8 @@ public class RiskReportTests
a.Action.Contains("Monitor") || a.Action.Contains("compensating"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Generate_WithEvidenceFactors_CalculatesConfidence()
{
var input = new RiskReportInput
@@ -113,7 +119,8 @@ public class RiskReportTests
result.ConfidenceScore!.Score.Should().BeGreaterThan(0);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Generate_WithAssumptions_IncludesInReport()
{
var assumptions = new AssumptionSet
@@ -147,7 +154,8 @@ public class RiskReportTests
result.DetailedNarrative.Should().Contain("Assumptions");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Generate_WithFalsifiability_IncludesInReport()
{
var falsifiability = new FalsifiabilityCriteria
@@ -183,7 +191,8 @@ public class RiskReportTests
result.Explanation.Should().Contain("falsified");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Generate_WithUnvalidatedAssumptions_RecommendsValidation()
{
var assumptions = new AssumptionSet
@@ -212,7 +221,8 @@ public class RiskReportTests
a.Action.Contains("Validate") || a.Action.Contains("assumption"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Generate_WithPartiallyEvaluatedFalsifiability_RecommendsCompletion()
{
var falsifiability = new FalsifiabilityCriteria
@@ -242,7 +252,8 @@ public class RiskReportTests
a.Action.Contains("falsifiability") || a.Action.Contains("evaluation"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RecommendedAction_HasRequiredProperties()
{
var action = new RecommendedAction(
@@ -257,7 +268,8 @@ public class RiskReportTests
action.Effort.Should().Be(EffortLevel.Low);
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(EffortLevel.Low)]
[InlineData(EffortLevel.Medium)]
[InlineData(EffortLevel.High)]

View File

@@ -17,5 +17,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.Explainability\StellaOps.Scanner.Explainability.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -11,6 +11,8 @@ using StellaOps.Scanner.Worker.Orchestration;
using StellaOps.Signals.Storage;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Integration.Tests;
/// <summary>
@@ -41,7 +43,8 @@ public class PoEPipelineTests : IDisposable
);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ScanWithVulnerability_GeneratesPoE_StoresInCas()
{
// Arrange
@@ -98,7 +101,8 @@ public class PoEPipelineTests : IDisposable
Assert.Equal(dsseBytes, artifact.DsseBytes);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ScanWithUnreachableVuln_DoesNotGeneratePoE()
{
// Arrange
@@ -124,7 +128,8 @@ public class PoEPipelineTests : IDisposable
Assert.Empty(results);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PoEGeneration_ProducesDeterministicHash()
{
// Arrange
@@ -141,7 +146,8 @@ public class PoEPipelineTests : IDisposable
Assert.StartsWith("blake3:", hash1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PoEStorage_PersistsToCas_RetrievesCorrectly()
{
// Arrange

View File

@@ -35,6 +35,7 @@
<!-- Authority: Verdict manifests, signing, replay -->
<ProjectReference Include="../../../Authority/__Libraries/StellaOps.Authority.Core/StellaOps.Authority.Core.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>

Some files were not shown because too many files have changed in this diff Show More