190 lines
7.2 KiB
C#
190 lines
7.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using MicrosoftOptions = Microsoft.Extensions.Options;
|
|
using StellaOps.Signals.Models;
|
|
using StellaOps.Signals.Services;
|
|
using StellaOps.Signals.Storage.Postgres.Repositories;
|
|
using Xunit;
|
|
using Xunit.Abstractions;
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.Signals.Storage.Postgres.Tests;
|
|
|
|
/// <summary>
|
|
/// Integration tests for callgraph projection to relational tables.
|
|
/// </summary>
|
|
[Collection(SignalsPostgresCollection.Name)]
|
|
public sealed class CallGraphProjectionIntegrationTests : IAsyncLifetime
|
|
{
|
|
private readonly SignalsPostgresFixture _fixture;
|
|
private readonly ITestOutputHelper _output;
|
|
private readonly SignalsDataSource _dataSource;
|
|
private readonly PostgresCallGraphQueryRepository _queryRepository;
|
|
private readonly CallGraphSyncService _service;
|
|
|
|
public CallGraphProjectionIntegrationTests(SignalsPostgresFixture fixture, ITestOutputHelper output)
|
|
{
|
|
_fixture = fixture;
|
|
_output = output;
|
|
|
|
var options = fixture.Fixture.CreateOptions();
|
|
options.SchemaName = fixture.SchemaName;
|
|
_dataSource = new SignalsDataSource(MicrosoftOptions.Options.Create(options), NullLogger<SignalsDataSource>.Instance);
|
|
|
|
var projectionRepository = new PostgresCallGraphProjectionRepository(
|
|
_dataSource,
|
|
NullLogger<PostgresCallGraphProjectionRepository>.Instance);
|
|
|
|
_queryRepository = new PostgresCallGraphQueryRepository(
|
|
_dataSource,
|
|
NullLogger<PostgresCallGraphQueryRepository>.Instance);
|
|
|
|
_service = new CallGraphSyncService(
|
|
projectionRepository,
|
|
TimeProvider.System,
|
|
NullLogger<CallGraphSyncService>.Instance);
|
|
}
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
await _fixture.ExecuteSqlAsync("TRUNCATE TABLE signals.scans CASCADE;");
|
|
}
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
await _dataSource.DisposeAsync();
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task SyncAsync_ProjectsNodesToRelationalTable()
|
|
{
|
|
var scanId = Guid.NewGuid();
|
|
var document = CreateSampleDocument();
|
|
|
|
// Act
|
|
var result = await _service.SyncAsync(scanId, "sha256:test-digest", document);
|
|
|
|
// Assert
|
|
Assert.True(result.WasUpdated);
|
|
Assert.Equal(document.Nodes.Count, result.NodesProjected);
|
|
Assert.Equal(document.Edges.Count, result.EdgesProjected);
|
|
Assert.Equal(document.Entrypoints.Count, result.EntrypointsProjected);
|
|
Assert.True(result.DurationMs >= 0);
|
|
|
|
_output.WriteLine($"Projected {result.NodesProjected} nodes, {result.EdgesProjected} edges, {result.EntrypointsProjected} entrypoints in {result.DurationMs}ms");
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task SyncAsync_IsIdempotent_DoesNotCreateDuplicates()
|
|
{
|
|
var scanId = Guid.NewGuid();
|
|
var document = CreateSampleDocument();
|
|
|
|
// Act - project twice
|
|
var result1 = await _service.SyncAsync(scanId, "sha256:test-digest", document);
|
|
var result2 = await _service.SyncAsync(scanId, "sha256:test-digest", document);
|
|
|
|
// Assert - second run should update, not duplicate
|
|
Assert.Equal(result1.NodesProjected, result2.NodesProjected);
|
|
Assert.Equal(result1.EdgesProjected, result2.EdgesProjected);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task SyncAsync_WithEntrypoints_ProjectsEntrypointsCorrectly()
|
|
{
|
|
var scanId = Guid.NewGuid();
|
|
var document = new CallgraphDocument
|
|
{
|
|
Id = Guid.NewGuid().ToString("N"),
|
|
Language = "csharp",
|
|
GraphHash = "test-hash",
|
|
Nodes = new List<CallgraphNode>
|
|
{
|
|
new() { Id = "node-1", Name = "GetUsers", Namespace = "Api.Controllers" },
|
|
new() { Id = "node-2", Name = "CreateUser", Namespace = "Api.Controllers" }
|
|
},
|
|
Edges = new List<CallgraphEdge>(),
|
|
Entrypoints = new List<CallgraphEntrypoint>
|
|
{
|
|
new() { NodeId = "node-1", Kind = EntrypointKind.Http, Route = "/api/users", HttpMethod = "GET", Order = 0 },
|
|
new() { NodeId = "node-2", Kind = EntrypointKind.Http, Route = "/api/users", HttpMethod = "POST", Order = 1 }
|
|
}
|
|
};
|
|
|
|
// Act
|
|
var result = await _service.SyncAsync(scanId, "sha256:test-digest", document);
|
|
|
|
// Assert
|
|
Assert.Equal(2, result.EntrypointsProjected);
|
|
_output.WriteLine($"Projected {result.EntrypointsProjected} HTTP entrypoints");
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task DeleteByScanAsync_RemovesAllProjectedData()
|
|
{
|
|
var scanId = Guid.NewGuid();
|
|
var document = CreateSampleDocument();
|
|
|
|
// Project first
|
|
await _service.SyncAsync(scanId, "sha256:test-digest", document);
|
|
|
|
// Act
|
|
await _service.DeleteByScanAsync(scanId);
|
|
|
|
// Assert - query should return empty stats
|
|
var stats = await _queryRepository.GetStatsAsync(scanId);
|
|
Assert.Equal(0, stats.NodeCount);
|
|
Assert.Equal(0, stats.EdgeCount);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task QueryRepository_CanQueryProjectedData()
|
|
{
|
|
var scanId = Guid.NewGuid();
|
|
var document = CreateSampleDocument();
|
|
|
|
// Project
|
|
await _service.SyncAsync(scanId, "sha256:test-digest", document);
|
|
|
|
// Act
|
|
var stats = await _queryRepository.GetStatsAsync(scanId);
|
|
|
|
// Assert
|
|
Assert.Equal(document.Nodes.Count, stats.NodeCount);
|
|
Assert.Equal(document.Edges.Count, stats.EdgeCount);
|
|
_output.WriteLine($"Query returned: {stats.NodeCount} nodes, {stats.EdgeCount} edges");
|
|
}
|
|
|
|
private static CallgraphDocument CreateSampleDocument()
|
|
{
|
|
return new CallgraphDocument
|
|
{
|
|
Id = Guid.NewGuid().ToString("N"),
|
|
Language = "csharp",
|
|
GraphHash = "sha256:sample-graph-hash",
|
|
Nodes = new List<CallgraphNode>
|
|
{
|
|
new() { Id = "node-1", Name = "Main", Kind = "method", Namespace = "Program", Visibility = SymbolVisibility.Public, IsEntrypointCandidate = true },
|
|
new() { Id = "node-2", Name = "DoWork", Kind = "method", Namespace = "Service", Visibility = SymbolVisibility.Internal },
|
|
new() { Id = "node-3", Name = "ProcessData", Kind = "method", Namespace = "Core", Visibility = SymbolVisibility.Private }
|
|
},
|
|
Edges = new List<CallgraphEdge>
|
|
{
|
|
new() { SourceId = "node-1", TargetId = "node-2", Kind = EdgeKind.Static, Reason = EdgeReason.DirectCall, Weight = 1.0 },
|
|
new() { SourceId = "node-2", TargetId = "node-3", Kind = EdgeKind.Static, Reason = EdgeReason.DirectCall, Weight = 1.0 }
|
|
},
|
|
Entrypoints = new List<CallgraphEntrypoint>
|
|
{
|
|
new() { NodeId = "node-1", Kind = EntrypointKind.Main, Phase = EntrypointPhase.AppStart, Order = 0 }
|
|
}
|
|
};
|
|
}
|
|
}
|