stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# StellaOps.ReachGraph.Cache.Tests - Local Agent Charter
|
||||
|
||||
## Roles
|
||||
- Backend developer
|
||||
- QA automation engineer
|
||||
|
||||
## Working directory
|
||||
- src/__Libraries/__Tests/StellaOps.ReachGraph.Cache.Tests
|
||||
|
||||
## Allowed dependencies
|
||||
- src/__Libraries/StellaOps.ReachGraph.Cache
|
||||
- src/__Libraries/StellaOps.ReachGraph
|
||||
- src/__Libraries/StellaOps.TestKit
|
||||
|
||||
## Required reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/reach-graph/README.md
|
||||
- docs/modules/reach-graph/architecture.md
|
||||
|
||||
## Determinism and test rules
|
||||
- Use deterministic inputs: avoid DateTime.UtcNow, DateTimeOffset.UtcNow, Guid.NewGuid, and Random.Shared in tests.
|
||||
- Use TimeProvider and fixed seeds or fixtures for time- and randomness-dependent tests.
|
||||
- Use CultureInfo.InvariantCulture for parsing and formatting in tests.
|
||||
- Tag tests with TestCategories (Unit, Integration, Performance) and keep integration tests out of unit-only runs.
|
||||
- Keep tests offline and deterministic; prefer TestServer/TestHost with fixed inputs.
|
||||
|
||||
## Quality and safety
|
||||
- ASCII-only strings and comments unless explicitly justified.
|
||||
- Clean up temp files/directories created during tests.
|
||||
@@ -0,0 +1,26 @@
|
||||
using Moq;
|
||||
using StackExchange.Redis;
|
||||
using StellaOps.TestKit;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.ReachGraph.Cache.Tests;
|
||||
|
||||
public sealed class ReachGraphValkeyCacheCancellationTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public async Task GetAsyncHonorsCancellation()
|
||||
{
|
||||
var harness = new ReachGraphValkeyCacheHarness("tenant-1");
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(
|
||||
() => harness.Cache.GetAsync("sha256:graph", cts.Token));
|
||||
|
||||
harness.Database.Verify(
|
||||
db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()),
|
||||
Times.Never);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Moq;
|
||||
using StackExchange.Redis;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.ReachGraph.Cache.Tests;
|
||||
|
||||
public sealed class ReachGraphValkeyCacheExistsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public async Task ExistsAsyncReturnsTrueWhenKeyExists()
|
||||
{
|
||||
var harness = new ReachGraphValkeyCacheHarness("tenant-1");
|
||||
|
||||
harness.Database
|
||||
.Setup(db => db.KeyExistsAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var result = await harness.Cache.ExistsAsync("sha256:graph");
|
||||
|
||||
Assert.True(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Moq;
|
||||
using StackExchange.Redis;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.ReachGraph.Cache.Tests;
|
||||
|
||||
public sealed class ReachGraphValkeyCacheGetTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public async Task GetAsyncReturnsNullWhenMissing()
|
||||
{
|
||||
var harness = new ReachGraphValkeyCacheHarness("tenant-1");
|
||||
RedisKey capturedKey = default;
|
||||
|
||||
harness.Database
|
||||
.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
|
||||
.Callback<RedisKey, CommandFlags>((key, _) => capturedKey = key)
|
||||
.ReturnsAsync(RedisValue.Null);
|
||||
|
||||
var result = await harness.Cache.GetAsync("sha256:graph");
|
||||
|
||||
Assert.Null(result);
|
||||
Assert.Equal("reachgraph:tenant-1:sha256:graph", capturedKey.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public async Task GetAsyncReturnsGraphWhenHit()
|
||||
{
|
||||
var harness = new ReachGraphValkeyCacheHarness("tenant-1");
|
||||
var graph = ReachGraphValkeyCacheTestData.CreateGraph("sha256:graph");
|
||||
var json = harness.Serializer.SerializeMinimal(graph);
|
||||
var stored = ReachGraphValkeyCacheTestData.Compress(json);
|
||||
|
||||
harness.Database
|
||||
.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
|
||||
.ReturnsAsync(stored);
|
||||
|
||||
var result = await harness.Cache.GetAsync("sha256:graph");
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("sha256:graph", result!.Artifact.Digest);
|
||||
Assert.Equal("reachgraph.min@v1", result.SchemaVersion);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StackExchange.Redis;
|
||||
using StellaOps.ReachGraph.Serialization;
|
||||
|
||||
namespace StellaOps.ReachGraph.Cache.Tests;
|
||||
|
||||
internal sealed class ReachGraphValkeyCacheHarness
|
||||
{
|
||||
public Mock<IConnectionMultiplexer> Multiplexer { get; } = new();
|
||||
public Mock<IDatabase> Database { get; } = new();
|
||||
public Mock<ILogger<ReachGraphValkeyCache>> Logger { get; } = new();
|
||||
public CanonicalReachGraphSerializer Serializer { get; } = new();
|
||||
public ReachGraphCacheOptions Settings { get; }
|
||||
public ReachGraphValkeyCache Cache { get; }
|
||||
|
||||
public ReachGraphValkeyCacheHarness(string tenantId, ReachGraphCacheOptions? options = null)
|
||||
{
|
||||
Settings = options ?? new ReachGraphCacheOptions
|
||||
{
|
||||
KeyPrefix = "reachgraph",
|
||||
DefaultTtl = TimeSpan.FromMinutes(30),
|
||||
SliceTtl = TimeSpan.FromMinutes(10),
|
||||
MaxGraphSizeBytes = 1024 * 1024,
|
||||
CompressInCache = true,
|
||||
Database = 0
|
||||
};
|
||||
|
||||
Multiplexer
|
||||
.Setup(m => m.GetDatabase(It.IsAny<int>(), It.IsAny<object?>()))
|
||||
.Returns(Database.Object);
|
||||
|
||||
Cache = new ReachGraphValkeyCache(
|
||||
Multiplexer.Object,
|
||||
Serializer,
|
||||
Options.Create(Settings),
|
||||
Logger.Object,
|
||||
tenantId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using Moq;
|
||||
using StackExchange.Redis;
|
||||
using StellaOps.TestKit;
|
||||
using System.Net;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.ReachGraph.Cache.Tests;
|
||||
|
||||
public sealed class ReachGraphValkeyCacheInvalidateTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public async Task InvalidateAsyncDeletesGraphAndSlices()
|
||||
{
|
||||
var harness = new ReachGraphValkeyCacheHarness("tenant-1");
|
||||
var endpoint1 = new DnsEndPoint("localhost", 6379);
|
||||
var endpoint2 = new DnsEndPoint("localhost", 6380);
|
||||
var server1 = new Mock<IServer>();
|
||||
var server2 = new Mock<IServer>();
|
||||
var keys1 = new[] { (RedisKey)"reachgraph:tenant-1:sha256:graph:slice:a" };
|
||||
var keys2 = new[] { (RedisKey)"reachgraph:tenant-1:sha256:graph:slice:b" };
|
||||
var capturedPattern = string.Empty;
|
||||
RedisKey capturedGraphKey = default;
|
||||
RedisKey[]? capturedSliceKeys = null;
|
||||
|
||||
server1.SetupGet(s => s.IsConnected).Returns(true);
|
||||
server1
|
||||
.Setup(s => s.Keys(
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<RedisValue>(),
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<long>(),
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<CommandFlags>()))
|
||||
.Callback<int, RedisValue, int, long, int, CommandFlags>((_, pattern, _, _, _, _) =>
|
||||
capturedPattern = pattern.ToString())
|
||||
.Returns(keys1);
|
||||
|
||||
server2.SetupGet(s => s.IsConnected).Returns(true);
|
||||
server2
|
||||
.Setup(s => s.Keys(
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<RedisValue>(),
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<long>(),
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<CommandFlags>()))
|
||||
.Returns(keys2);
|
||||
|
||||
harness.Multiplexer.Setup(m => m.GetEndPoints(It.IsAny<bool>())).Returns(
|
||||
new EndPoint[] { endpoint1, endpoint2 });
|
||||
harness.Multiplexer.Setup(m => m.GetServer(endpoint1, It.IsAny<object?>())).Returns(server1.Object);
|
||||
harness.Multiplexer.Setup(m => m.GetServer(endpoint2, It.IsAny<object?>())).Returns(server2.Object);
|
||||
harness.Database
|
||||
.Setup(db => db.KeyDeleteAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
|
||||
.Callback<RedisKey, CommandFlags>((key, _) => capturedGraphKey = key)
|
||||
.ReturnsAsync(true);
|
||||
harness.Database
|
||||
.Setup(db => db.KeyDeleteAsync(It.IsAny<RedisKey[]>(), It.IsAny<CommandFlags>()))
|
||||
.Callback<RedisKey[], CommandFlags>((keys, _) => capturedSliceKeys = keys)
|
||||
.ReturnsAsync((RedisKey[] keys, CommandFlags _) => keys.LongLength);
|
||||
|
||||
await harness.Cache.InvalidateAsync("sha256:graph");
|
||||
|
||||
Assert.Equal("reachgraph:tenant-1:sha256:graph", capturedGraphKey.ToString());
|
||||
Assert.Equal("reachgraph:tenant-1:sha256:graph:slice:*", capturedPattern);
|
||||
Assert.NotNull(capturedSliceKeys);
|
||||
Assert.Contains(keys1[0], capturedSliceKeys!);
|
||||
Assert.Contains(keys2[0], capturedSliceKeys!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Moq;
|
||||
using StackExchange.Redis;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.ReachGraph.Cache.Tests;
|
||||
|
||||
public sealed class ReachGraphValkeyCacheSetTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public async Task SetAsyncStoresCompressedGraph()
|
||||
{
|
||||
var options = new ReachGraphCacheOptions { DefaultTtl = TimeSpan.FromMinutes(5) };
|
||||
var harness = new ReachGraphValkeyCacheHarness("tenant-1", options);
|
||||
var graph = ReachGraphValkeyCacheTestData.CreateGraph("sha256:graph");
|
||||
|
||||
harness.Database
|
||||
.Setup(db => db.StringSetAsync(
|
||||
It.IsAny<RedisKey>(),
|
||||
It.IsAny<RedisValue>(),
|
||||
It.IsAny<TimeSpan?>(),
|
||||
It.IsAny<When>(),
|
||||
It.IsAny<CommandFlags>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
await harness.Cache.SetAsync("sha256:graph", graph, ttl: TimeSpan.FromMinutes(2));
|
||||
|
||||
var invocation = Assert.Single(harness.Database.Invocations, i => i.Method.Name == "StringSetAsync");
|
||||
var key = (RedisKey)invocation.Arguments[0]!;
|
||||
var storedValue = (RedisValue)invocation.Arguments[1]!;
|
||||
var stored = (byte[]?)storedValue;
|
||||
var ttl = (Expiration)invocation.Arguments[2]!;
|
||||
var json = harness.Serializer.SerializeMinimal(graph);
|
||||
Assert.NotNull(stored);
|
||||
var decompressed = ReachGraphValkeyCacheTestData.Decompress(stored);
|
||||
|
||||
Assert.Equal("reachgraph:tenant-1:sha256:graph", key.ToString());
|
||||
Assert.Equal((Expiration)TimeSpan.FromMinutes(2), ttl);
|
||||
Assert.Equal(json, decompressed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public async Task SetAsyncSkipsWhenGraphTooLarge()
|
||||
{
|
||||
var options = new ReachGraphCacheOptions { MaxGraphSizeBytes = 1 };
|
||||
var harness = new ReachGraphValkeyCacheHarness("tenant-1", options);
|
||||
var graph = ReachGraphValkeyCacheTestData.CreateGraph("sha256:graph");
|
||||
|
||||
await harness.Cache.SetAsync("sha256:graph", graph);
|
||||
|
||||
harness.Database.Verify(
|
||||
db => db.StringSetAsync(
|
||||
It.IsAny<RedisKey>(),
|
||||
It.IsAny<RedisValue>(),
|
||||
It.IsAny<TimeSpan?>(),
|
||||
It.IsAny<When>(),
|
||||
It.IsAny<CommandFlags>()),
|
||||
Times.Never);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Moq;
|
||||
using StackExchange.Redis;
|
||||
using StellaOps.TestKit;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.ReachGraph.Cache.Tests;
|
||||
|
||||
public sealed class ReachGraphValkeyCacheSliceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public async Task GetSliceAsyncReturnsBytesWhenHit()
|
||||
{
|
||||
var harness = new ReachGraphValkeyCacheHarness("tenant-1");
|
||||
var slice = Encoding.UTF8.GetBytes("slice-data");
|
||||
var stored = ReachGraphValkeyCacheTestData.Compress(slice);
|
||||
|
||||
harness.Database
|
||||
.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
|
||||
.ReturnsAsync(stored);
|
||||
|
||||
var result = await harness.Cache.GetSliceAsync("sha256:graph", "slice-1");
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(slice, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public async Task SetSliceAsyncStoresCompressedSlice()
|
||||
{
|
||||
var harness = new ReachGraphValkeyCacheHarness("tenant-1");
|
||||
var slice = Encoding.UTF8.GetBytes("slice-data");
|
||||
|
||||
harness.Database
|
||||
.Setup(db => db.StringSetAsync(
|
||||
It.IsAny<RedisKey>(),
|
||||
It.IsAny<RedisValue>(),
|
||||
It.IsAny<TimeSpan?>(),
|
||||
It.IsAny<When>(),
|
||||
It.IsAny<CommandFlags>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
await harness.Cache.SetSliceAsync("sha256:graph", "slice-1", slice, ttl: TimeSpan.FromMinutes(1));
|
||||
|
||||
var invocation = Assert.Single(harness.Database.Invocations, i => i.Method.Name == "StringSetAsync");
|
||||
var storedValue = (RedisValue)invocation.Arguments[1]!;
|
||||
var stored = (byte[]?)storedValue;
|
||||
var ttl = (Expiration)invocation.Arguments[2]!;
|
||||
Assert.NotNull(stored);
|
||||
var decompressed = ReachGraphValkeyCacheTestData.Decompress(stored);
|
||||
|
||||
Assert.Equal(slice, decompressed);
|
||||
Assert.Equal((Expiration)TimeSpan.FromMinutes(1), ttl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using StellaOps.ReachGraph.Schema;
|
||||
|
||||
namespace StellaOps.ReachGraph.Cache.Tests;
|
||||
|
||||
internal static class ReachGraphValkeyCacheTestData
|
||||
{
|
||||
public static ReachGraphMinimal CreateGraph(string digest)
|
||||
{
|
||||
return new ReachGraphMinimal
|
||||
{
|
||||
Artifact = new ReachGraphArtifact(
|
||||
"svc.payments",
|
||||
digest,
|
||||
ImmutableArray.Create("linux/amd64")),
|
||||
Scope = new ReachGraphScope(
|
||||
ImmutableArray.Create("/app/bin/svc"),
|
||||
ImmutableArray.Create("prod")),
|
||||
Nodes = ImmutableArray.Create(new ReachGraphNode
|
||||
{
|
||||
Id = "node-1",
|
||||
Kind = ReachGraphNodeKind.Package,
|
||||
Ref = "pkg:npm/lodash@4.17.21",
|
||||
IsEntrypoint = true
|
||||
}),
|
||||
Edges = ImmutableArray.Create(new ReachGraphEdge
|
||||
{
|
||||
From = "node-1",
|
||||
To = "node-1",
|
||||
Why = new EdgeExplanation
|
||||
{
|
||||
Type = EdgeExplanationType.Import,
|
||||
Confidence = 1.0
|
||||
}
|
||||
}),
|
||||
Provenance = new ReachGraphProvenance
|
||||
{
|
||||
Inputs = new ReachGraphInputs
|
||||
{
|
||||
Sbom = "sha256:sbom"
|
||||
},
|
||||
ComputedAt = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero),
|
||||
Analyzer = new ReachGraphAnalyzer("reach-graph", "1.0.0", "sha256:tool")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static byte[] Compress(byte[] data)
|
||||
{
|
||||
using var output = new MemoryStream();
|
||||
using (var gzip = new GZipStream(output, CompressionLevel.Fastest, leaveOpen: true))
|
||||
{
|
||||
gzip.Write(data);
|
||||
}
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
public static byte[] Decompress(byte[] compressed)
|
||||
{
|
||||
using var input = new MemoryStream(compressed);
|
||||
using var gzip = new GZipStream(input, CompressionMode.Decompress);
|
||||
using var output = new MemoryStream();
|
||||
gzip.CopyTo(output);
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.ReachGraph.Cache/StellaOps.ReachGraph.Cache.csproj" />
|
||||
<ProjectReference Include="../../StellaOps.ReachGraph/StellaOps.ReachGraph.csproj" />
|
||||
<ProjectReference Include="../../StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||
"parallelizeTestCollections": false,
|
||||
"maxParallelThreads": 1
|
||||
}
|
||||
Reference in New Issue
Block a user