Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly. - Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps. - Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges. - Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges. - Set up project file for the test project with necessary dependencies and configurations. - Include JSON fixture files for testing purposes.
333 lines
14 KiB
C#
333 lines
14 KiB
C#
using System.Net;
|
|
using System.Net.Http.Headers;
|
|
using System.Text.Json;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Mongo2Go;
|
|
using StellaOps.Scheduler.Models;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using StellaOps.Scheduler.WebService.PolicySimulations;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
|
|
namespace StellaOps.Scheduler.WebService.Tests;
|
|
|
|
public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplicationFactory<Program>>
|
|
{
|
|
private readonly WebApplicationFactory<Program> _factory;
|
|
|
|
public PolicySimulationEndpointTests(WebApplicationFactory<Program> factory)
|
|
{
|
|
_factory = factory;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateListGetSimulation()
|
|
{
|
|
using var client = _factory.CreateClient();
|
|
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-sim");
|
|
client.DefaultRequestHeaders.Add("X-Scopes", "policy:simulate");
|
|
|
|
var createResponse = await client.PostAsJsonAsync("/api/v1/scheduler/policies/simulations", new
|
|
{
|
|
policyId = "policy-alpha",
|
|
policyVersion = 3,
|
|
metadata = new Dictionary<string, string> { ["requestedBy"] = "unit-test" },
|
|
inputs = new
|
|
{
|
|
sbomSet = new[] { "sbom://alpha", "sbom://bravo" },
|
|
captureExplain = true
|
|
}
|
|
});
|
|
|
|
createResponse.EnsureSuccessStatusCode();
|
|
Assert.Equal(System.Net.HttpStatusCode.Created, createResponse.StatusCode);
|
|
var created = await createResponse.Content.ReadFromJsonAsync<JsonElement>();
|
|
var runId = created.GetProperty("simulation").GetProperty("runId").GetString();
|
|
Assert.False(string.IsNullOrEmpty(runId));
|
|
Assert.Equal("simulate", created.GetProperty("simulation").GetProperty("mode").GetString());
|
|
|
|
var listResponse = await client.GetAsync("/api/v1/scheduler/policies/simulations?limit=5");
|
|
listResponse.EnsureSuccessStatusCode();
|
|
var list = await listResponse.Content.ReadFromJsonAsync<JsonElement>();
|
|
Assert.True(list.GetProperty("simulations").EnumerateArray().Any());
|
|
|
|
var getResponse = await client.GetAsync($"/api/v1/scheduler/policies/simulations/{runId}");
|
|
getResponse.EnsureSuccessStatusCode();
|
|
var simulation = await getResponse.Content.ReadFromJsonAsync<JsonElement>();
|
|
Assert.Equal(runId, simulation.GetProperty("simulation").GetProperty("runId").GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MetricsEndpointWithoutProviderReturns501()
|
|
{
|
|
using var client = _factory.CreateClient();
|
|
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-sim-metrics-missing");
|
|
client.DefaultRequestHeaders.Add("X-Scopes", "policy:simulate");
|
|
|
|
var response = await client.GetAsync("/api/v1/scheduler/policies/simulations/metrics");
|
|
Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MetricsEndpointReturnsSummary()
|
|
{
|
|
var stub = new StubPolicySimulationMetricsProvider
|
|
{
|
|
Response = new PolicySimulationMetricsResponse(
|
|
new PolicySimulationQueueDepth(
|
|
3,
|
|
new Dictionary<string, long>
|
|
{
|
|
["pending"] = 2,
|
|
["dispatching"] = 1
|
|
}),
|
|
new PolicySimulationLatencyMetrics(
|
|
Samples: 2,
|
|
P50: 1.5,
|
|
P90: 2.5,
|
|
P95: 3.5,
|
|
P99: 4.0,
|
|
Mean: 2.0))
|
|
};
|
|
|
|
await using var factory = _factory.WithWebHostBuilder(builder =>
|
|
{
|
|
builder.ConfigureServices(services =>
|
|
{
|
|
services.AddSingleton<IPolicySimulationMetricsProvider>(stub);
|
|
services.AddSingleton<IPolicySimulationMetricsRecorder>(stub);
|
|
});
|
|
});
|
|
|
|
using var client = factory.CreateClient();
|
|
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-sim-metrics");
|
|
client.DefaultRequestHeaders.Add("X-Scopes", "policy:simulate");
|
|
|
|
var response = await client.GetAsync("/api/v1/scheduler/policies/simulations/metrics");
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var payload = await response.Content.ReadFromJsonAsync<JsonElement>();
|
|
Assert.Equal(3, payload.GetProperty("policy_simulation_queue_depth").GetProperty("total").GetInt32());
|
|
Assert.Equal(2, payload.GetProperty("policy_simulation_latency").GetProperty("samples").GetInt32());
|
|
Assert.Equal(2.0, payload.GetProperty("policy_simulation_latency").GetProperty("mean_seconds").GetDouble());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateSimulationRequiresScopeHeader()
|
|
{
|
|
using var client = _factory.CreateClient();
|
|
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-sim-auth");
|
|
|
|
var response = await client.PostAsJsonAsync("/api/v1/scheduler/policies/simulations", new
|
|
{
|
|
policyId = "policy-auth",
|
|
policyVersion = 1
|
|
});
|
|
|
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateSimulationRequiresPolicySimulateScope()
|
|
{
|
|
using var client = _factory.CreateClient();
|
|
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-sim-authz");
|
|
client.DefaultRequestHeaders.Add("X-Scopes", "policy:run");
|
|
|
|
var response = await client.PostAsJsonAsync("/api/v1/scheduler/policies/simulations", new
|
|
{
|
|
policyId = "policy-authz",
|
|
policyVersion = 2
|
|
});
|
|
|
|
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CancelSimulationMarksStatus()
|
|
{
|
|
using var client = _factory.CreateClient();
|
|
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-sim-cancel");
|
|
client.DefaultRequestHeaders.Add("X-Scopes", "policy:simulate");
|
|
|
|
var create = await client.PostAsJsonAsync("/api/v1/scheduler/policies/simulations", new
|
|
{
|
|
policyId = "policy-bravo",
|
|
policyVersion = 2
|
|
});
|
|
create.EnsureSuccessStatusCode();
|
|
var runId = (await create.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("simulation").GetProperty("runId").GetString();
|
|
|
|
var cancel = await client.PostAsJsonAsync($"/api/v1/scheduler/policies/simulations/{runId}/cancel", new
|
|
{
|
|
reason = "user-request"
|
|
});
|
|
|
|
cancel.EnsureSuccessStatusCode();
|
|
var cancelled = await cancel.Content.ReadFromJsonAsync<JsonElement>();
|
|
Assert.True(cancelled.GetProperty("simulation").GetProperty("cancellationRequested").GetBoolean());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RetrySimulationCreatesNewRun()
|
|
{
|
|
using var client = _factory.CreateClient();
|
|
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-sim-retry");
|
|
client.DefaultRequestHeaders.Add("X-Scopes", "policy:simulate");
|
|
|
|
var create = await client.PostAsJsonAsync("/api/v1/scheduler/policies/simulations", new
|
|
{
|
|
policyId = "policy-charlie",
|
|
policyVersion = 5
|
|
});
|
|
create.EnsureSuccessStatusCode();
|
|
var runId = (await create.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("simulation").GetProperty("runId").GetString();
|
|
|
|
// Mark as cancelled to allow retry
|
|
await client.PostAsJsonAsync($"/api/v1/scheduler/policies/simulations/{runId}/cancel", new { reason = "cleanup" });
|
|
|
|
var retry = await client.PostAsync($"/api/v1/scheduler/policies/simulations/{runId}/retry", content: null);
|
|
retry.EnsureSuccessStatusCode();
|
|
Assert.Equal(System.Net.HttpStatusCode.Created, retry.StatusCode);
|
|
var retried = await retry.Content.ReadFromJsonAsync<JsonElement>();
|
|
var newRunId = retried.GetProperty("simulation").GetProperty("runId").GetString();
|
|
Assert.False(string.IsNullOrEmpty(newRunId));
|
|
Assert.NotEqual(runId, newRunId);
|
|
var metadata = retried.GetProperty("simulation").GetProperty("metadata");
|
|
Assert.True(metadata.TryGetProperty("retry-of", out var retryOf));
|
|
Assert.Equal(runId, retryOf.GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task StreamSimulationEmitsCoreEvents()
|
|
{
|
|
using var client = _factory.CreateClient();
|
|
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-sim-stream");
|
|
client.DefaultRequestHeaders.Add("X-Scopes", "policy:simulate");
|
|
|
|
var create = await client.PostAsJsonAsync("/api/v1/scheduler/policies/simulations", new
|
|
{
|
|
policyId = "policy-delta",
|
|
policyVersion = 7
|
|
});
|
|
create.EnsureSuccessStatusCode();
|
|
var runId = (await create.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("simulation").GetProperty("runId").GetString();
|
|
|
|
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/scheduler/policies/simulations/{runId}/stream");
|
|
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
|
|
|
|
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
await using var stream = await response.Content.ReadAsStreamAsync();
|
|
using var reader = new StreamReader(stream);
|
|
|
|
var seenRetry = false;
|
|
var seenInitial = false;
|
|
var seenQueueLag = false;
|
|
var seenHeartbeat = false;
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
|
while (!cts.Token.IsCancellationRequested && !(seenRetry && seenInitial && seenQueueLag && seenHeartbeat))
|
|
{
|
|
var readTask = reader.ReadLineAsync();
|
|
var completed = await Task.WhenAny(readTask, Task.Delay(200, cts.Token));
|
|
if (completed != readTask)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var line = await readTask;
|
|
if (line is null)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (line.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (line.StartsWith("retry:", StringComparison.Ordinal))
|
|
{
|
|
seenRetry = true;
|
|
}
|
|
else if (line.StartsWith("event: initial", StringComparison.Ordinal))
|
|
{
|
|
seenInitial = true;
|
|
}
|
|
else if (line.StartsWith("event: queueLag", StringComparison.Ordinal))
|
|
{
|
|
seenQueueLag = true;
|
|
}
|
|
else if (line.StartsWith("event: heartbeat", StringComparison.Ordinal))
|
|
{
|
|
seenHeartbeat = true;
|
|
}
|
|
}
|
|
|
|
Assert.True(seenRetry, "Retry directive should be emitted before events.");
|
|
Assert.True(seenInitial, "Initial event was not observed.");
|
|
Assert.True(seenQueueLag, "Queue lag event was not observed.");
|
|
Assert.True(seenHeartbeat, "Heartbeat event was not observed.");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MongoBackedCreateSimulationPersists()
|
|
{
|
|
using var runner = MongoDbRunner.Start(additionalMongodArguments: "--quiet");
|
|
await using var factory = _factory.WithWebHostBuilder(builder =>
|
|
{
|
|
builder.ConfigureAppConfiguration((_, configuration) =>
|
|
{
|
|
configuration.AddInMemoryCollection(new[]
|
|
{
|
|
new KeyValuePair<string, string?>("Scheduler:Storage:ConnectionString", runner.ConnectionString),
|
|
new KeyValuePair<string, string?>("Scheduler:Storage:Database", $"scheduler_web_tests_{Guid.NewGuid():N}")
|
|
});
|
|
});
|
|
});
|
|
|
|
using var client = factory.CreateClient();
|
|
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-sim-mongo");
|
|
client.DefaultRequestHeaders.Add("X-Scopes", "policy:simulate");
|
|
|
|
var createResponse = await client.PostAsJsonAsync("/api/v1/scheduler/policies/simulations", new
|
|
{
|
|
policyId = "policy-mongo",
|
|
policyVersion = 11
|
|
});
|
|
createResponse.EnsureSuccessStatusCode();
|
|
var runId = (await createResponse.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("simulation").GetProperty("runId").GetString();
|
|
Assert.False(string.IsNullOrEmpty(runId));
|
|
|
|
var fetched = await client.GetAsync($"/api/v1/scheduler/policies/simulations/{runId}");
|
|
fetched.EnsureSuccessStatusCode();
|
|
var payload = await fetched.Content.ReadFromJsonAsync<JsonElement>();
|
|
Assert.Equal(runId, payload.GetProperty("simulation").GetProperty("runId").GetString());
|
|
}
|
|
|
|
private sealed class StubPolicySimulationMetricsProvider : IPolicySimulationMetricsProvider, IPolicySimulationMetricsRecorder
|
|
{
|
|
public PolicySimulationMetricsResponse Response { get; set; } = new(
|
|
new PolicySimulationQueueDepth(0, new Dictionary<string, long>()),
|
|
new PolicySimulationLatencyMetrics(0, null, null, null, null, null));
|
|
|
|
public List<double> RecordedLatencies { get; } = new();
|
|
|
|
public Task<PolicySimulationMetricsResponse> CaptureAsync(string tenantId, CancellationToken cancellationToken)
|
|
=> Task.FromResult(Response);
|
|
|
|
public void RecordLatency(PolicyRunStatus status, DateTimeOffset observedAt)
|
|
{
|
|
var finishedAt = status.FinishedAt ?? observedAt;
|
|
var latency = (finishedAt - status.QueuedAt).TotalSeconds;
|
|
if (latency >= 0)
|
|
{
|
|
RecordedLatencies.Add(latency);
|
|
}
|
|
}
|
|
}
|
|
}
|