audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories
This commit is contained in:
@@ -37,7 +37,7 @@ public sealed class ProvenanceHintBuilderTests
|
||||
hint.Evidence.BuildId.Should().NotBeNull();
|
||||
hint.Evidence.BuildId!.BuildId.Should().Be("abc123");
|
||||
hint.Evidence.BuildId.MatchedPackage.Should().Be("openssl");
|
||||
hint.SuggestedActions.Should().HaveCountGreaterOrEqualTo(1);
|
||||
hint.SuggestedActions.Should().HaveCountGreaterThanOrEqualTo(1);
|
||||
hint.SuggestedActions[0].Action.Should().Be("verify_build_id");
|
||||
hint.HintId.Should().StartWith("hint:sha256:");
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ public sealed class ProvenanceHintSerializationTests
|
||||
|
||||
// Assert
|
||||
deserialized.Should().NotBeNull();
|
||||
deserialized!.SuggestedActions.Should().HaveCountGreaterOrEqualTo(1);
|
||||
deserialized!.SuggestedActions.Should().HaveCountGreaterThanOrEqualTo(1);
|
||||
deserialized.SuggestedActions[0].Action.Should().NotBeNullOrEmpty();
|
||||
deserialized.SuggestedActions[0].Priority.Should().BeGreaterThan(0);
|
||||
deserialized.SuggestedActions[0].Effort.Should().NotBeNullOrEmpty();
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# AGENTS - Unknowns WebService Tests
|
||||
|
||||
## Roles
|
||||
- QA / test engineer: endpoint coverage and deterministic fixtures.
|
||||
- Backend engineer: align API contract and test helpers.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/unknowns/architecture.md
|
||||
- src/Unknowns/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/Unknowns/__Tests/StellaOps.Unknowns.WebService.Tests
|
||||
- Test target: src/Unknowns/StellaOps.Unknowns.WebService
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Use fixed timestamps and deterministic IDs in test fixtures.
|
||||
- Validate tenant handling and reject missing headers.
|
||||
|
||||
## Testing
|
||||
- Cover auth/tenant enforcement, paging bounds, asOf queries, and history endpoint behavior.
|
||||
- Include negative cases for invalid inputs (minConfidence, take/skip).
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
<PackageReference Include="coverlet.collector">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Unknowns.WebService\StellaOps.Unknowns.WebService.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,360 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// UnknownsEndpointsTests.cs
|
||||
// Sprint: SPRINT_20260106_001_005_UNKNOWNS_provenance_hints
|
||||
// Task: WS-007 - Add integration tests for WebService endpoints
|
||||
// Description: Integration tests for Unknowns WebService endpoints
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Unknowns.Core.Models;
|
||||
using StellaOps.Unknowns.Core.Repositories;
|
||||
using StellaOps.Unknowns.WebService.Endpoints;
|
||||
using Xunit;
|
||||
using NSubstitute;
|
||||
|
||||
namespace StellaOps.Unknowns.WebService.Tests;
|
||||
|
||||
public sealed class UnknownsEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly IUnknownRepository _mockRepository;
|
||||
|
||||
public UnknownsEndpointsTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_mockRepository = Substitute.For<IUnknownRepository>();
|
||||
|
||||
_factory = factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// Remove existing repository registration
|
||||
var descriptor = services.SingleOrDefault(
|
||||
d => d.ServiceType == typeof(IUnknownRepository));
|
||||
if (descriptor != null)
|
||||
{
|
||||
services.Remove(descriptor);
|
||||
}
|
||||
|
||||
// Add mock repository
|
||||
services.AddSingleton(_mockRepository);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task ListUnknowns_ReturnsEmptyList_WhenNoUnknowns()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
_mockRepository.GetOpenUnknownsAsync(tenantId, Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns([]);
|
||||
_mockRepository.CountOpenAsync(tenantId, Arg.Any<CancellationToken>())
|
||||
.Returns(0);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/unknowns");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownsListResponse>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result.Items);
|
||||
Assert.Equal(0, result.Total);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task GetUnknownById_ReturnsUnknown_WhenExists()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
var unknownId = Guid.NewGuid();
|
||||
var unknown = CreateTestUnknown(unknownId, tenantId);
|
||||
|
||||
_mockRepository.GetByIdAsync(tenantId, unknownId, Arg.Any<CancellationToken>())
|
||||
.Returns(unknown);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync($"/api/unknowns/{unknownId}");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownDto>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(unknownId, result.Id);
|
||||
Assert.Equal("AmbiguousPackage", result.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task GetUnknownById_ReturnsNotFound_WhenDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
var unknownId = Guid.NewGuid();
|
||||
|
||||
_mockRepository.GetByIdAsync(tenantId, unknownId, Arg.Any<CancellationToken>())
|
||||
.Returns((Unknown?)null);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync($"/api/unknowns/{unknownId}");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task GetUnknownHints_ReturnsHints_WhenUnknownHasHints()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
var unknownId = Guid.NewGuid();
|
||||
var unknown = CreateTestUnknownWithHints(unknownId, tenantId);
|
||||
|
||||
_mockRepository.GetByIdAsync(tenantId, unknownId, Arg.Any<CancellationToken>())
|
||||
.Returns(unknown);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync($"/api/unknowns/{unknownId}/hints");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ProvenanceHintsResponse>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(unknownId, result.UnknownId);
|
||||
Assert.NotEmpty(result.Hints);
|
||||
Assert.Equal("Likely debian:bookworm backport", result.BestHypothesis);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task GetByTriageBand_ReturnsHotUnknowns()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
var unknowns = new List<Unknown>
|
||||
{
|
||||
CreateTestUnknown(Guid.NewGuid(), tenantId, TriageBand.Hot),
|
||||
CreateTestUnknown(Guid.NewGuid(), tenantId, TriageBand.Hot)
|
||||
};
|
||||
|
||||
_mockRepository.GetByTriageBandAsync(tenantId, TriageBand.Hot, Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(unknowns);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/unknowns/triage/Hot");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownsListResponse>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.Items.Count);
|
||||
Assert.All(result.Items, item => Assert.Equal("Hot", item.TriageBand));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task GetHotQueue_ReturnsHotUnknowns()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
var unknowns = new List<Unknown>
|
||||
{
|
||||
CreateTestUnknown(Guid.NewGuid(), tenantId, TriageBand.Hot)
|
||||
};
|
||||
|
||||
_mockRepository.GetHotQueueAsync(tenantId, Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(unknowns);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/unknowns/hot-queue");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownsListResponse>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result.Items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task GetHighConfidenceHints_ReturnsFilteredResults()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
var unknowns = new List<Unknown>
|
||||
{
|
||||
CreateTestUnknownWithHints(Guid.NewGuid(), tenantId)
|
||||
};
|
||||
|
||||
_mockRepository.GetWithHighConfidenceHintsAsync(tenantId, 0.7, Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(unknowns);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/unknowns/high-confidence?minConfidence=0.7");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownsListResponse>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result.Items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task GetSummary_ReturnsSummaryStatistics()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
|
||||
_mockRepository.CountByKindAsync(tenantId, Arg.Any<CancellationToken>())
|
||||
.Returns(new Dictionary<UnknownKind, long>
|
||||
{
|
||||
{ UnknownKind.AmbiguousPackage, 5 },
|
||||
{ UnknownKind.MissingSbom, 3 }
|
||||
});
|
||||
|
||||
_mockRepository.CountBySeverityAsync(tenantId, Arg.Any<CancellationToken>())
|
||||
.Returns(new Dictionary<UnknownSeverity, long>
|
||||
{
|
||||
{ UnknownSeverity.High, 2 },
|
||||
{ UnknownSeverity.Medium, 6 }
|
||||
});
|
||||
|
||||
_mockRepository.CountByTriageBandAsync(tenantId, Arg.Any<CancellationToken>())
|
||||
.Returns(new Dictionary<TriageBand, long>
|
||||
{
|
||||
{ TriageBand.Hot, 3 },
|
||||
{ TriageBand.Warm, 5 }
|
||||
});
|
||||
|
||||
_mockRepository.CountOpenAsync(tenantId, Arg.Any<CancellationToken>())
|
||||
.Returns(8);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/unknowns/summary");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownsSummaryResponse>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(8, result.TotalOpen);
|
||||
Assert.Equal(5, result.ByKind["AmbiguousPackage"]);
|
||||
Assert.Equal(3, result.ByTriageBand["Hot"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task HealthCheck_ReturnsHealthy_WhenDatabaseIsAvailable()
|
||||
{
|
||||
// Arrange
|
||||
_mockRepository.GetOpenUnknownsAsync(Arg.Any<string>(), 1, Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns([]);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/health");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private static Unknown CreateTestUnknown(
|
||||
Guid id,
|
||||
string tenantId,
|
||||
TriageBand band = TriageBand.Warm)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
return new Unknown
|
||||
{
|
||||
Id = id,
|
||||
TenantId = tenantId,
|
||||
SubjectHash = $"sha256:{Guid.NewGuid():N}",
|
||||
SubjectType = UnknownSubjectType.Package,
|
||||
SubjectRef = "pkg:npm/lodash@4.17.21",
|
||||
Kind = UnknownKind.AmbiguousPackage,
|
||||
Severity = UnknownSeverity.Medium,
|
||||
ValidFrom = now.AddDays(-7),
|
||||
SysFrom = now.AddDays(-7),
|
||||
CompositeScore = band == TriageBand.Hot ? 0.85 : 0.55,
|
||||
TriageBand = band,
|
||||
CreatedAt = now.AddDays(-7),
|
||||
CreatedBy = "system",
|
||||
ProvenanceHints = []
|
||||
};
|
||||
}
|
||||
|
||||
private static Unknown CreateTestUnknownWithHints(Guid id, string tenantId)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
return new Unknown
|
||||
{
|
||||
Id = id,
|
||||
TenantId = tenantId,
|
||||
SubjectHash = $"sha256:{Guid.NewGuid():N}",
|
||||
SubjectType = UnknownSubjectType.Binary,
|
||||
SubjectRef = "/usr/lib/x86_64-linux-gnu/libssl.so.3",
|
||||
Kind = UnknownKind.UnknownBuildId,
|
||||
Severity = UnknownSeverity.High,
|
||||
ValidFrom = now.AddDays(-3),
|
||||
SysFrom = now.AddDays(-3),
|
||||
CompositeScore = 0.72,
|
||||
TriageBand = TriageBand.Hot,
|
||||
CreatedAt = now.AddDays(-3),
|
||||
CreatedBy = "scanner",
|
||||
ProvenanceHints = [
|
||||
new ProvenanceHint
|
||||
{
|
||||
Id = $"hint:{Guid.NewGuid():N}",
|
||||
Type = ProvenanceHintType.BuildIdMatch,
|
||||
Confidence = 0.85,
|
||||
ConfidenceLevel = HintConfidence.High,
|
||||
Hypothesis = "Likely debian:bookworm backport",
|
||||
SuggestedActions = [
|
||||
new SuggestedAction
|
||||
{
|
||||
Action = "Verify debian package version",
|
||||
Priority = 1,
|
||||
Description = "Check against Debian security tracker"
|
||||
}
|
||||
],
|
||||
GeneratedAt = now.AddDays(-3)
|
||||
}
|
||||
],
|
||||
BestHypothesis = "Likely debian:bookworm backport",
|
||||
CombinedConfidence = 0.85,
|
||||
PrimarySuggestedAction = "Verify debian package version"
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user