Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Tests.TestInfrastructure;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class CallgraphIngestionTests : IClassFixture<SignalsTestFactory>
|
||||
{
|
||||
private readonly SignalsTestFactory factory;
|
||||
|
||||
public CallgraphIngestionTests(SignalsTestFactory factory)
|
||||
{
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("java")]
|
||||
[InlineData("nodejs")]
|
||||
[InlineData("python")]
|
||||
[InlineData("go")]
|
||||
public async Task Ingest_Callgraph_PersistsDocumentAndArtifact(string language)
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Scopes", "signals:write");
|
||||
|
||||
var component = $"demo-{language}";
|
||||
var request = CreateRequest(language, component: component);
|
||||
var response = await client.PostAsJsonAsync("/signals/callgraphs", request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<CallgraphIngestResponse>();
|
||||
Assert.NotNull(body);
|
||||
|
||||
var database = new MongoClient(factory.MongoRunner.ConnectionString).GetDatabase("signals-tests");
|
||||
var collection = database.GetCollection<CallgraphDocument>("callgraphs");
|
||||
var doc = await collection.Find(d => d.Id == body!.CallgraphId).FirstOrDefaultAsync();
|
||||
|
||||
Assert.NotNull(doc);
|
||||
Assert.Equal(language, doc!.Language);
|
||||
Assert.Equal(component, doc.Component);
|
||||
Assert.Equal("1.0.0", doc.Version);
|
||||
Assert.Equal(2, doc.Nodes.Count);
|
||||
Assert.Equal(1, doc.Edges.Count);
|
||||
|
||||
var artifactPath = Path.Combine(factory.StoragePath, body.ArtifactPath);
|
||||
Assert.True(File.Exists(artifactPath));
|
||||
Assert.False(string.IsNullOrWhiteSpace(body.ArtifactHash));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ingest_UnsupportedLanguage_ReturnsBadRequest()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Scopes", "signals:write");
|
||||
|
||||
var request = CreateRequest("ruby");
|
||||
var response = await client.PostAsJsonAsync("/signals/callgraphs", request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ingest_InvalidArtifactContent_ReturnsBadRequest()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Scopes", "signals:write");
|
||||
|
||||
var request = CreateRequest("java") with { ArtifactContentBase64 = "not-base64" };
|
||||
var response = await client.PostAsJsonAsync("/signals/callgraphs", request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ingest_InvalidGraphStructure_ReturnsUnprocessableEntity()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Scopes", "signals:write");
|
||||
|
||||
var json = "{\"formatVersion\":\"1.0\",\"graph\":{}}";
|
||||
var request = CreateRequest("java", json);
|
||||
var response = await client.PostAsJsonAsync("/signals/callgraphs", request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ingest_SameComponentUpsertsDocument()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Scopes", "signals:write");
|
||||
|
||||
var firstRequest = CreateRequest("python");
|
||||
var secondJson = "{\"graph\":{\"nodes\":[{\"id\":\"module.entry\",\"name\":\"module.entry\"}],\"edges\":[]}}";
|
||||
var secondRequest = CreateRequest("python", secondJson);
|
||||
|
||||
var firstResponse = await client.PostAsJsonAsync("/signals/callgraphs", firstRequest);
|
||||
var secondResponse = await client.PostAsJsonAsync("/signals/callgraphs", secondRequest);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Accepted, firstResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.Accepted, secondResponse.StatusCode);
|
||||
|
||||
var database = new MongoClient(factory.MongoRunner.ConnectionString).GetDatabase("signals-tests");
|
||||
var collection = database.GetCollection<CallgraphDocument>("callgraphs");
|
||||
var count = await collection.CountDocumentsAsync(FilterDefinition<CallgraphDocument>.Empty);
|
||||
|
||||
Assert.Equal(1, count);
|
||||
var doc = await collection.Find(_ => true).FirstAsync();
|
||||
Assert.Single(doc.Nodes);
|
||||
Assert.Equal("python", doc.Language);
|
||||
}
|
||||
|
||||
private static CallgraphIngestRequest CreateRequest(string language, string? customJson = null, string component = "demo")
|
||||
{
|
||||
var json = customJson ?? "{\"formatVersion\":\"1.0\",\"graph\":{\"nodes\":[{\"id\":\"main.entry\",\"name\":\"main.entry\",\"kind\":\"function\",\"file\":\"main\",\"line\":1},{\"id\":\"helper.run\",\"name\":\"helper.run\",\"kind\":\"function\",\"file\":\"helper\",\"line\":2}],\"edges\":[{\"source\":\"main.entry\",\"target\":\"helper.run\",\"type\":\"call\"}]}}";
|
||||
var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
|
||||
|
||||
return new CallgraphIngestRequest(
|
||||
Language: language,
|
||||
Component: component,
|
||||
Version: "1.0.0",
|
||||
ArtifactContentType: "application/json",
|
||||
ArtifactFileName: $"{language}-callgraph.json",
|
||||
ArtifactContentBase64: base64,
|
||||
Metadata: new Dictionary<string, string?>
|
||||
{
|
||||
["source"] = "unit-test"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Signals.Tests.TestInfrastructure;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class SignalsApiTests : IClassFixture<SignalsTestFactory>
|
||||
{
|
||||
private readonly SignalsTestFactory factory;
|
||||
|
||||
public SignalsApiTests(SignalsTestFactory factory)
|
||||
{
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Healthz_ReturnsOk()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
var response = await client.GetAsync("/healthz");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Readyz_ReturnsOk()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
var response = await client.GetAsync("/readyz");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var payload = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
||||
Assert.NotNull(payload);
|
||||
Assert.Equal("ready", payload!["status"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ping_WithoutScopeHeader_ReturnsUnauthorized()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
var response = await client.GetAsync("/signals/ping");
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ping_WithMissingScope_ReturnsForbidden()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Scopes", "signals:write");
|
||||
var response = await client.GetAsync("/signals/ping");
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ping_WithReadScope_ReturnsNoContent()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Scopes", "signals:read");
|
||||
var response = await client.GetAsync("/signals/ping");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ping_WithFallbackDisabled_ReturnsUnauthorized()
|
||||
{
|
||||
using var app = factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||
{
|
||||
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Signals:Authority:AllowAnonymousFallback"] = "false"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
using var client = app.CreateClient();
|
||||
var response = await client.GetAsync("/signals/ping");
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Status_WithReadScope_ReturnsOk()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Scopes", "signals:read");
|
||||
var response = await client.GetAsync("/signals/status");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var payload = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
||||
Assert.NotNull(payload);
|
||||
Assert.Equal("signals", payload!["service"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Status_WithMissingScope_ReturnsForbidden()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Scopes", "signals:write");
|
||||
var response = await client.GetAsync("/signals/status");
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="Mongo2Go" Version="4.1.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../../Signals/StellaOps.Signals/StellaOps.Signals.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Mongo2Go;
|
||||
|
||||
namespace StellaOps.Signals.Tests.TestInfrastructure;
|
||||
|
||||
internal sealed class SignalsTestFactory : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
{
|
||||
private readonly MongoDbRunner mongoRunner;
|
||||
private readonly string storagePath;
|
||||
|
||||
public SignalsTestFactory()
|
||||
{
|
||||
mongoRunner = MongoDbRunner.Start(singleNodeReplSet: true);
|
||||
storagePath = Path.Combine(Path.GetTempPath(), "signals-tests", Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(storagePath);
|
||||
}
|
||||
|
||||
public string StoragePath => storagePath;
|
||||
|
||||
public MongoDbRunner MongoRunner => mongoRunner;
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.ConfigureAppConfiguration((context, configuration) =>
|
||||
{
|
||||
var settings = new Dictionary<string, string?>
|
||||
{
|
||||
["Signals:Authority:Enabled"] = "false",
|
||||
["Signals:Authority:AllowAnonymousFallback"] = "true",
|
||||
["Signals:Mongo:ConnectionString"] = mongoRunner.ConnectionString,
|
||||
["Signals:Mongo:Database"] = "signals-tests",
|
||||
["Signals:Mongo:CallgraphsCollection"] = "callgraphs",
|
||||
["Signals:Storage:RootPath"] = storagePath
|
||||
};
|
||||
|
||||
configuration.AddInMemoryCollection(settings);
|
||||
});
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await Task.Run(() => mongoRunner.Dispose());
|
||||
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(storagePath))
|
||||
{
|
||||
Directory.Delete(storagePath, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best effort cleanup.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user