nuget reorganization

This commit is contained in:
master
2025-11-18 23:45:25 +02:00
parent 77cee6a209
commit d3ecd7f8e6
7712 changed files with 13963 additions and 10007504 deletions

View File

@@ -1,47 +0,0 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.RawModels;
using Xunit;
namespace StellaOps.Concelier.Core.Tests.Linksets;
public sealed class AdvisoryLinksetNormalizationTests
{
[Fact]
public void FromRawLinksetWithConfidence_ExtractsNotesAsConflicts()
{
var linkset = new RawLinkset
{
PackageUrls = ImmutableArray.Create("pkg:npm/foo@1.0.0"),
Notes = new Dictionary<string, string>
{
{ "severity", "disagree" }
}
};
var (normalized, confidence, conflicts) = AdvisoryLinksetNormalization.FromRawLinksetWithConfidence(linkset, 0.8);
Assert.NotNull(normalized);
Assert.Equal(0.8, confidence);
Assert.Single(conflicts);
Assert.Equal("severity", conflicts[0].Field);
Assert.Equal("disagree", conflicts[0].Reason);
}
[Theory]
[InlineData(-1, 0)]
[InlineData(2, 1)]
[InlineData(double.NaN, null)]
public void FromRawLinksetWithConfidence_ClampsConfidence(double input, double? expected)
{
var linkset = new RawLinkset
{
PackageUrls = ImmutableArray<string>.Empty
};
var (_, confidence, _) = AdvisoryLinksetNormalization.FromRawLinksetWithConfidence(linkset, input);
Assert.Equal(expected, confidence);
}
}

View File

@@ -15,21 +15,24 @@ public sealed class AdvisoryLinksetQueryServiceTests
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),
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),
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),
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);
var firstPage = await service.QueryAsync(new AdvisoryLinksetQueryOptions("tenant", Limit: 2), CancellationToken.None);
Assert.Equal(2, firstPage.Linksets.Length);
Assert.True(firstPage.HasMore);
@@ -37,7 +40,7 @@ public sealed class AdvisoryLinksetQueryServiceTests
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);
var secondPage = await service.QueryAsync(new AdvisoryLinksetQueryOptions("tenant", Limit: 2, Cursor: firstPage.NextCursor), CancellationToken.None);
Assert.Single(secondPage.Linksets);
Assert.False(secondPage.HasMore);
@@ -53,7 +56,7 @@ public sealed class AdvisoryLinksetQueryServiceTests
await Assert.ThrowsAsync<FormatException>(async () =>
{
await service.QueryAsync(new AdvisoryLinksetQueryOptions("tenant", limit: 1, Cursor: "not-base64"), CancellationToken.None);
await service.QueryAsync(new AdvisoryLinksetQueryOptions("tenant", Limit: 1, Cursor: "not-base64"), CancellationToken.None);
});
}

View File

@@ -0,0 +1,100 @@
using System;
using System.Collections.Immutable;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using StellaOps.Concelier.Core.Observations;
using StellaOps.Concelier.Models.Observations;
using StellaOps.Concelier.RawModels;
using Xunit;
namespace StellaOps.Concelier.Core.Tests.Observations;
public sealed class AdvisoryObservationAggregationTests
{
[Fact]
public void BuildAggregateLinkset_AccumulatesScopesAndRelationships()
{
var rawLinkset = new RawLinkset
{
Scopes = ImmutableArray.Create("pkg:npm/foo", "os:debian"),
Relationships = ImmutableArray.Create(
new RawRelationship("depends_on", "pkg:npm/foo", "pkg:npm/bar"))
};
var observation = CreateObservation("obs-1", rawLinkset);
var method = typeof(AdvisoryObservationQueryService).GetMethod(
"BuildAggregateLinkset",
BindingFlags.NonPublic | BindingFlags.Static)!;
var aggregate = (AdvisoryObservationLinksetAggregate)method.Invoke(
null,
new object?[] { ImmutableArray.Create(observation) })!;
Assert.Equal(ImmutableArray.Create("os:debian", "pkg:npm/foo"), aggregate.Scopes);
Assert.Single(aggregate.Relationships);
Assert.Equal("depends_on", aggregate.Relationships[0].Type);
}
[Fact]
public void FromRawLinksetWithConfidence_AssignsLowerConfidenceWhenConflictsPresent()
{
var linkset = new RawLinkset
{
Notes = new Dictionary<string, string>
{
{ "severity", "disagree" }
}
};
var (normalized, confidence, conflicts) = AdvisoryLinksetNormalization.FromRawLinksetWithConfidence(linkset);
Assert.Equal(0.5, confidence);
Assert.Single(conflicts);
Assert.Null(normalized); // no purls supplied
}
[Fact]
public void BuildAggregateLinkset_EmptyInputReturnsEmptyArrays()
{
var method = typeof(AdvisoryObservationQueryService).GetMethod(
"BuildAggregateLinkset",
BindingFlags.NonPublic | BindingFlags.Static)!;
var aggregate = (AdvisoryObservationLinksetAggregate)method.Invoke(
null,
new object?[] { ImmutableArray<AdvisoryObservation>.Empty })!;
Assert.True(aggregate.Scopes.IsEmpty);
Assert.True(aggregate.Relationships.IsEmpty);
}
private static AdvisoryObservation CreateObservation(string id, RawLinkset rawLinkset)
{
var source = new AdvisoryObservationSource("vendor", "stream", "api");
var upstream = new AdvisoryObservationUpstream(
"adv-id",
null,
DateTimeOffset.UtcNow,
DateTimeOffset.UtcNow,
"sha256:abc",
new AdvisoryObservationSignature(false, null, null, null));
var content = new AdvisoryObservationContent("json", null, JsonNode.Parse("{}")!);
var linkset = new AdvisoryObservationLinkset(
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<AdvisoryObservationReference>());
return new AdvisoryObservation(
id,
"tenant",
source,
upstream,
content,
linkset,
rawLinkset,
DateTimeOffset.UtcNow);
}
}

View File

@@ -164,15 +164,69 @@ public sealed class AdvisoryRawServiceTests
var guard = aocGuard ?? new AocWriteGuard();
var resolvedWriteGuard = writeGuard ?? new NoOpWriteGuard();
var linksetMapper = new PassthroughLinksetMapper();
var observationFactory = new StubObservationFactory();
var observationSink = new NullObservationSink();
var linksetSink = new NullLinksetSink();
return new AdvisoryRawService(
repository,
resolvedWriteGuard,
guard,
linksetMapper,
observationFactory,
observationSink,
linksetSink,
TimeProvider.System,
NullLogger<AdvisoryRawService>.Instance);
}
private sealed class NullObservationSink : IAdvisoryObservationSink
{
public Task UpsertAsync(Models.Observations.AdvisoryObservation observation, CancellationToken cancellationToken)
=> Task.CompletedTask;
}
private sealed class NullLinksetSink : IAdvisoryLinksetSink
{
public Task UpsertAsync(AdvisoryLinkset linkset, CancellationToken cancellationToken)
=> Task.CompletedTask;
}
private sealed class StubObservationFactory : IAdvisoryObservationFactory
{
public Models.Observations.AdvisoryObservation Create(Models.Advisory advisory, string tenant, string source, RawModels.AdvisoryRawDocument raw, string advisoryKey, string observationId, DateTimeOffset createdAt)
{
var upstream = new Models.Observations.AdvisoryObservationUpstream(
upstreamId: raw.Upstream.UpstreamId,
documentVersion: raw.Upstream.DocumentVersion,
fetchedAt: raw.Upstream.RetrievedAt ?? createdAt,
receivedAt: createdAt,
contentHash: raw.Upstream.ContentHash,
signature: new Models.Observations.AdvisoryObservationSignature(raw.Upstream.Signature.Present, raw.Upstream.Signature.Format, raw.Upstream.Signature.KeyId, raw.Upstream.Signature.Signature),
metadata: raw.Upstream.Provenance);
var content = new Models.Observations.AdvisoryObservationContent(raw.Content.Format, raw.Content.SpecVersion, JsonDocument.Parse(raw.Content.Raw.GetRawText()).RootElement);
var linkset = new Models.Observations.AdvisoryObservationLinkset(
raw.Linkset.Aliases,
raw.Linkset.PackageUrls,
raw.Linkset.Cpes,
ImmutableArray<Models.Observations.AdvisoryObservationReference>.Empty);
var rawLinkset = raw.Linkset;
return new Models.Observations.AdvisoryObservation(
observationId,
tenant,
new Models.Observations.AdvisoryObservationSource(source, "stream", "api"),
upstream,
content,
linkset,
rawLinkset,
createdAt);
}
}
private static AdvisoryRawDocument CreateDocument()
{
using var raw = JsonDocument.Parse("""{"id":"demo"}""");

View File

@@ -20,13 +20,14 @@ public sealed class EnsureAdvisoryLinksetsTenantLowerMigrationTests : IClassFixt
[Fact]
public async Task ApplyAsync_LowersTenantIds()
{
var collection = _fixture.Database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.AdvisoryLinksets);
await _fixture.Database.DropCollectionAsync(MongoStorageDefaults.Collections.AdvisoryLinksets);
var collection = _fixture.Database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.AdvisoryLinksets);
await collection.InsertManyAsync(new[]
{
new BsonDocument { { "TenantId", "Tenant-A" }, { "Source", "src" }, { "AdvisoryId", "ADV-1" }, { "Observations", new BsonArray() } },
new BsonDocument { { "TenantId", "tenant-b" }, { "Source", "src" }, { "AdvisoryId", "ADV-2" }, { "Observations", new BsonArray() } }
new BsonDocument { { "TenantId", "tenant-b" }, { "Source", "src" }, { "AdvisoryId", "ADV-2" }, { "Observations", new BsonArray() } },
new BsonDocument { { "Source", "src" }, { "AdvisoryId", "ADV-3" }, { "Observations", new BsonArray() } } // missing tenant should be ignored
});
var migration = new EnsureAdvisoryLinksetsTenantLowerMigration();

View File

@@ -4,6 +4,12 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<CollectCoverage>false</CollectCoverage>
<RunAnalyzers>false</RunAnalyzers>
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
<UseSharedCompilation>false</UseSharedCompilation>
<CopyBuildOutputToOutputDirectory>true</CopyBuildOutputToOutputDirectory>
<CopyOutputSymbolsToOutputDirectory>true</CopyOutputSymbolsToOutputDirectory>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Core/StellaOps.Concelier.Core.csproj" />

View File

@@ -69,6 +69,7 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
public Task InitializeAsync()
{
PrepareMongoEnvironment();
_runner = MongoDbRunner.Start(singleNodeReplSet: true);
_factory = new ConcelierApplicationFactory(_runner.ConnectionString);
WarmupFactory(_factory);
@@ -145,6 +146,12 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
Assert.Equal("https://example.test/advisory-1", references[0].GetProperty("url").GetString());
Assert.Equal("patch", references[1].GetProperty("type").GetString());
var confidence = linkset.GetProperty("confidence").GetDouble();
Assert.Equal(1.0, confidence);
var conflicts = linkset.GetProperty("conflicts").EnumerateArray().ToArray();
Assert.Empty(conflicts);
Assert.False(root.GetProperty("hasMore").GetBoolean());
Assert.True(root.GetProperty("nextCursor").ValueKind == JsonValueKind.Null);
}
@@ -2500,20 +2507,92 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
{
_output.WriteLine($"[PROGRAM LOG] {entry.Level}: {entry.Message}");
}
}
}
private static void WarmupFactory(WebApplicationFactory<Program> factory)
{
using var client = factory.CreateClient();
}
private static void WarmupFactory(WebApplicationFactory<Program> factory)
{
using var client = factory.CreateClient();
}
/// <summary>
/// Ensure Mongo2Go can start without external downloads by pointing it to cached binaries and OpenSSL 1.1 libs shipped in repo.
/// </summary>
private static void PrepareMongoEnvironment()
{
var repoRoot = FindRepoRoot();
if (repoRoot is null)
{
return;
}
var cacheDir = Path.Combine(repoRoot, ".cache", "mongodb-local");
Directory.CreateDirectory(cacheDir);
Environment.SetEnvironmentVariable("MONGO2GO_CACHE_LOCATION", cacheDir);
Environment.SetEnvironmentVariable("MONGO2GO_DOWNLOADS", cacheDir);
Environment.SetEnvironmentVariable("MONGO2GO_MONGODB_VERSION", "4.4.4");
var opensslPath = Path.Combine(repoRoot, "tests", "native", "openssl-1.1", "linux-x64");
if (Directory.Exists(opensslPath))
{
// Prepend OpenSSL 1.1 path so Mongo2Go binaries find libssl/libcrypto.
var existing = Environment.GetEnvironmentVariable("LD_LIBRARY_PATH");
var combined = string.IsNullOrEmpty(existing) ? opensslPath : $"{opensslPath}:{existing}";
Environment.SetEnvironmentVariable("LD_LIBRARY_PATH", combined);
}
// Also drop the OpenSSL libs next to the mongod binary Mongo2Go will spawn, in case LD_LIBRARY_PATH is ignored.
var mongoBin = Directory.Exists(Path.Combine(repoRoot, ".nuget"))
? Directory.GetFiles(Path.Combine(repoRoot, ".nuget", "packages", "mongo2go"), "mongod", SearchOption.AllDirectories)
.FirstOrDefault(path => path.Contains("mongodb-linux-4.4.4", StringComparison.OrdinalIgnoreCase))
: null;
if (mongoBin is not null && File.Exists(mongoBin) && Directory.Exists(opensslPath))
{
var binDir = Path.GetDirectoryName(mongoBin)!;
foreach (var libName in new[] { "libssl.so.1.1", "libcrypto.so.1.1" })
{
var target = Path.Combine(binDir, libName);
var source = Path.Combine(opensslPath, libName);
if (File.Exists(source) && !File.Exists(target))
{
File.Copy(source, target);
}
}
}
}
private static string? FindRepoRoot()
{
var current = AppContext.BaseDirectory;
while (!string.IsNullOrEmpty(current))
{
if (File.Exists(Path.Combine(current, "Directory.Build.props")))
{
return current;
}
var parent = Directory.GetParent(current);
if (parent is null)
{
break;
}
current = parent.FullName;
}
return null;
}
private static AdvisoryIngestRequest BuildAdvisoryIngestRequest(
string? contentHash,
string upstreamId,
bool enforceContentHash = true)
bool enforceContentHash = true,
IReadOnlyList<string>? purls = null,
IReadOnlyList<string>? notes = null)
{
var raw = CreateJsonElement($@"{{""id"":""{upstreamId}"",""modified"":""{DefaultIngestTimestamp:O}""}}");
var normalizedContentHash = NormalizeContentHash(contentHash, raw, enforceContentHash);
var resolvedPurls = purls ?? new[] { "pkg:npm/demo@1.0.0" };
var resolvedNotes = notes ?? Array.Empty<string>();
var references = new[]
{
new AdvisoryLinksetReferenceRequest("advisory", $"https://example.test/advisories/{upstreamId}", null)
@@ -2534,11 +2613,12 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
new[] { upstreamId, $"{upstreamId}-ALIAS" }),
new AdvisoryLinksetRequest(
new[] { upstreamId },
new[] { "pkg:npm/demo@1.0.0" },
resolvedPurls,
Array.Empty<AdvisoryLinksetRelationshipRequest>(),
Array.Empty<string>(),
Array.Empty<string>(),
references,
Array.Empty<string>(),
Array.Empty<string>(),
resolvedNotes,
new Dictionary<string, string> { ["note"] = "ingest-test" }));
}