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:
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[]
|
||||
|
||||
Reference in New Issue
Block a user