Add unit tests and implementations for MongoDB index models and OpenAPI metadata

- Implemented `MongoIndexModelTests` to verify index models for various stores.
- Created `OpenApiMetadataFactory` with methods to generate OpenAPI metadata.
- Added tests for `OpenApiMetadataFactory` to ensure expected defaults and URL overrides.
- Introduced `ObserverSurfaceSecrets` and `WebhookSurfaceSecrets` for managing secrets.
- Developed `RuntimeSurfaceFsClient` and `WebhookSurfaceFsClient` for manifest retrieval.
- Added dependency injection tests for `SurfaceEnvironmentRegistration` in both Observer and Webhook contexts.
- Implemented tests for secret resolution in `ObserverSurfaceSecretsTests` and `WebhookSurfaceSecretsTests`.
- Created `EnsureLinkNotMergeCollectionsMigrationTests` to validate MongoDB migration logic.
- Added project files for MongoDB tests and NuGet package mirroring.
This commit is contained in:
master
2025-11-17 21:21:56 +02:00
parent d3128aec24
commit 9075bad2d9
146 changed files with 152183 additions and 82 deletions

View File

@@ -0,0 +1,94 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using StellaOps.Concelier.Core.Linksets;
using Xunit;
namespace StellaOps.Concelier.Core.Tests.Linksets;
public sealed class AdvisoryLinksetQueryServiceTests
{
[Fact]
public async Task QueryAsync_ReturnsPagedResults_WithCursor()
{
var linksets = new List<AdvisoryLinkset>
{
new("tenant", "ghsa", "adv-003",
ImmutableArray.Create("obs-003"),
new AdvisoryLinksetNormalized(new[]{"pkg:npm/a"}, new[]{"1.0.0"}, null, null),
null, DateTimeOffset.Parse("2025-11-10T12:00:00Z"), null),
new("tenant", "ghsa", "adv-002",
ImmutableArray.Create("obs-002"),
new AdvisoryLinksetNormalized(new[]{"pkg:npm/b"}, new[]{"2.0.0"}, null, null),
null, DateTimeOffset.Parse("2025-11-09T12:00:00Z"), null),
new("tenant", "ghsa", "adv-001",
ImmutableArray.Create("obs-001"),
new AdvisoryLinksetNormalized(new[]{"pkg:npm/c"}, new[]{"3.0.0"}, null, null),
null, DateTimeOffset.Parse("2025-11-08T12:00:00Z"), null),
};
var lookup = new FakeLinksetLookup(linksets);
var service = new AdvisoryLinksetQueryService(lookup);
var firstPage = await service.QueryAsync(new AdvisoryLinksetQueryOptions("tenant", limit: 2), CancellationToken.None);
Assert.Equal(2, firstPage.Linksets.Length);
Assert.True(firstPage.HasMore);
Assert.False(string.IsNullOrWhiteSpace(firstPage.NextCursor));
Assert.Equal("adv-003", firstPage.Linksets[0].AdvisoryId);
Assert.Equal("pkg:npm/a", firstPage.Linksets[0].Normalized?.Purls?.First());
var secondPage = await service.QueryAsync(new AdvisoryLinksetQueryOptions("tenant", limit: 2, Cursor: firstPage.NextCursor), CancellationToken.None);
Assert.Single(secondPage.Linksets);
Assert.False(secondPage.HasMore);
Assert.Null(secondPage.NextCursor);
Assert.Equal("adv-001", secondPage.Linksets[0].AdvisoryId);
}
[Fact]
public async Task QueryAsync_InvalidCursor_ThrowsFormatException()
{
var lookup = new FakeLinksetLookup(Array.Empty<AdvisoryLinkset>());
var service = new AdvisoryLinksetQueryService(lookup);
await Assert.ThrowsAsync<FormatException>(async () =>
{
await service.QueryAsync(new AdvisoryLinksetQueryOptions("tenant", limit: 1, Cursor: "not-base64"), CancellationToken.None);
});
}
private sealed class FakeLinksetLookup : IAdvisoryLinksetLookup
{
private readonly IReadOnlyList<AdvisoryLinkset> _linksets;
public FakeLinksetLookup(IReadOnlyList<AdvisoryLinkset> linksets)
{
_linksets = linksets;
}
public Task<IReadOnlyList<AdvisoryLinkset>> FindByTenantAsync(
string tenantId,
IEnumerable<string>? advisoryIds,
IEnumerable<string>? sources,
AdvisoryLinksetCursor? cursor,
int limit,
CancellationToken cancellationToken)
{
var ordered = _linksets
.Where(ls => ls.TenantId == tenantId)
.OrderByDescending(ls => ls.CreatedAt)
.ThenBy(ls => ls.AdvisoryId, StringComparer.Ordinal)
.ToList();
if (cursor is not null)
{
ordered = ordered
.Where(ls => ls.CreatedAt < cursor.CreatedAt ||
(ls.CreatedAt == cursor.CreatedAt && string.Compare(ls.AdvisoryId, cursor.AdvisoryId, StringComparison.Ordinal) > 0))
.ToList();
}
return Task.FromResult<IReadOnlyList<AdvisoryLinkset>>(ordered.Take(limit).ToList());
}
}
}

View File

@@ -205,6 +205,104 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
Assert.Equal("tenant-a:nvd:alpha:1", secondObservations[0].GetProperty("observationId").GetString());
}
[Fact]
public async Task LinksetsEndpoint_ReturnsNormalizedLinksetsFromIngestion()
{
var tenant = "tenant-linkset-ingest";
using var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-Stella-Tenant", tenant);
var firstIngest = await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest("sha256:linkset-1", "GHSA-LINK-001", purls: new[] { "pkg:npm/demo@1.0.0" }));
firstIngest.EnsureSuccessStatusCode();
var secondIngest = await client.PostAsJsonAsync("/ingest/advisory", BuildAdvisoryIngestRequest("sha256:linkset-2", "GHSA-LINK-002", purls: new[] { "pkg:npm/demo@2.0.0" }));
secondIngest.EnsureSuccessStatusCode();
var response = await client.GetAsync("/linksets?tenant=tenant-linkset-ingest&limit=10");
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<AdvisoryLinksetQueryResponse>();
Assert.NotNull(payload);
Assert.Equal(2, payload!.Linksets.Length);
var linksetAdvisoryIds = payload.Linksets.Select(ls => ls.AdvisoryId).OrderBy(id => id, StringComparer.Ordinal).ToArray();
Assert.Equal(new[] { "GHSA-LINK-001", "GHSA-LINK-002" }, linksetAdvisoryIds);
var allPurls = payload.Linksets.SelectMany(ls => ls.Purls).OrderBy(p => p, StringComparer.Ordinal).ToArray();
Assert.Contains("pkg:npm/demo@1.0.0", allPurls);
Assert.Contains("pkg:npm/demo@2.0.0", allPurls);
var versions = payload.Linksets
.SelectMany(ls => ls.Versions)
.Distinct(StringComparer.Ordinal)
.OrderBy(v => v, StringComparer.Ordinal)
.ToArray();
Assert.Contains("1.0.0", versions);
Assert.Contains("2.0.0", versions);
Assert.False(payload.HasMore);
Assert.True(string.IsNullOrEmpty(payload.NextCursor));
}
[Fact]
public async Task LinksetsEndpoint_SupportsCursorPagination()
{
var tenant = "tenant-linkset-page";
var documents = new[]
{
CreateLinksetDocument(
tenant,
"nvd",
"ADV-002",
new[] { "obs-2" },
new[] { "pkg:npm/demo@2.0.0" },
new[] { "2.0.0" },
new DateTime(2025, 1, 6, 0, 0, 0, DateTimeKind.Utc)),
CreateLinksetDocument(
tenant,
"osv",
"ADV-001",
new[] { "obs-1" },
new[] { "pkg:npm/demo@1.0.0" },
new[] { "1.0.0" },
new DateTime(2025, 1, 5, 0, 0, 0, DateTimeKind.Utc)),
CreateLinksetDocument(
"tenant-other",
"osv",
"ADV-999",
new[] { "obs-x" },
new[] { "pkg:npm/other@1.0.0" },
new[] { "1.0.0" },
new DateTime(2025, 1, 4, 0, 0, 0, DateTimeKind.Utc))
};
await SeedLinksetDocumentsAsync(documents);
using var client = _factory.CreateClient();
var firstResponse = await client.GetAsync($"/linksets?tenant={tenant}&limit=1");
firstResponse.EnsureSuccessStatusCode();
var firstPayload = await firstResponse.Content.ReadFromJsonAsync<AdvisoryLinksetQueryResponse>();
Assert.NotNull(firstPayload);
var first = Assert.Single(firstPayload!.Linksets);
Assert.Equal("ADV-002", first.AdvisoryId);
Assert.Equal(new[] { "pkg:npm/demo@2.0.0" }, first.Purls.ToArray());
Assert.Equal(new[] { "2.0.0" }, first.Versions.ToArray());
Assert.True(firstPayload.HasMore);
Assert.False(string.IsNullOrWhiteSpace(firstPayload.NextCursor));
var secondResponse = await client.GetAsync($"/linksets?tenant={tenant}&limit=1&cursor={Uri.EscapeDataString(firstPayload.NextCursor!)}");
secondResponse.EnsureSuccessStatusCode();
var secondPayload = await secondResponse.Content.ReadFromJsonAsync<AdvisoryLinksetQueryResponse>();
Assert.NotNull(secondPayload);
var second = Assert.Single(secondPayload!.Linksets);
Assert.Equal("ADV-001", second.AdvisoryId);
Assert.Equal(new[] { "pkg:npm/demo@1.0.0" }, second.Purls.ToArray());
Assert.Equal(new[] { "1.0.0" }, second.Versions.ToArray());
Assert.False(secondPayload.HasMore);
Assert.True(string.IsNullOrEmpty(secondPayload.NextCursor));
}
[Fact]
public async Task ObservationsEndpoint_ReturnsBadRequestWhenTenantMissing()
{
@@ -1505,6 +1603,52 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
await SeedAdvisoryRawDocumentsAsync(rawDocuments);
}
private async Task SeedLinksetDocumentsAsync(IEnumerable<AdvisoryLinksetDocument> documents)
{
var client = new MongoClient(_runner.ConnectionString);
var database = client.GetDatabase(MongoStorageDefaults.DefaultDatabaseName);
var collection = database.GetCollection<AdvisoryLinksetDocument>(MongoStorageDefaults.Collections.AdvisoryLinksets);
try
{
await database.DropCollectionAsync(MongoStorageDefaults.Collections.AdvisoryLinksets);
}
catch (MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase))
{
// Collection not created yet; safe to ignore.
}
var snapshot = documents?.ToArray() ?? Array.Empty<AdvisoryLinksetDocument>();
if (snapshot.Length > 0)
{
await collection.InsertManyAsync(snapshot);
}
}
private static AdvisoryLinksetDocument CreateLinksetDocument(
string tenant,
string source,
string advisoryId,
IEnumerable<string> observationIds,
IEnumerable<string> purls,
IEnumerable<string> versions,
DateTime createdAtUtc)
{
return new AdvisoryLinksetDocument
{
TenantId = tenant,
Source = source,
AdvisoryId = advisoryId,
Observations = observationIds.ToList(),
CreatedAt = DateTime.SpecifyKind(createdAtUtc, DateTimeKind.Utc),
Normalized = new AdvisoryLinksetNormalizedDocument
{
Purls = purls.ToList(),
Versions = versions.ToList()
}
};
}
private static AdvisoryObservationDocument[] BuildSampleObservationDocuments()
{
return new[]