using System.Text.Json; using FluentAssertions; using Microsoft.Extensions.Time.Testing; using StellaOps.Scanner.Sources.Domain; using Xunit; namespace StellaOps.Scanner.Sources.Tests.Domain; public class SbomSourceTests { private readonly FakeTimeProvider _timeProvider; public SbomSourceTests() { _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 15, 10, 0, 0, TimeSpan.Zero)); } 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", timeProvider: _timeProvider); // 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.Pending); 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", timeProvider: _timeProvider, cronSchedule: "0 * * * *"); // Every hour // Assert source.CronSchedule.Should().Be("0 * * * *"); source.NextScheduledRun.Should().NotBeNull(); source.NextScheduledRun.Should().BeAfter(_timeProvider.GetUtcNow()); } [Fact] public void Create_WithZastavaType_GeneratesWebhookEndpoint() { // Arrange & Act var source = SbomSource.Create( tenantId: "tenant-1", name: "webhook-source", sourceType: SbomSourceType.Zastava, configuration: SampleConfig, createdBy: "user-1", timeProvider: _timeProvider); // Assert source.WebhookEndpoint.Should().NotBeNullOrEmpty(); source.WebhookSecretRef.Should().NotBeNullOrEmpty(); } [Fact] public void Activate_FromPending_ChangesStatusToActive() { // Arrange var source = SbomSource.Create( tenantId: "tenant-1", name: "test-source", sourceType: SbomSourceType.Docker, configuration: SampleConfig, createdBy: "user-1", timeProvider: _timeProvider); // Act source.Activate("activator", _timeProvider); // 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", timeProvider: _timeProvider); source.Activate("activator", _timeProvider); // Act source.Pause("Maintenance window", "TICKET-123", "operator", _timeProvider); // 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", timeProvider: _timeProvider); source.Activate("activator", _timeProvider); source.Pause("Maintenance", null, "operator", _timeProvider); // Act source.Resume("operator", _timeProvider); // 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", timeProvider: _timeProvider); source.Activate("activator", _timeProvider); // Simulate some failures var runAt = _timeProvider.GetUtcNow(); source.RecordFailedRun(runAt, "Error 1", _timeProvider); source.RecordFailedRun(runAt, "Error 2", _timeProvider); source.ConsecutiveFailures.Should().Be(2); // Act source.RecordSuccessfulRun(runAt, _timeProvider); // 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", timeProvider: _timeProvider); source.Activate("activator", _timeProvider); // Act - fail multiple times var runAt = _timeProvider.GetUtcNow(); for (var i = 0; i < 5; i++) { source.RecordFailedRun(runAt, $"Error {i + 1}", _timeProvider); } // 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", timeProvider: _timeProvider); source.MaxScansPerHour = 10; source.Activate("activator", _timeProvider); // Act var isLimited = source.IsRateLimited(_timeProvider); // 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", timeProvider: _timeProvider); var newConfig = JsonDocument.Parse(""" { "registryType": "DockerHub", "registryUrl": "https://registry-1.docker.io" } """); // Act source.UpdateConfiguration(newConfig, "updater", _timeProvider); // Assert source.Configuration.RootElement.GetProperty("registryType").GetString() .Should().Be("DockerHub"); source.UpdatedBy.Should().Be("updater"); } }