using System.Net; using System.Net.Http.Json; using StellaOps.Scanner.WebService.Contracts; using Xunit; namespace StellaOps.Scanner.WebService.Tests; public sealed class CallGraphEndpointsTests { [Fact] public async Task SubmitCallGraphRequiresContentDigestHeader() { using var secrets = new TestSurfaceSecretsScope(); using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); using var client = factory.CreateClient(); var scanId = await CreateScanAsync(client); var request = CreateMinimalCallGraph(scanId); var response = await client.PostAsJsonAsync($"/api/v1/scans/{scanId}/callgraphs", request); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task SubmitCallGraphReturnsAcceptedAndDetectsDuplicates() { using var secrets = new TestSurfaceSecretsScope(); using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); using var client = factory.CreateClient(); var scanId = await CreateScanAsync(client); var request = CreateMinimalCallGraph(scanId); using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/scans/{scanId}/callgraphs") { Content = JsonContent.Create(request) }; httpRequest.Headers.TryAddWithoutValidation("Content-Digest", "sha256:deadbeef"); var first = await client.SendAsync(httpRequest); Assert.Equal(HttpStatusCode.Accepted, first.StatusCode); var payload = await first.Content.ReadFromJsonAsync(); Assert.NotNull(payload); Assert.False(string.IsNullOrWhiteSpace(payload!.CallgraphId)); Assert.Equal("sha256:deadbeef", payload.Digest); Assert.Equal(2, payload.NodeCount); Assert.Equal(1, payload.EdgeCount); using var secondRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/scans/{scanId}/callgraphs") { Content = JsonContent.Create(request) }; secondRequest.Headers.TryAddWithoutValidation("Content-Digest", "sha256:deadbeef"); var second = await client.SendAsync(secondRequest); Assert.Equal(HttpStatusCode.Conflict, second.StatusCode); } private static async Task CreateScanAsync(HttpClient client) { var response = await client.PostAsJsonAsync("/api/v1/scans", new ScanSubmitRequest { Image = new ScanImageDescriptor { Reference = "example.com/demo:1.0", Digest = "sha256:0123456789abcdef" } }); Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); var payload = await response.Content.ReadFromJsonAsync(); Assert.NotNull(payload); Assert.False(string.IsNullOrWhiteSpace(payload!.ScanId)); return payload.ScanId; } private static CallGraphV1Dto CreateMinimalCallGraph(string scanId) { return new CallGraphV1Dto( Schema: "stella.callgraph.v1", ScanKey: scanId, Language: "dotnet", Nodes: new[] { new CallGraphNodeDto(NodeId: "n1", SymbolKey: "Demo.Entry", ArtifactKey: null, Visibility: "public", IsEntrypointCandidate: true), new CallGraphNodeDto(NodeId: "n2", SymbolKey: "Demo.Vuln", ArtifactKey: null, Visibility: "public", IsEntrypointCandidate: false), }, Edges: new[] { new CallGraphEdgeDto(From: "n1", To: "n2", Kind: "static", Reason: "direct", Weight: 1.0) }); } }