up
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Findings.Ledger.Infrastructure.Attestation;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests;
|
||||
|
||||
public class AttestationStatusCalculatorTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0, 0, OverallVerificationStatus.NoAttestations)]
|
||||
[InlineData(3, 3, OverallVerificationStatus.AllVerified)]
|
||||
[InlineData(4, 1, OverallVerificationStatus.PartiallyVerified)]
|
||||
[InlineData(2, 0, OverallVerificationStatus.NoneVerified)]
|
||||
public void Compute_ReturnsExpectedStatus(int attestationCount, int verifiedCount, OverallVerificationStatus expected)
|
||||
{
|
||||
AttestationStatusCalculator.Compute(attestationCount, verifiedCount)
|
||||
.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using StellaOps.Findings.Ledger;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests;
|
||||
|
||||
public class DeprecationHeadersTests
|
||||
{
|
||||
[Fact]
|
||||
public void Apply_SetsStandardDeprecationHeaders()
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
DeprecationHeaders.Apply(context.Response, "ledger.export.findings");
|
||||
|
||||
context.Response.Headers["Deprecation"].ToString().Should().Be("true");
|
||||
context.Response.Headers["Sunset"].ToString().Should().Be(DeprecationHeaders.SunsetDate);
|
||||
context.Response.Headers["X-Deprecated-Endpoint"].ToString().Should().Be("ledger.export.findings");
|
||||
context.Response.Headers["Link"].ToString().Should().Contain("/.well-known/openapi");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Findings.Ledger.OpenApi;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests;
|
||||
|
||||
public class OpenApiMetadataFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void ComputeEtag_IsDeterministicAndWeak()
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes("spec-content");
|
||||
|
||||
var etag1 = OpenApiMetadataFactory.ComputeEtag(bytes);
|
||||
var etag2 = OpenApiMetadataFactory.ComputeEtag(bytes);
|
||||
|
||||
etag1.Should().StartWith("W/\"");
|
||||
etag1.Should().Be(etag2);
|
||||
etag1.Length.Should().BeGreaterThan(6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSpecPath_ResolvesExistingSpec()
|
||||
{
|
||||
var path = OpenApiMetadataFactory.GetSpecPath(AppContext.BaseDirectory);
|
||||
File.Exists(path).Should().BeTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Findings.Ledger.OpenApi;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests;
|
||||
|
||||
public class OpenApiSdkSurfaceTests
|
||||
{
|
||||
private readonly string _specContent;
|
||||
|
||||
public OpenApiSdkSurfaceTests()
|
||||
{
|
||||
var path = OpenApiMetadataFactory.GetSpecPath(AppContext.BaseDirectory);
|
||||
_specContent = File.ReadAllText(path, Encoding.UTF8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindingsEndpoints_ExposePaginationAndFilters()
|
||||
{
|
||||
_specContent.Should().Contain("/findings");
|
||||
_specContent.Should().Contain("page_token");
|
||||
_specContent.Should().MatchRegex("nextPageToken|next_page_token");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceSchemas_ExposeEvidenceLinks()
|
||||
{
|
||||
_specContent.Should().Contain("evidenceBundleRef");
|
||||
_specContent.Should().Contain("ExportProvenance");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttestationPointers_ExposeProvenanceMetadata()
|
||||
{
|
||||
_specContent.Should().Contain("/v1/ledger/attestations");
|
||||
_specContent.Should().Contain("attestation");
|
||||
_specContent.Should().Contain("provenance");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Findings.Ledger.Domain;
|
||||
using StellaOps.Findings.Ledger.Infrastructure;
|
||||
using StellaOps.Findings.Ledger.Infrastructure.Attestation;
|
||||
using StellaOps.Findings.Ledger.Services;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests;
|
||||
|
||||
public class ScoredFindingsQueryServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task QueryAsync_MapsAttestationMetadata()
|
||||
{
|
||||
var projection = new FindingProjection(
|
||||
TenantId: "tenant-a",
|
||||
FindingId: "finding-123",
|
||||
PolicyVersion: "v1",
|
||||
Status: "affected",
|
||||
Severity: 7.5m,
|
||||
RiskScore: 0.9m,
|
||||
RiskSeverity: "critical",
|
||||
RiskProfileVersion: "p1",
|
||||
RiskExplanationId: Guid.NewGuid(),
|
||||
RiskEventSequence: 42,
|
||||
Labels: new(),
|
||||
CurrentEventId: Guid.NewGuid(),
|
||||
ExplainRef: "explain-1",
|
||||
PolicyRationale: new(),
|
||||
UpdatedAt: DateTimeOffset.UtcNow,
|
||||
CycleHash: "abc123",
|
||||
AttestationCount: 3,
|
||||
VerifiedAttestationCount: 2,
|
||||
FailedAttestationCount: 1,
|
||||
UnverifiedAttestationCount: 0,
|
||||
AttestationStatus: OverallVerificationStatus.PartiallyVerified);
|
||||
|
||||
var repo = new FakeFindingProjectionRepository(projection);
|
||||
var service = new ScoredFindingsQueryService(
|
||||
repo,
|
||||
new NullRiskExplanationStore(),
|
||||
TimeProvider.System,
|
||||
NullLogger<ScoredFindingsQueryService>.Instance);
|
||||
|
||||
var result = await service.QueryAsync(new ScoredFindingsQuery
|
||||
{
|
||||
TenantId = "tenant-a",
|
||||
Limit = 10
|
||||
});
|
||||
|
||||
result.TotalCount.Should().Be(1);
|
||||
result.Findings.Should().HaveCount(1);
|
||||
|
||||
var finding = result.Findings.Single();
|
||||
finding.AttestationCount.Should().Be(3);
|
||||
finding.VerifiedAttestationCount.Should().Be(2);
|
||||
finding.FailedAttestationCount.Should().Be(1);
|
||||
finding.UnverifiedAttestationCount.Should().Be(0);
|
||||
finding.AttestationStatus.Should().Be(OverallVerificationStatus.PartiallyVerified);
|
||||
}
|
||||
|
||||
private sealed class FakeFindingProjectionRepository : IFindingProjectionRepository
|
||||
{
|
||||
private readonly FindingProjection _projection;
|
||||
|
||||
public FakeFindingProjectionRepository(FindingProjection projection)
|
||||
{
|
||||
_projection = projection;
|
||||
}
|
||||
|
||||
public Task<ProjectionCheckpoint> GetCheckpointAsync(CancellationToken cancellationToken) =>
|
||||
Task.FromResult(ProjectionCheckpoint.Initial(TimeProvider.System));
|
||||
|
||||
public Task<(IReadOnlyList<FindingProjection> Projections, int TotalCount)> QueryScoredAsync(
|
||||
ScoredFindingsQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
Task.FromResult((new List<FindingProjection> { _projection } as IReadOnlyList<FindingProjection>, 1));
|
||||
|
||||
public Task<FindingStatsResult> GetFindingStatsSinceAsync(string tenantId, DateTimeOffset since, CancellationToken cancellationToken) =>
|
||||
Task.FromResult(new FindingStatsResult(0, 0, 0, 0, 0, 0));
|
||||
|
||||
public Task<(int Total, int Scored, decimal AvgScore, decimal MaxScore)> GetRiskAggregatesAsync(string tenantId, string? policyVersion, CancellationToken cancellationToken) =>
|
||||
Task.FromResult((0, 0, 0m, 0m));
|
||||
|
||||
public Task<ScoreDistribution> GetScoreDistributionAsync(string tenantId, string? policyVersion, CancellationToken cancellationToken) =>
|
||||
Task.FromResult(new ScoreDistribution());
|
||||
|
||||
public Task<SeverityDistribution> GetSeverityDistributionAsync(string tenantId, string? policyVersion, CancellationToken cancellationToken) =>
|
||||
Task.FromResult(new SeverityDistribution());
|
||||
|
||||
public Task SaveCheckpointAsync(ProjectionCheckpoint checkpoint, CancellationToken cancellationToken) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
public Task InsertHistoryAsync(FindingHistoryEntry entry, CancellationToken cancellationToken) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
public Task InsertActionAsync(TriageActionEntry entry, CancellationToken cancellationToken) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
public Task<FindingProjection?> GetAsync(string tenantId, string findingId, string policyVersion, CancellationToken cancellationToken) =>
|
||||
Task.FromResult<FindingProjection?>(_projection);
|
||||
|
||||
public Task UpsertAsync(FindingProjection projection, CancellationToken cancellationToken) =>
|
||||
Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class NullRiskExplanationStore : IRiskExplanationStore
|
||||
{
|
||||
public Task<ScoredFindingExplanation?> GetAsync(string tenantId, string findingId, Guid? explanationId, CancellationToken cancellationToken) =>
|
||||
Task.FromResult<ScoredFindingExplanation?>(null);
|
||||
|
||||
public Task StoreAsync(string tenantId, ScoredFindingExplanation explanation, CancellationToken cancellationToken) =>
|
||||
Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user