using FluentAssertions; using StellaOps.Scanner.Sources.Domain; using Xunit; namespace StellaOps.Scanner.Sources.Tests.Domain; public class SbomSourceRunTests { [Fact] public void Create_WithValidInputs_CreatesRunInPendingStatus() { // Arrange var sourceId = Guid.NewGuid(); var correlationId = Guid.NewGuid().ToString("N"); // Act var run = SbomSourceRun.Create( sourceId: sourceId, tenantId: "tenant-1", trigger: SbomSourceRunTrigger.Manual, correlationId: correlationId, triggerDetails: "Triggered by user"); // Assert run.RunId.Should().NotBeEmpty(); run.SourceId.Should().Be(sourceId); run.TenantId.Should().Be("tenant-1"); run.Trigger.Should().Be(SbomSourceRunTrigger.Manual); run.CorrelationId.Should().Be(correlationId); run.TriggerDetails.Should().Be("Triggered by user"); run.Status.Should().Be(SbomSourceRunStatus.Pending); run.ItemsDiscovered.Should().Be(0); run.ItemsScanned.Should().Be(0); } [Fact] public void Start_SetsStatusToRunning() { // Arrange var run = CreateTestRun(); // Act run.Start(); // Assert run.Status.Should().Be(SbomSourceRunStatus.Running); } [Fact] public void SetDiscoveredItems_UpdatesDiscoveryCount() { // Arrange var run = CreateTestRun(); run.Start(); // Act run.SetDiscoveredItems(10); // Assert run.ItemsDiscovered.Should().Be(10); } [Fact] public void RecordItemSuccess_IncrementsCounts() { // Arrange var run = CreateTestRun(); run.Start(); run.SetDiscoveredItems(5); // Act var scanJobId = Guid.NewGuid(); run.RecordItemSuccess(scanJobId); run.RecordItemSuccess(Guid.NewGuid()); // Assert run.ItemsScanned.Should().Be(2); run.ItemsSucceeded.Should().Be(2); run.ScanJobIds.Should().Contain(scanJobId); } [Fact] public void RecordItemFailure_IncrementsCounts() { // Arrange var run = CreateTestRun(); run.Start(); run.SetDiscoveredItems(5); // Act run.RecordItemFailure(); run.RecordItemFailure(); // Assert run.ItemsScanned.Should().Be(2); run.ItemsFailed.Should().Be(2); run.ItemsSucceeded.Should().Be(0); } [Fact] public void RecordItemSkipped_IncrementsCounts() { // Arrange var run = CreateTestRun(); run.Start(); run.SetDiscoveredItems(5); // Act run.RecordItemSkipped(); // Assert run.ItemsScanned.Should().Be(1); run.ItemsSkipped.Should().Be(1); } [Fact] public void Complete_SetsSuccessStatusAndDuration() { // Arrange var run = CreateTestRun(); run.Start(); run.SetDiscoveredItems(3); run.RecordItemSuccess(Guid.NewGuid()); run.RecordItemSuccess(Guid.NewGuid()); run.RecordItemSuccess(Guid.NewGuid()); // Act run.Complete(); // Assert run.Status.Should().Be(SbomSourceRunStatus.Succeeded); run.CompletedAt.Should().NotBeNull(); run.DurationMs.Should().BeGreaterOrEqualTo(0); } [Fact] public void Fail_SetsFailedStatusAndErrorMessage() { // Arrange var run = CreateTestRun(); run.Start(); // Act run.Fail("Connection timeout", new { retries = 3 }); // Assert run.Status.Should().Be(SbomSourceRunStatus.Failed); run.ErrorMessage.Should().Be("Connection timeout"); run.ErrorDetails.Should().NotBeNull(); run.CompletedAt.Should().NotBeNull(); } [Fact] public void Cancel_SetsCancelledStatus() { // Arrange var run = CreateTestRun(); run.Start(); // Act run.Cancel(); // Assert run.Status.Should().Be(SbomSourceRunStatus.Cancelled); run.CompletedAt.Should().NotBeNull(); } [Fact] public void MixedResults_TracksAllCountsCorrectly() { // Arrange var run = CreateTestRun(); run.Start(); run.SetDiscoveredItems(10); // Act run.RecordItemSuccess(Guid.NewGuid()); // 1 success run.RecordItemSuccess(Guid.NewGuid()); // 2 successes run.RecordItemFailure(); // 1 failure run.RecordItemSkipped(); // 1 skipped run.RecordItemSuccess(Guid.NewGuid()); // 3 successes run.RecordItemFailure(); // 2 failures // Assert run.ItemsScanned.Should().Be(6); run.ItemsSucceeded.Should().Be(3); run.ItemsFailed.Should().Be(2); run.ItemsSkipped.Should().Be(1); run.ScanJobIds.Should().HaveCount(3); } [Theory] [InlineData(SbomSourceRunTrigger.Manual, "Manual trigger")] [InlineData(SbomSourceRunTrigger.Scheduled, "Cron: 0 * * * *")] [InlineData(SbomSourceRunTrigger.Webhook, "Harbor push event")] [InlineData(SbomSourceRunTrigger.Push, "Registry push event")] public void Create_WithDifferentTriggers_StoresTriggerInfo( SbomSourceRunTrigger trigger, string details) { // Arrange & Act var run = SbomSourceRun.Create( sourceId: Guid.NewGuid(), tenantId: "tenant-1", trigger: trigger, correlationId: Guid.NewGuid().ToString("N"), triggerDetails: details); // Assert run.Trigger.Should().Be(trigger); run.TriggerDetails.Should().Be(details); } private static SbomSourceRun CreateTestRun() { return SbomSourceRun.Create( sourceId: Guid.NewGuid(), tenantId: "tenant-1", trigger: SbomSourceRunTrigger.Manual, correlationId: Guid.NewGuid().ToString("N")); } }