wip - advisories and ui extensions
This commit is contained in:
@@ -0,0 +1,380 @@
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.Sources.Configuration;
|
||||
using StellaOps.Scanner.Sources.Domain;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sources.Tests.Configuration;
|
||||
|
||||
public class SourceConfigValidatorTests
|
||||
{
|
||||
private readonly SourceConfigValidator _validator = new(NullLogger<SourceConfigValidator>.Instance);
|
||||
|
||||
#region Zastava Configuration Tests
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidZastavaConfig_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"registryType": "Harbor",
|
||||
"registryUrl": "https://harbor.example.com",
|
||||
"filters": {
|
||||
"repositoryPatterns": ["library/*"]
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Zastava, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Errors.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ZastavaConfig_MissingRegistryType_ReturnsFalure()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"registryUrl": "https://harbor.example.com"
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Zastava, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("registryType"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ZastavaConfig_InvalidRegistryType_ReturnsFalure()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"registryType": "InvalidRegistry",
|
||||
"registryUrl": "https://harbor.example.com"
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Zastava, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("Invalid registryType"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ZastavaConfig_MissingRegistryUrl_ReturnsFalure()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"registryType": "Harbor"
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Zastava, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("registryUrl"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ZastavaConfig_NoFilters_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"registryType": "Harbor",
|
||||
"registryUrl": "https://harbor.example.com"
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Zastava, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Warnings.Should().Contain(w => w.Contains("No filters"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Docker Configuration Tests
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidDockerConfig_WithImages_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"registryUrl": "https://registry.example.com",
|
||||
"images": [
|
||||
{
|
||||
"repository": "library/nginx",
|
||||
"tag": "latest"
|
||||
}
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Docker, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidDockerConfig_WithDiscovery_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"registryUrl": "https://registry.example.com",
|
||||
"discoveryOptions": {
|
||||
"repositoryPattern": "library/*",
|
||||
"maxTagsPerRepo": 5
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Docker, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DockerConfig_NoImagesOrDiscovery_ReturnsFalure()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"registryUrl": "https://registry.example.com"
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Docker, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("images") || e.Contains("discoveryOptions"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DockerConfig_ImageMissingRepository_ReturnsFalure()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"tag": "latest"
|
||||
}
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Docker, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("repository"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CLI Configuration Tests
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidCliConfig_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"acceptedFormats": ["CycloneDX", "SPDX"],
|
||||
"validationRules": {
|
||||
"requireSignature": false,
|
||||
"maxFileSizeBytes": 10485760
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Cli, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_CliConfig_InvalidFormat_ReturnsFalure()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"acceptedFormats": ["InvalidFormat"]
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Cli, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("Invalid SBOM format"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_CliConfig_Empty_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("{}");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Cli, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Warnings.Should().Contain(w => w.Contains("validation rules"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Git Configuration Tests
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidGitConfig_HttpsUrl_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"repositoryUrl": "https://github.com/example/repo",
|
||||
"provider": "GitHub",
|
||||
"authMethod": "Token",
|
||||
"branchConfig": {
|
||||
"defaultBranch": "main"
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Git, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidGitConfig_SshUrl_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"repositoryUrl": "git@github.com:example/repo.git",
|
||||
"provider": "GitHub",
|
||||
"authMethod": "SshKey"
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Git, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_GitConfig_MissingRepositoryUrl_ReturnsFalure()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"provider": "GitHub"
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Git, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("repositoryUrl"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_GitConfig_InvalidProvider_ReturnsFalure()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"repositoryUrl": "https://github.com/example/repo",
|
||||
"provider": "InvalidProvider"
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Git, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("Invalid provider"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_GitConfig_NoBranchConfig_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var config = JsonDocument.Parse("""
|
||||
{
|
||||
"repositoryUrl": "https://github.com/example/repo",
|
||||
"provider": "GitHub"
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(SbomSourceType.Git, config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Warnings.Should().Contain(w => w.Contains("branch configuration"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Schema Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(SbomSourceType.Zastava)]
|
||||
[InlineData(SbomSourceType.Docker)]
|
||||
[InlineData(SbomSourceType.Cli)]
|
||||
[InlineData(SbomSourceType.Git)]
|
||||
public void GetConfigurationSchema_ReturnsValidJsonSchema(SbomSourceType sourceType)
|
||||
{
|
||||
// Act
|
||||
var schema = _validator.GetConfigurationSchema(sourceType);
|
||||
|
||||
// Assert
|
||||
schema.Should().NotBeNullOrEmpty();
|
||||
var parsed = JsonDocument.Parse(schema);
|
||||
parsed.RootElement.GetProperty("$schema").GetString()
|
||||
.Should().Contain("json-schema.org");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Sources/StellaOps.Scanner.Sources.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user