Merge branch 'main' of https://git.stella-ops.org/stella-ops.org/git.stella-ops.org
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
[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");
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[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");
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[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 }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user