using System; using System.IO; using System.Text; using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StellaOps.Signals.Options; using StellaOps.Signals.Services; using Xunit; namespace StellaOps.Signals.Tests; public class EdgeBundleIngestionServiceTests { private readonly EdgeBundleIngestionService _service; private const string TestTenantId = "test-tenant"; private const string TestGraphHash = "blake3:abc123def456"; public EdgeBundleIngestionServiceTests() { var opts = new SignalsOptions(); opts.Storage.RootPath = Path.GetTempPath(); var options = Microsoft.Extensions.Options.Options.Create(opts); _service = new EdgeBundleIngestionService(NullLogger.Instance, options); } [Fact] public async Task IngestAsync_ParsesBundleAndStoresDocument() { // Arrange var bundle = new { schema = "edge-bundle-v1", bundleId = "bundle:test123", graphHash = TestGraphHash, bundleReason = "RuntimeHits", generatedAt = DateTimeOffset.UtcNow.ToString("O"), edges = new[] { new { from = "func_a", to = "func_b", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.9 }, new { from = "func_b", to = "func_c", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.8 } } }; var bundleJson = JsonSerializer.Serialize(bundle); using var stream = new MemoryStream(Encoding.UTF8.GetBytes(bundleJson)); // Act var result = await _service.IngestAsync(TestTenantId, stream, null); // Assert Assert.Equal("bundle:test123", result.BundleId); Assert.Equal(TestGraphHash, result.GraphHash); Assert.Equal("RuntimeHits", result.BundleReason); Assert.Equal(2, result.EdgeCount); Assert.Equal(0, result.RevokedCount); Assert.False(result.Quarantined); Assert.Contains("cas://reachability/edges/", result.CasUri); } [Fact] public async Task IngestAsync_TracksRevokedEdgesForQuarantine() { // Arrange var bundle = new { schema = "edge-bundle-v1", bundleId = "bundle:revoked123", graphHash = TestGraphHash, bundleReason = "Revoked", edges = new[] { new { from = "func_a", to = "func_b", kind = "call", reason = "Revoked", revoked = true, confidence = 1.0 }, new { from = "func_b", to = "func_c", kind = "call", reason = "TargetRemoved", revoked = true, confidence = 1.0 }, new { from = "func_c", to = "func_d", kind = "call", reason = "LowConfidence", revoked = false, confidence = 0.3 } } }; var bundleJson = JsonSerializer.Serialize(bundle); using var stream = new MemoryStream(Encoding.UTF8.GetBytes(bundleJson)); // Act var result = await _service.IngestAsync(TestTenantId, stream, null); // Assert Assert.Equal(3, result.EdgeCount); Assert.Equal(2, result.RevokedCount); Assert.True(result.Quarantined); } [Fact] public async Task IsEdgeRevokedAsync_ReturnsTrueForRevokedEdges() { // Arrange var bundle = new { bundleId = "bundle:revoke-check", graphHash = TestGraphHash, bundleReason = "Revoked", edges = new[] { new { from = "vuln_func", to = "patched_func", kind = "call", reason = "Revoked", revoked = true, confidence = 1.0 } } }; var bundleJson = JsonSerializer.Serialize(bundle); using var stream = new MemoryStream(Encoding.UTF8.GetBytes(bundleJson)); await _service.IngestAsync(TestTenantId, stream, null); // Act & Assert Assert.True(await _service.IsEdgeRevokedAsync(TestTenantId, TestGraphHash, "vuln_func", "patched_func")); Assert.False(await _service.IsEdgeRevokedAsync(TestTenantId, TestGraphHash, "other_func", "some_func")); } [Fact] public async Task GetBundlesForGraphAsync_ReturnsAllBundlesForGraph() { // Arrange - ingest multiple bundles var graphHash = $"blake3:graph_{Guid.NewGuid():N}"; var bundle1 = new { bundleId = "bundle:1", graphHash, bundleReason = "RuntimeHits", edges = new[] { new { from = "a", to = "b", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 1.0 } } }; var bundle2 = new { bundleId = "bundle:2", graphHash, bundleReason = "InitArray", edges = new[] { new { from = "c", to = "d", kind = "call", reason = "InitArray", revoked = false, confidence = 1.0 } } }; using var stream1 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle1))); using var stream2 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle2))); await _service.IngestAsync(TestTenantId, stream1, null); await _service.IngestAsync(TestTenantId, stream2, null); // Act var bundles = await _service.GetBundlesForGraphAsync(TestTenantId, graphHash); // Assert Assert.Equal(2, bundles.Length); Assert.Contains(bundles, b => b.BundleId == "bundle:1"); Assert.Contains(bundles, b => b.BundleId == "bundle:2"); } [Fact] public async Task GetRevokedEdgesAsync_ReturnsOnlyRevokedEdges() { // Arrange var graphHash = $"blake3:revoked_graph_{Guid.NewGuid():N}"; var bundle = new { bundleId = "bundle:mixed", graphHash, bundleReason = "Revoked", edges = new[] { new { from = "a", to = "b", kind = "call", reason = "Revoked", revoked = true, confidence = 1.0 }, new { from = "c", to = "d", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.9 }, new { from = "e", to = "f", kind = "call", reason = "Revoked", revoked = true, confidence = 1.0 } } }; using var stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle))); await _service.IngestAsync(TestTenantId, stream, null); // Act var revokedEdges = await _service.GetRevokedEdgesAsync(TestTenantId, graphHash); // Assert Assert.Equal(2, revokedEdges.Length); Assert.All(revokedEdges, e => Assert.True(e.Revoked)); } [Fact] public async Task IngestAsync_WithDsseStream_SetsVerifiedAndDsseFields() { // Arrange var bundle = new { bundleId = "bundle:verified", graphHash = TestGraphHash, bundleReason = "RuntimeHits", edges = new[] { new { from = "a", to = "b", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 1.0 } } }; var dsseEnvelope = new { payloadType = "application/vnd.stellaops.edgebundle.predicate+json", payload = Convert.ToBase64String(Encoding.UTF8.GetBytes("{}")), signatures = new[] { new { keyid = "test", sig = "abc123" } } }; using var bundleStream = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle))); using var dsseStream = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(dsseEnvelope))); // Act var result = await _service.IngestAsync(TestTenantId, bundleStream, dsseStream); // Assert Assert.NotNull(result.DsseCasUri); Assert.EndsWith(".dsse", result.DsseCasUri); } [Fact] public async Task IngestAsync_ThrowsOnMissingGraphHash() { // Arrange var bundle = new { bundleId = "bundle:no-graph", bundleReason = "RuntimeHits", edges = Array.Empty() }; using var stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle))); // Act & Assert await Assert.ThrowsAsync(() => _service.IngestAsync(TestTenantId, stream, null)); } [Fact] public async Task IngestAsync_UpdatesExistingBundleWithSameId() { // Arrange var graphHash = $"blake3:update_test_{Guid.NewGuid():N}"; var bundle1 = new { bundleId = "bundle:same-id", graphHash, bundleReason = "RuntimeHits", edges = new[] { new { from = "a", to = "b", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.5 } } }; var bundle2 = new { bundleId = "bundle:same-id", graphHash, bundleReason = "RuntimeHits", edges = new[] { new { from = "a", to = "b", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.9 }, new { from = "c", to = "d", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.8 } } }; using var stream1 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle1))); using var stream2 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle2))); // Act await _service.IngestAsync(TestTenantId, stream1, null); await _service.IngestAsync(TestTenantId, stream2, null); // Assert var bundles = await _service.GetBundlesForGraphAsync(TestTenantId, graphHash); Assert.Single(bundles); Assert.Equal(2, bundles[0].Edges.Count); // Updated to have 2 edges } }