Files
git.stella-ops.org/src/Policy/__Tests/StellaOps.Policy.Exceptions.Tests/ExceptionObjectTests.cs

333 lines
9.6 KiB
C#

using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Policy.Exceptions.Models;
using Xunit;
namespace StellaOps.Policy.Exceptions.Tests;
/// <summary>
/// Unit tests for ExceptionObject domain model.
/// </summary>
public sealed class ExceptionObjectTests
{
[Fact]
public void ExceptionObject_WithValidScope_ShouldBeValid()
{
// Arrange & Act
var scope = new ExceptionScope
{
VulnerabilityId = "CVE-2024-12345"
};
// Assert
scope.IsValid.Should().BeTrue();
}
[Fact]
public void ExceptionScope_WithNoConstraints_ShouldBeInvalid()
{
// Arrange & Act
var scope = new ExceptionScope();
// Assert
scope.IsValid.Should().BeFalse();
}
[Fact]
public void ExceptionScope_WithArtifactDigest_ShouldBeValid()
{
// Arrange & Act
var scope = new ExceptionScope
{
ArtifactDigest = "sha256:abc123def456"
};
// Assert
scope.IsValid.Should().BeTrue();
}
[Fact]
public void ExceptionScope_WithPurlPattern_ShouldBeValid()
{
// Arrange & Act
var scope = new ExceptionScope
{
PurlPattern = "pkg:npm/lodash@*"
};
// Assert
scope.IsValid.Should().BeTrue();
}
[Fact]
public void ExceptionScope_WithPolicyRuleId_ShouldBeValid()
{
// Arrange & Act
var scope = new ExceptionScope
{
PolicyRuleId = "no-root-containers"
};
// Assert
scope.IsValid.Should().BeTrue();
}
[Fact]
public void ExceptionObject_IsEffective_WhenActiveAndNotExpired_ShouldBeTrue()
{
// Arrange
var exception = CreateException(
status: ExceptionStatus.Active,
expiresAt: DateTimeOffset.UtcNow.AddDays(30));
// Act & Assert
exception.IsEffective.Should().BeTrue();
exception.HasExpired.Should().BeFalse();
}
[Fact]
public void ExceptionObject_IsEffective_WhenActiveButExpired_ShouldBeFalse()
{
// Arrange
var exception = CreateException(
status: ExceptionStatus.Active,
expiresAt: DateTimeOffset.UtcNow.AddDays(-1));
// Act & Assert
exception.IsEffective.Should().BeFalse();
exception.HasExpired.Should().BeTrue();
}
[Fact]
public void ExceptionObject_IsEffective_WhenProposed_ShouldBeFalse()
{
// Arrange
var exception = CreateException(
status: ExceptionStatus.Proposed,
expiresAt: DateTimeOffset.UtcNow.AddDays(30));
// Act & Assert
exception.IsEffective.Should().BeFalse();
}
[Fact]
public void ExceptionObject_IsEffective_WhenRevoked_ShouldBeFalse()
{
// Arrange
var exception = CreateException(
status: ExceptionStatus.Revoked,
expiresAt: DateTimeOffset.UtcNow.AddDays(30));
// Act & Assert
exception.IsEffective.Should().BeFalse();
}
[Fact]
public void ExceptionObject_IsEffective_WhenExpiredStatus_ShouldBeFalse()
{
// Arrange
var exception = CreateException(
status: ExceptionStatus.Expired,
expiresAt: DateTimeOffset.UtcNow.AddDays(-1));
// Act & Assert
exception.IsEffective.Should().BeFalse();
}
[Theory]
[InlineData(ExceptionStatus.Proposed)]
[InlineData(ExceptionStatus.Approved)]
[InlineData(ExceptionStatus.Active)]
[InlineData(ExceptionStatus.Expired)]
[InlineData(ExceptionStatus.Revoked)]
public void ExceptionStatus_AllValues_ShouldBeRecognized(ExceptionStatus status)
{
// Arrange
var exception = CreateException(status: status);
// Assert
exception.Status.Should().Be(status);
}
[Theory]
[InlineData(ExceptionType.Vulnerability)]
[InlineData(ExceptionType.Policy)]
[InlineData(ExceptionType.Unknown)]
[InlineData(ExceptionType.Component)]
public void ExceptionType_AllValues_ShouldBeRecognized(ExceptionType type)
{
// Arrange
var exception = CreateException(type: type);
// Assert
exception.Type.Should().Be(type);
}
[Theory]
[InlineData(ExceptionReason.FalsePositive)]
[InlineData(ExceptionReason.AcceptedRisk)]
[InlineData(ExceptionReason.CompensatingControl)]
[InlineData(ExceptionReason.TestOnly)]
[InlineData(ExceptionReason.VendorNotAffected)]
[InlineData(ExceptionReason.ScheduledFix)]
[InlineData(ExceptionReason.DeprecationInProgress)]
[InlineData(ExceptionReason.RuntimeMitigation)]
[InlineData(ExceptionReason.NetworkIsolation)]
[InlineData(ExceptionReason.Other)]
public void ExceptionReason_AllValues_ShouldBeRecognized(ExceptionReason reason)
{
// Arrange
var exception = CreateException(reason: reason);
// Assert
exception.ReasonCode.Should().Be(reason);
}
[Fact]
public void ExceptionObject_WithMultipleApprovers_ShouldStoreAll()
{
// Arrange
var approvers = ImmutableArray.Create("approver1", "approver2", "approver3");
var exception = CreateException(approverIds: approvers);
// Assert
exception.ApproverIds.Should().HaveCount(3);
exception.ApproverIds.Should().Contain(["approver1", "approver2", "approver3"]);
}
[Fact]
public void ExceptionObject_WithEvidenceRefs_ShouldStoreAll()
{
// Arrange
var evidenceRefs = ImmutableArray.Create(
"sha256:evidence1hash",
"sha256:evidence2hash");
var exception = CreateException(evidenceRefs: evidenceRefs);
// Assert
exception.EvidenceRefs.Should().HaveCount(2);
exception.EvidenceRefs.Should().Contain("sha256:evidence1hash");
}
[Fact]
public void ExceptionObject_IsBlockedByRecheck_WhenBlockTriggered_ShouldBeTrue()
{
// Arrange
var exception = CreateException(recheckResult: new RecheckEvaluationResult
{
IsTriggered = true,
TriggeredConditions = [],
RecommendedAction = RecheckAction.Block,
EvaluatedAt = DateTimeOffset.UtcNow
});
// Act & Assert
exception.IsBlockedByRecheck.Should().BeTrue();
exception.RequiresReapproval.Should().BeFalse();
}
[Fact]
public void ExceptionObject_RequiresReapproval_WhenReapprovalTriggered_ShouldBeTrue()
{
// Arrange
var exception = CreateException(recheckResult: new RecheckEvaluationResult
{
IsTriggered = true,
TriggeredConditions = [],
RecommendedAction = RecheckAction.RequireReapproval,
EvaluatedAt = DateTimeOffset.UtcNow
});
// Act & Assert
exception.RequiresReapproval.Should().BeTrue();
exception.IsBlockedByRecheck.Should().BeFalse();
}
[Fact]
public void ExceptionObject_WithMetadata_ShouldStoreKeyValuePairs()
{
// Arrange
var metadata = ImmutableDictionary<string, string>.Empty
.Add("team", "security")
.Add("priority", "high");
var exception = CreateException(metadata: metadata);
// Assert
exception.Metadata.Should().HaveCount(2);
exception.Metadata["team"].Should().Be("security");
exception.Metadata["priority"].Should().Be("high");
}
[Fact]
public void ExceptionScope_WithEnvironments_ShouldStoreAll()
{
// Arrange
var scope = new ExceptionScope
{
VulnerabilityId = "CVE-2024-12345",
Environments = ["prod", "staging", "dev"]
};
// Assert
scope.Environments.Should().HaveCount(3);
scope.Environments.Should().Contain(["prod", "staging", "dev"]);
}
[Fact]
public void ExceptionScope_WithTenantId_ShouldStoreValue()
{
// Arrange
var tenantId = Guid.NewGuid();
var scope = new ExceptionScope
{
VulnerabilityId = "CVE-2024-12345",
TenantId = tenantId
};
// Assert
scope.TenantId.Should().Be(tenantId);
}
#region Test Helpers
private static ExceptionObject CreateException(
ExceptionStatus status = ExceptionStatus.Active,
ExceptionType type = ExceptionType.Vulnerability,
ExceptionReason reason = ExceptionReason.AcceptedRisk,
DateTimeOffset? expiresAt = null,
ImmutableArray<string>? approverIds = null,
ImmutableArray<string>? evidenceRefs = null,
ImmutableDictionary<string, string>? metadata = null,
RecheckEvaluationResult? recheckResult = null)
{
return new ExceptionObject
{
ExceptionId = $"EXC-{Guid.NewGuid():N}",
Version = 1,
Status = status,
Type = type,
Scope = new ExceptionScope
{
VulnerabilityId = "CVE-2024-12345"
},
OwnerId = "owner@example.com",
RequesterId = "requester@example.com",
ApproverIds = approverIds ?? [],
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow,
ExpiresAt = expiresAt ?? DateTimeOffset.UtcNow.AddDays(30),
ReasonCode = reason,
Rationale = "This is a test rationale that meets the minimum character requirement of 50 characters.",
EvidenceRefs = evidenceRefs ?? [],
CompensatingControls = [],
Metadata = metadata ?? ImmutableDictionary<string, string>.Empty,
LastRecheckResult = recheckResult,
LastRecheckAt = recheckResult?.EvaluatedAt
};
}
#endregion
}