Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Scanner.Reachability.Slices;
|
||||
using StellaOps.Scanner.WebService.Endpoints;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for slice query and replay endpoints.
|
||||
/// </summary>
|
||||
public sealed class SliceEndpointsTests : IClassFixture<ScannerApplicationFixture>
|
||||
{
|
||||
private readonly ScannerApplicationFixture _fixture;
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public SliceEndpointsTests(ScannerApplicationFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_client = fixture.CreateClient();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QuerySlice_WithValidCve_ReturnsSlice()
|
||||
{
|
||||
// Arrange
|
||||
var request = new SliceQueryRequestDto
|
||||
{
|
||||
ScanId = "test-scan-001",
|
||||
CveId = "CVE-2024-1234",
|
||||
Symbols = new List<string> { "vulnerable_function" }
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/slices/query", request);
|
||||
|
||||
// Assert
|
||||
// Note: May return 404 if no test data, but validates endpoint registration
|
||||
Assert.True(
|
||||
response.StatusCode == HttpStatusCode.OK ||
|
||||
response.StatusCode == HttpStatusCode.NotFound ||
|
||||
response.StatusCode == HttpStatusCode.Unauthorized,
|
||||
$"Unexpected status: {response.StatusCode}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QuerySlice_WithoutScanId_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var request = new SliceQueryRequestDto
|
||||
{
|
||||
CveId = "CVE-2024-1234"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/slices/query", request);
|
||||
|
||||
// Assert
|
||||
Assert.True(
|
||||
response.StatusCode == HttpStatusCode.BadRequest ||
|
||||
response.StatusCode == HttpStatusCode.Unauthorized,
|
||||
$"Expected BadRequest or Unauthorized, got {response.StatusCode}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QuerySlice_WithoutCveOrSymbols_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var request = new SliceQueryRequestDto
|
||||
{
|
||||
ScanId = "test-scan-001"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/slices/query", request);
|
||||
|
||||
// Assert
|
||||
Assert.True(
|
||||
response.StatusCode == HttpStatusCode.BadRequest ||
|
||||
response.StatusCode == HttpStatusCode.Unauthorized,
|
||||
$"Expected BadRequest or Unauthorized, got {response.StatusCode}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSlice_WithValidDigest_ReturnsSlice()
|
||||
{
|
||||
// Arrange
|
||||
var digest = "sha256:abc123";
|
||||
|
||||
// Act
|
||||
var response = await _client.GetAsync($"/api/slices/{digest}");
|
||||
|
||||
// Assert
|
||||
Assert.True(
|
||||
response.StatusCode == HttpStatusCode.OK ||
|
||||
response.StatusCode == HttpStatusCode.NotFound ||
|
||||
response.StatusCode == HttpStatusCode.Unauthorized,
|
||||
$"Unexpected status: {response.StatusCode}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSlice_WithDsseAccept_ReturnsDsseEnvelope()
|
||||
{
|
||||
// Arrange
|
||||
var digest = "sha256:abc123";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, $"/api/slices/{digest}");
|
||||
request.Headers.Add("Accept", "application/dsse+json");
|
||||
|
||||
// Act
|
||||
var response = await _client.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(
|
||||
response.StatusCode == HttpStatusCode.OK ||
|
||||
response.StatusCode == HttpStatusCode.NotFound ||
|
||||
response.StatusCode == HttpStatusCode.Unauthorized,
|
||||
$"Unexpected status: {response.StatusCode}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReplaySlice_WithValidDigest_ReturnsReplayResult()
|
||||
{
|
||||
// Arrange
|
||||
var request = new SliceReplayRequestDto
|
||||
{
|
||||
SliceDigest = "sha256:abc123"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/slices/replay", request);
|
||||
|
||||
// Assert
|
||||
Assert.True(
|
||||
response.StatusCode == HttpStatusCode.OK ||
|
||||
response.StatusCode == HttpStatusCode.NotFound ||
|
||||
response.StatusCode == HttpStatusCode.Unauthorized,
|
||||
$"Unexpected status: {response.StatusCode}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReplaySlice_WithoutDigest_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var request = new SliceReplayRequestDto();
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/slices/replay", request);
|
||||
|
||||
// Assert
|
||||
Assert.True(
|
||||
response.StatusCode == HttpStatusCode.BadRequest ||
|
||||
response.StatusCode == HttpStatusCode.Unauthorized,
|
||||
$"Expected BadRequest or Unauthorized, got {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SliceDiffComputer.
|
||||
/// </summary>
|
||||
public sealed class SliceDiffComputerTests
|
||||
{
|
||||
private readonly SliceDiffComputer _computer = new();
|
||||
|
||||
[Fact]
|
||||
public void Compare_IdenticalSlices_ReturnsMatch()
|
||||
{
|
||||
// Arrange
|
||||
var slice = CreateTestSlice();
|
||||
|
||||
// Act
|
||||
var result = _computer.Compare(slice, slice);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Match);
|
||||
Assert.Empty(result.MissingNodes);
|
||||
Assert.Empty(result.ExtraNodes);
|
||||
Assert.Empty(result.MissingEdges);
|
||||
Assert.Empty(result.ExtraEdges);
|
||||
Assert.Null(result.VerdictDiff);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_DifferentNodes_ReturnsDiff()
|
||||
{
|
||||
// Arrange
|
||||
var original = CreateTestSlice();
|
||||
var modified = original with
|
||||
{
|
||||
Subgraph = original.Subgraph with
|
||||
{
|
||||
Nodes = original.Subgraph.Nodes.Add(new SliceNode
|
||||
{
|
||||
Id = "extra-node",
|
||||
Symbol = "extra_func",
|
||||
Kind = SliceNodeKind.Intermediate
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _computer.Compare(original, modified);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Match);
|
||||
Assert.Empty(result.MissingNodes);
|
||||
Assert.Single(result.ExtraNodes);
|
||||
Assert.Contains("extra-node", result.ExtraNodes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_DifferentEdges_ReturnsDiff()
|
||||
{
|
||||
// Arrange
|
||||
var original = CreateTestSlice();
|
||||
var modified = original with
|
||||
{
|
||||
Subgraph = original.Subgraph with
|
||||
{
|
||||
Edges = original.Subgraph.Edges.RemoveAt(0)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _computer.Compare(original, modified);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Match);
|
||||
Assert.Single(result.MissingEdges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_DifferentVerdict_ReturnsDiff()
|
||||
{
|
||||
// Arrange
|
||||
var original = CreateTestSlice();
|
||||
var modified = original with
|
||||
{
|
||||
Verdict = original.Verdict with
|
||||
{
|
||||
Status = SliceVerdictStatus.Unreachable
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _computer.Compare(original, modified);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Match);
|
||||
Assert.NotNull(result.VerdictDiff);
|
||||
Assert.Contains("Status:", result.VerdictDiff);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeCacheKey_SameInputs_ReturnsSameKey()
|
||||
{
|
||||
// Arrange
|
||||
var symbols = new[] { "func_a", "func_b" };
|
||||
var entrypoints = new[] { "main" };
|
||||
|
||||
// Act
|
||||
var key1 = SliceDiffComputer.ComputeCacheKey("scan1", "CVE-2024-1234", symbols, entrypoints, null);
|
||||
var key2 = SliceDiffComputer.ComputeCacheKey("scan1", "CVE-2024-1234", symbols, entrypoints, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(key1, key2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeCacheKey_DifferentInputs_ReturnsDifferentKey()
|
||||
{
|
||||
// Arrange
|
||||
var symbols = new[] { "func_a", "func_b" };
|
||||
|
||||
// Act
|
||||
var key1 = SliceDiffComputer.ComputeCacheKey("scan1", "CVE-2024-1234", symbols, null, null);
|
||||
var key2 = SliceDiffComputer.ComputeCacheKey("scan2", "CVE-2024-1234", symbols, null, null);
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(key1, key2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToSummary_MatchingSlices_ReturnsMatchMessage()
|
||||
{
|
||||
// Arrange
|
||||
var result = new SliceDiffResult { Match = true };
|
||||
|
||||
// Act
|
||||
var summary = result.ToSummary();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("match exactly", summary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToSummary_DifferingSlices_ReturnsDetailedDiff()
|
||||
{
|
||||
// Arrange
|
||||
var result = new SliceDiffResult
|
||||
{
|
||||
Match = false,
|
||||
MissingNodes = ImmutableArray.Create("node1", "node2"),
|
||||
ExtraEdges = ImmutableArray.Create("edge1"),
|
||||
VerdictDiff = "Status: reachable -> unreachable"
|
||||
};
|
||||
|
||||
// Act
|
||||
var summary = result.ToSummary();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Missing nodes", summary);
|
||||
Assert.Contains("Extra edges", summary);
|
||||
Assert.Contains("Verdict changed", summary);
|
||||
}
|
||||
|
||||
private static ReachabilitySlice CreateTestSlice()
|
||||
{
|
||||
return new ReachabilitySlice
|
||||
{
|
||||
Inputs = new SliceInputs
|
||||
{
|
||||
GraphDigest = "sha256:graph123"
|
||||
},
|
||||
Query = new SliceQuery
|
||||
{
|
||||
CveId = "CVE-2024-1234",
|
||||
TargetSymbols = ImmutableArray.Create("vulnerable_func"),
|
||||
Entrypoints = ImmutableArray.Create("main")
|
||||
},
|
||||
Subgraph = new SliceSubgraph
|
||||
{
|
||||
Nodes = ImmutableArray.Create(
|
||||
new SliceNode { Id = "main", Symbol = "main", Kind = SliceNodeKind.Entrypoint },
|
||||
new SliceNode { Id = "vuln", Symbol = "vulnerable_func", Kind = SliceNodeKind.Target }
|
||||
),
|
||||
Edges = ImmutableArray.Create(
|
||||
new SliceEdge { From = "main", To = "vuln", Kind = SliceEdgeKind.Direct, Confidence = 1.0 }
|
||||
)
|
||||
},
|
||||
Verdict = new SliceVerdict
|
||||
{
|
||||
Status = SliceVerdictStatus.Reachable,
|
||||
Confidence = 0.95
|
||||
},
|
||||
Manifest = new Scanner.Core.ScanManifest()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SliceCache.
|
||||
/// </summary>
|
||||
public sealed class SliceCacheTests
|
||||
{
|
||||
[Fact]
|
||||
public void TryGet_EmptyCache_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SliceCacheOptions());
|
||||
using var cache = new SliceCache(options);
|
||||
|
||||
// Act
|
||||
var found = cache.TryGet("nonexistent", out var entry);
|
||||
|
||||
// Assert
|
||||
Assert.False(found);
|
||||
Assert.Null(entry);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Set_ThenGet_ReturnsEntry()
|
||||
{
|
||||
// Arrange
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SliceCacheOptions());
|
||||
using var cache = new SliceCache(options);
|
||||
var slice = CreateTestSlice();
|
||||
|
||||
// Act
|
||||
cache.Set("key1", slice, "sha256:abc123");
|
||||
var found = cache.TryGet("key1", out var entry);
|
||||
|
||||
// Assert
|
||||
Assert.True(found);
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal("sha256:abc123", entry.Digest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGet_IncrementsCacheStats()
|
||||
{
|
||||
// Arrange
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SliceCacheOptions());
|
||||
using var cache = new SliceCache(options);
|
||||
var slice = CreateTestSlice();
|
||||
cache.Set("key1", slice, "sha256:abc123");
|
||||
|
||||
// Act
|
||||
cache.TryGet("key1", out _); // hit
|
||||
cache.TryGet("missing", out _); // miss
|
||||
|
||||
var stats = cache.GetStats();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, stats.HitCount);
|
||||
Assert.Equal(1, stats.MissCount);
|
||||
Assert.Equal(0.5, stats.HitRate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_RemovesAllEntries()
|
||||
{
|
||||
// Arrange
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SliceCacheOptions());
|
||||
using var cache = new SliceCache(options);
|
||||
var slice = CreateTestSlice();
|
||||
cache.Set("key1", slice, "sha256:abc123");
|
||||
cache.Set("key2", slice, "sha256:def456");
|
||||
|
||||
// Act
|
||||
cache.Clear();
|
||||
var stats = cache.GetStats();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, stats.ItemCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Invalidate_RemovesSpecificEntry()
|
||||
{
|
||||
// Arrange
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SliceCacheOptions());
|
||||
using var cache = new SliceCache(options);
|
||||
var slice = CreateTestSlice();
|
||||
cache.Set("key1", slice, "sha256:abc123");
|
||||
cache.Set("key2", slice, "sha256:def456");
|
||||
|
||||
// Act
|
||||
cache.Invalidate("key1");
|
||||
|
||||
// Assert
|
||||
Assert.False(cache.TryGet("key1", out _));
|
||||
Assert.True(cache.TryGet("key2", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Disabled_NeverCaches()
|
||||
{
|
||||
// Arrange
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SliceCacheOptions { Enabled = false });
|
||||
using var cache = new SliceCache(options);
|
||||
var slice = CreateTestSlice();
|
||||
|
||||
// Act
|
||||
cache.Set("key1", slice, "sha256:abc123");
|
||||
var found = cache.TryGet("key1", out _);
|
||||
|
||||
// Assert
|
||||
Assert.False(found);
|
||||
}
|
||||
|
||||
private static ReachabilitySlice CreateTestSlice()
|
||||
{
|
||||
return new ReachabilitySlice
|
||||
{
|
||||
Inputs = new SliceInputs { GraphDigest = "sha256:graph123" },
|
||||
Query = new SliceQuery(),
|
||||
Subgraph = new SliceSubgraph(),
|
||||
Verdict = new SliceVerdict { Status = SliceVerdictStatus.Unknown, Confidence = 0.0 },
|
||||
Manifest = new Scanner.Core.ScanManifest()
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user