using System.Text.Json; using FluentAssertions; using StellaOps.Scanner.Sources.Domain; using Xunit; namespace StellaOps.Scanner.Sources.Tests.Domain; public class SbomSourceTests { private static readonly JsonDocument SampleConfig = JsonDocument.Parse(""" { "registryType": "Harbor", "registryUrl": "https://harbor.example.com" } """); [Fact] public void Create_WithValidInputs_CreatesSourceInDraftStatus() { // Arrange & Act var source = SbomSource.Create( tenantId: "tenant-1", name: "test-source", sourceType: SbomSourceType.Zastava, configuration: SampleConfig, createdBy: "user-1"); // Assert source.SourceId.Should().NotBeEmpty(); source.TenantId.Should().Be("tenant-1"); source.Name.Should().Be("test-source"); source.SourceType.Should().Be(SbomSourceType.Zastava); source.Status.Should().Be(SbomSourceStatus.Draft); source.CreatedBy.Should().Be("user-1"); source.Paused.Should().BeFalse(); source.ConsecutiveFailures.Should().Be(0); } [Fact] public void Create_WithCronSchedule_CalculatesNextScheduledRun() { // Arrange & Act var source = SbomSource.Create( tenantId: "tenant-1", name: "scheduled-source", sourceType: SbomSourceType.Docker, configuration: SampleConfig, createdBy: "user-1", cronSchedule: "0 * * * *"); // Every hour // Assert source.CronSchedule.Should().Be("0 * * * *"); source.NextScheduledRun.Should().NotBeNull(); source.NextScheduledRun.Should().BeAfter(DateTimeOffset.UtcNow); } [Fact] public void Create_WithZastavaType_GeneratesWebhookEndpointAndSecret() { // Arrange & Act var source = SbomSource.Create( tenantId: "tenant-1", name: "webhook-source", sourceType: SbomSourceType.Zastava, configuration: SampleConfig, createdBy: "user-1"); // Assert source.WebhookEndpoint.Should().NotBeNullOrEmpty(); source.WebhookSecret.Should().NotBeNullOrEmpty(); source.WebhookSecret!.Length.Should().BeGreaterOrEqualTo(32); } [Fact] public void Activate_FromDraft_ChangesStatusToActive() { // Arrange var source = SbomSource.Create( tenantId: "tenant-1", name: "test-source", sourceType: SbomSourceType.Docker, configuration: SampleConfig, createdBy: "user-1"); // Act source.Activate("activator"); // Assert source.Status.Should().Be(SbomSourceStatus.Active); source.UpdatedBy.Should().Be("activator"); } [Fact] public void Pause_WhenActive_PausesSource() { // Arrange var source = SbomSource.Create( tenantId: "tenant-1", name: "test-source", sourceType: SbomSourceType.Docker, configuration: SampleConfig, createdBy: "user-1"); source.Activate("activator"); // Act source.Pause("Maintenance window", "TICKET-123", "operator"); // Assert source.Paused.Should().BeTrue(); source.PauseReason.Should().Be("Maintenance window"); source.PauseTicket.Should().Be("TICKET-123"); source.PausedAt.Should().NotBeNull(); } [Fact] public void Resume_WhenPaused_UnpausesSource() { // Arrange var source = SbomSource.Create( tenantId: "tenant-1", name: "test-source", sourceType: SbomSourceType.Docker, configuration: SampleConfig, createdBy: "user-1"); source.Activate("activator"); source.Pause("Maintenance", null, "operator"); // Act source.Resume("operator"); // Assert source.Paused.Should().BeFalse(); source.PauseReason.Should().BeNull(); source.PausedAt.Should().BeNull(); } [Fact] public void RecordSuccessfulRun_ResetsConsecutiveFailures() { // Arrange var source = SbomSource.Create( tenantId: "tenant-1", name: "test-source", sourceType: SbomSourceType.Docker, configuration: SampleConfig, createdBy: "user-1"); source.Activate("activator"); // Simulate some failures source.RecordFailedRun("Error 1"); source.RecordFailedRun("Error 2"); source.ConsecutiveFailures.Should().Be(2); // Act source.RecordSuccessfulRun(); // Assert source.ConsecutiveFailures.Should().Be(0); source.LastRunStatus.Should().Be(SbomSourceRunStatus.Succeeded); source.LastRunError.Should().BeNull(); } [Fact] public void RecordFailedRun_MultipleTimes_MovesToErrorStatus() { // Arrange var source = SbomSource.Create( tenantId: "tenant-1", name: "test-source", sourceType: SbomSourceType.Docker, configuration: SampleConfig, createdBy: "user-1"); source.Activate("activator"); // Act - fail 5 times (threshold is 5) for (var i = 0; i < 5; i++) { source.RecordFailedRun($"Error {i + 1}"); } // Assert source.Status.Should().Be(SbomSourceStatus.Error); source.ConsecutiveFailures.Should().Be(5); } [Fact] public void IsRateLimited_WhenUnderLimit_ReturnsFalse() { // Arrange var source = SbomSource.Create( tenantId: "tenant-1", name: "test-source", sourceType: SbomSourceType.Docker, configuration: SampleConfig, createdBy: "user-1"); source.MaxScansPerHour = 10; source.Activate("activator"); // Act var isLimited = source.IsRateLimited(); // Assert isLimited.Should().BeFalse(); } [Fact] public void UpdateConfiguration_ChangesConfigAndUpdatesTimestamp() { // Arrange var source = SbomSource.Create( tenantId: "tenant-1", name: "test-source", sourceType: SbomSourceType.Docker, configuration: SampleConfig, createdBy: "user-1"); var newConfig = JsonDocument.Parse(""" { "registryType": "DockerHub", "registryUrl": "https://registry-1.docker.io" } """); // Act source.UpdateConfiguration(newConfig, "updater"); // Assert source.Configuration.RootElement.GetProperty("registryType").GetString() .Should().Be("DockerHub"); source.UpdatedBy.Should().Be("updater"); } }