Files
git.stella-ops.org/src/ReachGraph/__Tests/StellaOps.ReachGraph.WebService.Tests/ReachGraphApiIntegrationTests.cs

306 lines
11 KiB
C#

// Licensed to StellaOps under the BUSL-1.1 license.
using System.Collections.Immutable;
using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.ReachGraph.Schema;
using StellaOps.ReachGraph.WebService.Models;
using Xunit;
namespace StellaOps.ReachGraph.WebService.Tests;
/// <summary>
/// Integration tests for the ReachGraph Store API.
/// Uses in-memory providers for quick testing without containers.
/// </summary>
public class ReachGraphApiIntegrationTests : IClassFixture<ReachGraphTestFactory>
{
private readonly HttpClient _client;
private const string TenantHeader = "X-Tenant-ID";
private const string TestTenant = "test-tenant";
public ReachGraphApiIntegrationTests(ReachGraphTestFactory factory)
{
_client = factory.CreateClient();
_client.DefaultRequestHeaders.Add(TenantHeader, TestTenant);
}
[Fact]
public async Task Upsert_ValidGraph_ReturnsCreated()
{
// Arrange
var graph = CreateSampleGraph();
var request = new UpsertReachGraphRequest { Graph = graph };
// Act
var response = await _client.PostAsJsonAsync("/v1/reachgraphs", request);
// Assert
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<UpsertReachGraphResponse>();
Assert.NotNull(result);
Assert.True(result.Created);
Assert.StartsWith("blake3:", result.Digest);
Assert.Equal(graph.Nodes.Length, result.NodeCount);
Assert.Equal(graph.Edges.Length, result.EdgeCount);
}
[Fact]
public async Task Upsert_SameGraph_ReturnsOk()
{
// Arrange
var graph = CreateSampleGraph();
var request = new UpsertReachGraphRequest { Graph = graph };
// Act - upsert twice
var response1 = await _client.PostAsJsonAsync("/v1/reachgraphs", request);
var response2 = await _client.PostAsJsonAsync("/v1/reachgraphs", request);
// Assert - first is Created, second is OK (idempotent)
Assert.Equal(HttpStatusCode.Created, response1.StatusCode);
Assert.Equal(HttpStatusCode.OK, response2.StatusCode);
var result1 = await response1.Content.ReadFromJsonAsync<UpsertReachGraphResponse>();
var result2 = await response2.Content.ReadFromJsonAsync<UpsertReachGraphResponse>();
Assert.True(result1!.Created);
Assert.False(result2!.Created);
Assert.Equal(result1.Digest, result2.Digest);
}
[Fact]
public async Task GetByDigest_ExistingGraph_ReturnsGraph()
{
// Arrange
var graph = CreateSampleGraph();
var upsertRequest = new UpsertReachGraphRequest { Graph = graph };
var upsertResponse = await _client.PostAsJsonAsync("/v1/reachgraphs", upsertRequest);
var upsertResult = await upsertResponse.Content.ReadFromJsonAsync<UpsertReachGraphResponse>();
// Act
var response = await _client.GetAsync($"/v1/reachgraphs/{upsertResult!.Digest}");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<ReachGraphMinimal>();
Assert.NotNull(result);
Assert.Equal(graph.SchemaVersion, result.SchemaVersion);
Assert.Equal(graph.Artifact.Name, result.Artifact.Name);
}
[Fact]
public async Task GetByDigest_NonExisting_ReturnsNotFound()
{
// Arrange
var nonExistingDigest = "blake3:0000000000000000000000000000000000000000000000000000000000000000";
// Act
var response = await _client.GetAsync($"/v1/reachgraphs/{nonExistingDigest}");
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task SliceByCve_ReturnsRelevantNodes()
{
// Arrange
var graph = CreateGraphWithCve();
var upsertRequest = new UpsertReachGraphRequest { Graph = graph };
var upsertResponse = await _client.PostAsJsonAsync("/v1/reachgraphs", upsertRequest);
var upsertResult = await upsertResponse.Content.ReadFromJsonAsync<UpsertReachGraphResponse>();
// Act
var response = await _client.GetAsync($"/v1/reachgraphs/{upsertResult!.Digest}/slice?cve=CVE-2024-1234");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<CveSliceResponse>();
Assert.NotNull(result);
Assert.Equal("cve", result.SliceQuery.Type);
Assert.Equal("CVE-2024-1234", result.SliceQuery.Cve);
}
[Fact]
public async Task SliceByPackage_ReturnsConnectedNodes()
{
// Arrange
var graph = CreateSampleGraph();
var upsertRequest = new UpsertReachGraphRequest { Graph = graph };
var upsertResponse = await _client.PostAsJsonAsync("/v1/reachgraphs", upsertRequest);
var upsertResult = await upsertResponse.Content.ReadFromJsonAsync<UpsertReachGraphResponse>();
// Act
var response = await _client.GetAsync(
$"/v1/reachgraphs/{upsertResult!.Digest}/slice?q=pkg:npm/*&depth=2");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<SliceQueryResponse>();
Assert.NotNull(result);
Assert.Equal("package", result.SliceQuery.Type);
}
[Fact]
public async Task Replay_MatchingInputs_ReturnsMatch()
{
// Arrange
var graph = CreateSampleGraph();
var upsertRequest = new UpsertReachGraphRequest { Graph = graph };
var upsertResponse = await _client.PostAsJsonAsync("/v1/reachgraphs", upsertRequest);
var upsertResult = await upsertResponse.Content.ReadFromJsonAsync<UpsertReachGraphResponse>();
var replayRequest = new ReplayRequest
{
ExpectedDigest = upsertResult!.Digest,
Inputs = new ReplayInputs
{
Sbom = graph.Provenance.Inputs.Sbom,
Vex = graph.Provenance.Inputs.Vex,
Callgraph = graph.Provenance.Inputs.Callgraph
}
};
// Act
var response = await _client.PostAsJsonAsync("/v1/reachgraphs/replay", replayRequest);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<ReplayResponse>();
Assert.NotNull(result);
Assert.True(result.Match);
Assert.Equal(upsertResult.Digest, result.ComputedDigest);
Assert.True(result.InputsVerified?.Sbom);
}
[Fact]
public async Task ListByArtifact_ReturnsSubgraphs()
{
// Arrange
var graph = CreateSampleGraph();
var upsertRequest = new UpsertReachGraphRequest { Graph = graph };
await _client.PostAsJsonAsync("/v1/reachgraphs", upsertRequest);
// Act
var response = await _client.GetAsync(
$"/v1/reachgraphs/by-artifact/{Uri.EscapeDataString(graph.Artifact.Digest)}");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<ListByArtifactResponse>();
Assert.NotNull(result);
Assert.True(result.TotalCount >= 1);
}
[Fact]
public async Task Delete_ExistingGraph_ReturnsNoContent()
{
// Arrange
var graph = CreateSampleGraph();
var upsertRequest = new UpsertReachGraphRequest { Graph = graph };
var upsertResponse = await _client.PostAsJsonAsync("/v1/reachgraphs", upsertRequest);
var upsertResult = await upsertResponse.Content.ReadFromJsonAsync<UpsertReachGraphResponse>();
// Act
var response = await _client.DeleteAsync($"/v1/reachgraphs/{upsertResult!.Digest}");
// Assert
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
// Verify it's gone
var getResponse = await _client.GetAsync($"/v1/reachgraphs/{upsertResult.Digest}");
Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
}
#region Helper Methods
private static ReachGraphMinimal CreateSampleGraph() => new()
{
SchemaVersion = "reachgraph.min@v1",
Artifact = new ReachGraphArtifact(
"test-app:v1.0.0",
"sha256:abc123def456789abc123def456789abc123def456789abc123def456789abc1",
["linux/amd64"]),
Scope = new ReachGraphScope(
["/app/main"],
["prod"]),
Nodes =
[
new ReachGraphNode
{
Id = "sha256:entry1",
Kind = ReachGraphNodeKind.Function,
Ref = "main()",
File = "src/main.ts",
Line = 1,
IsEntrypoint = true
},
new ReachGraphNode
{
Id = "sha256:pkg1",
Kind = ReachGraphNodeKind.Package,
Ref = "pkg:npm/lodash@4.17.21"
},
new ReachGraphNode
{
Id = "sha256:sink1",
Kind = ReachGraphNodeKind.Function,
Ref = "lodash.template()",
File = "node_modules/lodash/template.js",
Line = 100,
IsSink = true
}
],
Edges =
[
new ReachGraphEdge
{
From = "sha256:entry1",
To = "sha256:pkg1",
Why = new EdgeExplanation
{
Type = EdgeExplanationType.Import,
Loc = "src/main.ts:3",
Confidence = 1.0
}
},
new ReachGraphEdge
{
From = "sha256:pkg1",
To = "sha256:sink1",
Why = new EdgeExplanation
{
Type = EdgeExplanationType.Import,
Confidence = 1.0
}
}
],
Provenance = new ReachGraphProvenance
{
Inputs = new ReachGraphInputs
{
Sbom = "sha256:sbom123abc456def789",
Vex = "sha256:vex456def789abc123",
Callgraph = "sha256:cg789abc123def456"
},
ComputedAt = DateTimeOffset.UtcNow,
Analyzer = new ReachGraphAnalyzer(
"stellaops-scanner",
"1.0.0",
"sha256:toolchain123456789")
}
};
private static ReachGraphMinimal CreateGraphWithCve() => CreateSampleGraph() with
{
Scope = new ReachGraphScope(
["/app/main"],
["prod"],
["CVE-2024-1234"])
};
#endregion
}