Add Authority Advisory AI and API Lifecycle Configuration
- Introduced AuthorityAdvisoryAiOptions and related classes for managing advisory AI configurations, including remote inference options and tenant-specific settings. - Added AuthorityApiLifecycleOptions to control API lifecycle settings, including legacy OAuth endpoint configurations. - Implemented validation and normalization methods for both advisory AI and API lifecycle options to ensure proper configuration. - Created AuthorityNotificationsOptions and its related classes for managing notification settings, including ack tokens, webhooks, and escalation options. - Developed IssuerDirectoryClient and related models for interacting with the issuer directory service, including caching mechanisms and HTTP client configurations. - Added support for dependency injection through ServiceCollectionExtensions for the Issuer Directory Client. - Updated project file to include necessary package references for the new Issuer Directory Client library.
This commit is contained in:
@@ -1,18 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Scanner.EntryTrace;
|
||||
using StellaOps.Scanner.EntryTrace.Serialization;
|
||||
using StellaOps.Scanner.Storage.Catalog;
|
||||
using StellaOps.Scanner.Storage.Repositories;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
@@ -126,14 +132,74 @@ public sealed class ScansEndpointsTests
|
||||
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||
|
||||
Assert.NotNull(coordinator);
|
||||
Assert.True(coordinator.TokenMatched);
|
||||
Assert.True(coordinator.LastToken.CanBeCanceled);
|
||||
}
|
||||
|
||||
private sealed class RecordingCoordinator : IScanCoordinator
|
||||
{
|
||||
private readonly IHttpContextAccessor accessor;
|
||||
private readonly InMemoryScanCoordinator inner;
|
||||
Assert.True(coordinator.TokenMatched);
|
||||
Assert.True(coordinator.LastToken.CanBeCanceled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EntryTraceEndpointReturnsStoredResult()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
var scanId = $"scan-entrytrace-{Guid.NewGuid():n}";
|
||||
var graph = new EntryTraceGraph(
|
||||
EntryTraceOutcome.Resolved,
|
||||
ImmutableArray<EntryTraceNode>.Empty,
|
||||
ImmutableArray<EntryTraceEdge>.Empty,
|
||||
ImmutableArray<EntryTraceDiagnostic>.Empty,
|
||||
ImmutableArray.Create(new EntryTracePlan(
|
||||
ImmutableArray.Create("/bin/bash", "-lc", "./start.sh"),
|
||||
ImmutableDictionary<string, string>.Empty,
|
||||
"/workspace",
|
||||
"root",
|
||||
"/bin/bash",
|
||||
EntryTraceTerminalType.Script,
|
||||
"bash",
|
||||
0.9,
|
||||
ImmutableDictionary<string, string>.Empty)),
|
||||
ImmutableArray.Create(new EntryTraceTerminal(
|
||||
"/bin/bash",
|
||||
EntryTraceTerminalType.Script,
|
||||
"bash",
|
||||
0.9,
|
||||
ImmutableDictionary<string, string>.Empty,
|
||||
"root",
|
||||
"/workspace",
|
||||
ImmutableArray<string>.Empty)));
|
||||
|
||||
var ndjson = new List<string> { "{\"kind\":\"entry\"}" };
|
||||
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var repository = scope.ServiceProvider.GetRequiredService<EntryTraceRepository>();
|
||||
await repository.UpsertAsync(new EntryTraceDocument
|
||||
{
|
||||
ScanId = scanId,
|
||||
ImageDigest = "sha256:entrytrace",
|
||||
GeneratedAtUtc = DateTime.UtcNow,
|
||||
GraphJson = EntryTraceGraphSerializer.Serialize(graph),
|
||||
Ndjson = ndjson
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
var response = await client.GetAsync($"/api/v1/scans/{scanId}/entrytrace");
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var payload = await response.Content.ReadFromJsonAsync<EntryTraceResponse>();
|
||||
Assert.NotNull(payload);
|
||||
Assert.Equal(scanId, payload!.ScanId);
|
||||
Assert.Equal("sha256:entrytrace", payload.ImageDigest);
|
||||
Assert.Equal(graph.Outcome, payload.Graph.Outcome);
|
||||
Assert.Single(payload.Graph.Plans);
|
||||
Assert.Equal("/bin/bash", payload.Graph.Plans[0].TerminalPath);
|
||||
Assert.Single(payload.Graph.Terminals);
|
||||
Assert.Equal(ndjson, payload.Ndjson);
|
||||
}
|
||||
|
||||
private sealed class RecordingCoordinator : IScanCoordinator
|
||||
{
|
||||
private readonly IHttpContextAccessor accessor;
|
||||
private readonly InMemoryScanCoordinator inner;
|
||||
|
||||
public RecordingCoordinator(IHttpContextAccessor accessor, TimeProvider timeProvider, IScanProgressPublisher publisher)
|
||||
{
|
||||
@@ -358,15 +424,111 @@ public sealed class ScansEndpointsTests
|
||||
Assert.Equal(new[] { "alpha", "Beta", "zeta" }, names);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetEntryTraceReturnsStoredResult()
|
||||
{
|
||||
var scanId = $"scan-{Guid.NewGuid():n}";
|
||||
var generatedAt = new DateTimeOffset(2025, 11, 1, 12, 0, 0, TimeSpan.Zero);
|
||||
var plan = new EntryTracePlan(
|
||||
ImmutableArray.Create("/usr/local/bin/app"),
|
||||
ImmutableDictionary<string, string>.Empty,
|
||||
"/workspace",
|
||||
"appuser",
|
||||
"/usr/local/bin/app",
|
||||
EntryTraceTerminalType.Native,
|
||||
"go",
|
||||
90d,
|
||||
ImmutableDictionary<string, string>.Empty);
|
||||
var terminal = new EntryTraceTerminal(
|
||||
"/usr/local/bin/app",
|
||||
EntryTraceTerminalType.Native,
|
||||
"go",
|
||||
90d,
|
||||
ImmutableDictionary<string, string>.Empty,
|
||||
"appuser",
|
||||
"/workspace",
|
||||
ImmutableArray<string>.Empty);
|
||||
var graph = new EntryTraceGraph(
|
||||
EntryTraceOutcome.Resolved,
|
||||
ImmutableArray<EntryTraceNode>.Empty,
|
||||
ImmutableArray<EntryTraceEdge>.Empty,
|
||||
ImmutableArray<EntryTraceDiagnostic>.Empty,
|
||||
ImmutableArray.Create(plan),
|
||||
ImmutableArray.Create(terminal));
|
||||
var ndjson = EntryTraceNdjsonWriter.Serialize(
|
||||
graph,
|
||||
new EntryTraceNdjsonMetadata(scanId, "sha256:test", generatedAt));
|
||||
var storedResult = new EntryTraceResult(scanId, "sha256:test", generatedAt, graph, ndjson);
|
||||
|
||||
using var factory = new ScannerApplicationFactory(
|
||||
configuration: null,
|
||||
services =>
|
||||
{
|
||||
services.AddSingleton<IEntryTraceResultStore>(new StubEntryTraceResultStore(storedResult));
|
||||
});
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
var response = await client.GetAsync($"/api/v1/scans/{scanId}/entrytrace");
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var payload = await response.Content.ReadFromJsonAsync<EntryTraceResponse>();
|
||||
Assert.NotNull(payload);
|
||||
Assert.Equal(storedResult.ScanId, payload!.ScanId);
|
||||
Assert.Equal(storedResult.ImageDigest, payload.ImageDigest);
|
||||
Assert.Equal(storedResult.GeneratedAtUtc, payload.GeneratedAt);
|
||||
Assert.Equal(storedResult.Graph.Plans.Length, payload.Graph.Plans.Length);
|
||||
Assert.Equal(storedResult.Ndjson, payload.Ndjson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetEntryTraceReturnsNotFoundWhenMissing()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory(
|
||||
configuration: null,
|
||||
services =>
|
||||
{
|
||||
services.AddSingleton<IEntryTraceResultStore>(new StubEntryTraceResultStore(null));
|
||||
});
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
var response = await client.GetAsync("/api/v1/scans/scan-missing/entrytrace");
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
private sealed record ProgressEnvelope(
|
||||
string ScanId,
|
||||
int Sequence,
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
private sealed record ProgressEnvelope(
|
||||
string ScanId,
|
||||
int Sequence,
|
||||
string State,
|
||||
string? Message,
|
||||
DateTimeOffset Timestamp,
|
||||
string CorrelationId,
|
||||
Dictionary<string, JsonElement> Data);
|
||||
}
|
||||
string CorrelationId,
|
||||
Dictionary<string, JsonElement> Data);
|
||||
|
||||
private sealed class StubEntryTraceResultStore : IEntryTraceResultStore
|
||||
{
|
||||
private readonly EntryTraceResult? _result;
|
||||
|
||||
public StubEntryTraceResultStore(EntryTraceResult? result)
|
||||
{
|
||||
_result = result;
|
||||
}
|
||||
|
||||
public Task<EntryTraceResult?> GetAsync(string scanId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_result is not null && string.Equals(_result.ScanId, scanId, StringComparison.Ordinal))
|
||||
{
|
||||
return Task.FromResult<EntryTraceResult?>(_result);
|
||||
}
|
||||
|
||||
return Task.FromResult<EntryTraceResult?>(null);
|
||||
}
|
||||
|
||||
public Task StoreAsync(EntryTraceResult result, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user