fix tests. new product advisories enhancements
This commit is contained in:
@@ -15,13 +15,13 @@ namespace StellaOps.ReachGraph.WebService.Tests;
|
||||
/// Integration tests for the ReachGraph Store API.
|
||||
/// Uses in-memory providers for quick testing without containers.
|
||||
/// </summary>
|
||||
public class ReachGraphApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
public class ReachGraphApiIntegrationTests : IClassFixture<ReachGraphTestFactory>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private const string TenantHeader = "X-Tenant-ID";
|
||||
private const string TestTenant = "test-tenant";
|
||||
|
||||
public ReachGraphApiIntegrationTests(WebApplicationFactory<Program> factory)
|
||||
public ReachGraphApiIntegrationTests(ReachGraphTestFactory factory)
|
||||
{
|
||||
_client = factory.CreateClient();
|
||||
_client.DefaultRequestHeaders.Add(TenantHeader, TestTenant);
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
// Licensed to StellaOps under the BUSL-1.1 license.
|
||||
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.ReachGraph.Cache;
|
||||
using StellaOps.ReachGraph.Persistence;
|
||||
using StellaOps.ReachGraph.Schema;
|
||||
using StackExchange.Redis;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.ReachGraph.WebService.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Test factory for ReachGraph WebService integration tests.
|
||||
/// Provides in-memory implementations of PostgreSQL and Redis dependencies.
|
||||
/// </summary>
|
||||
public sealed class ReachGraphTestFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
// Must configure settings BEFORE services are built to avoid connection string exception
|
||||
builder.UseSetting("ConnectionStrings:PostgreSQL", "Host=localhost;Database=reachgraph_test;Username=test;Password=test");
|
||||
builder.UseSetting("ConnectionStrings:Redis", "localhost:6379,abortConnect=false");
|
||||
|
||||
builder.UseEnvironment("Development");
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// Remove real implementations that would try to connect
|
||||
var npgsqlDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(Npgsql.NpgsqlDataSource));
|
||||
if (npgsqlDescriptor != null) services.Remove(npgsqlDescriptor);
|
||||
|
||||
var redisDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IConnectionMultiplexer));
|
||||
if (redisDescriptor != null) services.Remove(redisDescriptor);
|
||||
|
||||
services.RemoveAll<IReachGraphRepository>();
|
||||
services.RemoveAll<IReachGraphCache>();
|
||||
|
||||
// Add in-memory implementations
|
||||
services.AddSingleton<IReachGraphRepository, InMemoryReachGraphRepository>();
|
||||
services.AddSingleton<IReachGraphCache, InMemoryReachGraphCache>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of <see cref="IReachGraphRepository"/> for testing.
|
||||
/// </summary>
|
||||
internal sealed class InMemoryReachGraphRepository : IReachGraphRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, (ReachGraphMinimal Graph, DateTimeOffset StoredAt)> _graphs = new();
|
||||
private readonly ConcurrentDictionary<string, List<string>> _byArtifact = new();
|
||||
private readonly ConcurrentDictionary<string, List<string>> _byCve = new();
|
||||
private readonly List<ReplayLogEntry> _replayLog = new();
|
||||
|
||||
public Task<StoreResult> StoreAsync(ReachGraphMinimal graph, string tenantId, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var digest = $"blake3:{Guid.NewGuid():N}";
|
||||
var key = MakeKey(tenantId, digest);
|
||||
var isNew = _graphs.TryAdd(key, (graph, DateTimeOffset.UtcNow));
|
||||
|
||||
// Track by artifact
|
||||
var artifactKey = MakeArtifactKey(tenantId, graph.Artifact.Digest);
|
||||
_byArtifact.AddOrUpdate(
|
||||
artifactKey,
|
||||
_ => [digest],
|
||||
(_, list) => { if (!list.Contains(digest)) list.Add(digest); return list; });
|
||||
|
||||
// Track by CVE
|
||||
if (graph.Scope.Cves is { Length: > 0 })
|
||||
{
|
||||
foreach (var cve in graph.Scope.Cves)
|
||||
{
|
||||
var cveKey = MakeCveKey(tenantId, cve);
|
||||
_byCve.AddOrUpdate(
|
||||
cveKey,
|
||||
_ => [digest],
|
||||
(_, list) => { if (!list.Contains(digest)) list.Add(digest); return list; });
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(new StoreResult
|
||||
{
|
||||
Digest = digest,
|
||||
Created = isNew,
|
||||
ArtifactDigest = graph.Artifact.Digest,
|
||||
NodeCount = graph.Nodes.Length,
|
||||
EdgeCount = graph.Edges.Length,
|
||||
StoredAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
public Task<ReachGraphMinimal?> GetByDigestAsync(string digest, string tenantId, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var key = MakeKey(tenantId, digest);
|
||||
return Task.FromResult(_graphs.TryGetValue(key, out var entry) ? entry.Graph : null);
|
||||
}
|
||||
|
||||
public Task<bool> DeleteAsync(string digest, string tenantId, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var key = MakeKey(tenantId, digest);
|
||||
return Task.FromResult(_graphs.TryRemove(key, out _));
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<ReachGraphSummary>> ListByArtifactAsync(
|
||||
string artifactDigest,
|
||||
string tenantId,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var artifactKey = MakeArtifactKey(tenantId, artifactDigest);
|
||||
|
||||
if (!_byArtifact.TryGetValue(artifactKey, out var digests))
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<ReachGraphSummary>>(Array.Empty<ReachGraphSummary>());
|
||||
}
|
||||
|
||||
var items = digests
|
||||
.Take(limit)
|
||||
.Select(d =>
|
||||
{
|
||||
var graph = _graphs[MakeKey(tenantId, d)].Graph;
|
||||
return new ReachGraphSummary
|
||||
{
|
||||
Digest = d,
|
||||
ArtifactDigest = graph.Artifact.Digest,
|
||||
NodeCount = graph.Nodes.Length,
|
||||
EdgeCount = graph.Edges.Length,
|
||||
BlobSizeBytes = 0,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Scope = graph.Scope
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<ReachGraphSummary>>(items);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<ReachGraphSummary>> FindByCveAsync(
|
||||
string cveId,
|
||||
string tenantId,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var cveKey = MakeCveKey(tenantId, cveId);
|
||||
|
||||
if (!_byCve.TryGetValue(cveKey, out var digests))
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<ReachGraphSummary>>(Array.Empty<ReachGraphSummary>());
|
||||
}
|
||||
|
||||
var items = digests
|
||||
.Take(limit)
|
||||
.Select(d =>
|
||||
{
|
||||
var graph = _graphs[MakeKey(tenantId, d)].Graph;
|
||||
return new ReachGraphSummary
|
||||
{
|
||||
Digest = d,
|
||||
ArtifactDigest = graph.Artifact.Digest,
|
||||
NodeCount = graph.Nodes.Length,
|
||||
EdgeCount = graph.Edges.Length,
|
||||
BlobSizeBytes = 0,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Scope = graph.Scope
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<ReachGraphSummary>>(items);
|
||||
}
|
||||
|
||||
public Task RecordReplayAsync(ReplayLogEntry entry, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
_replayLog.Add(entry);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string MakeKey(string tenantId, string digest) => $"{tenantId}:{digest}";
|
||||
private static string MakeArtifactKey(string tenantId, string artifactDigest) => $"{tenantId}:artifact:{artifactDigest}";
|
||||
private static string MakeCveKey(string tenantId, string cveId) => $"{tenantId}:cve:{cveId}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of <see cref="IReachGraphCache"/> for testing.
|
||||
/// </summary>
|
||||
internal sealed class InMemoryReachGraphCache : IReachGraphCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ReachGraphMinimal> _cache = new();
|
||||
private readonly ConcurrentDictionary<string, byte[]> _sliceCache = new();
|
||||
|
||||
public Task<ReachGraphMinimal?> GetAsync(string digest, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(_cache.TryGetValue(digest, out var graph) ? graph : null);
|
||||
}
|
||||
|
||||
public Task SetAsync(string digest, ReachGraphMinimal graph, TimeSpan? ttl, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
_cache[digest] = graph;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<byte[]?> GetSliceAsync(string digest, string sliceKey, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var key = $"{digest}:{sliceKey}";
|
||||
return Task.FromResult(_sliceCache.TryGetValue(key, out var slice) ? slice : null);
|
||||
}
|
||||
|
||||
public Task SetSliceAsync(string digest, string sliceKey, byte[] slice, TimeSpan? ttl, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var key = $"{digest}:{sliceKey}";
|
||||
_sliceCache[key] = slice;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task InvalidateAsync(string digest, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
_cache.TryRemove(digest, out _);
|
||||
// Also remove related slices
|
||||
var keysToRemove = _sliceCache.Keys.Where(k => k.StartsWith($"{digest}:")).ToList();
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
_sliceCache.TryRemove(key, out _);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<bool> ExistsAsync(string digest, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(_cache.ContainsKey(digest));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user