audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories

This commit is contained in:
master
2026-01-07 18:49:59 +02:00
parent 04ec098046
commit 608a7f85c0
866 changed files with 56323 additions and 6231 deletions

View File

@@ -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:");
}

View File

@@ -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();

View File

@@ -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).

View File

@@ -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>

View File

@@ -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"
};
}
}