more audit work
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
|
||||
using LedgerProgram = StellaOps.Findings.Ledger.WebService.Program;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests;
|
||||
|
||||
internal sealed class FindingsLedgerWebApplicationFactory : WebApplicationFactory<LedgerProgram>
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, string?> DefaultEnvironment =
|
||||
new Dictionary<string, string?>(StringComparer.Ordinal)
|
||||
{
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__DATABASE__CONNECTIONSTRING"] = "Host=localhost;Database=stellaops_test;Username=stella;Password=stella",
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__ATTACHMENTS__ENCRYPTIONKEY"] = "test-encryption-key",
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__ATTACHMENTS__SIGNEDURLSECRET"] = "test-signed-url-secret",
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__ATTACHMENTS__CSRFSHAREDSECRET"] = "test-csrf-secret",
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__AUTHORITY__ISSUER"] = "https://authority.local",
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__AUTHORITY__REQUIREHTTPSMETADATA"] = "false",
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__AUTHORITY__REQUIREDSCOPES__0"] = StellaOpsScopes.FindingsRead,
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__AUTHORITY__REQUIREDSCOPES__1"] = "findings:write"
|
||||
};
|
||||
|
||||
private readonly Dictionary<string, string?> originalEnvironment = new(StringComparer.Ordinal);
|
||||
|
||||
public FindingsLedgerWebApplicationFactory()
|
||||
{
|
||||
foreach (var pair in DefaultEnvironment)
|
||||
{
|
||||
originalEnvironment[pair.Key] = Environment.GetEnvironmentVariable(pair.Key);
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.UseEnvironment("Production");
|
||||
builder.UseDefaultServiceProvider(options =>
|
||||
{
|
||||
options.ValidateScopes = false;
|
||||
options.ValidateOnBuild = false;
|
||||
});
|
||||
|
||||
builder.ConfigureAppConfiguration((context, configBuilder) =>
|
||||
{
|
||||
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["findings:ledger:database:connectionString"] = "Host=localhost;Database=stellaops_test;Username=stella;Password=stella",
|
||||
["findings:ledger:attachments:encryptionKey"] = "test-encryption-key",
|
||||
["findings:ledger:attachments:signedUrlSecret"] = "test-signed-url-secret",
|
||||
["findings:ledger:attachments:csrfSharedSecret"] = "test-csrf-secret",
|
||||
["findings:ledger:authority:issuer"] = "https://authority.local",
|
||||
["findings:ledger:authority:requireHttpsMetadata"] = "false",
|
||||
["findings:ledger:authority:requiredScopes:0"] = StellaOpsScopes.FindingsRead,
|
||||
["findings:ledger:authority:requiredScopes:1"] = "findings:write"
|
||||
});
|
||||
});
|
||||
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.RemoveAll<IHostedService>();
|
||||
services.RemoveAll<IConfigureOptions<AuthenticationOptions>>();
|
||||
services.RemoveAll<IPostConfigureOptions<AuthenticationOptions>>();
|
||||
services.RemoveAll<IConfigureOptions<JwtBearerOptions>>();
|
||||
services.RemoveAll<IPostConfigureOptions<JwtBearerOptions>>();
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = FindingsLedgerTestAuthHandler.SchemeName;
|
||||
options.DefaultChallengeScheme = FindingsLedgerTestAuthHandler.SchemeName;
|
||||
})
|
||||
.AddScheme<AuthenticationSchemeOptions, FindingsLedgerTestAuthHandler>(
|
||||
FindingsLedgerTestAuthHandler.SchemeName,
|
||||
_ => { })
|
||||
.AddScheme<AuthenticationSchemeOptions, FindingsLedgerTestAuthHandler>(
|
||||
StellaOpsAuthenticationDefaults.AuthenticationScheme,
|
||||
_ => { });
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var pair in originalEnvironment)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FindingsLedgerTestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
internal const string SchemeName = "FindingsLedgerTest";
|
||||
|
||||
public FindingsLedgerTestAuthHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder)
|
||||
: base(options, logger, encoder)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (!Request.Headers.TryGetValue("Authorization", out var rawHeader) ||
|
||||
!AuthenticationHeaderValue.TryParse(rawHeader, out var header))
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.NoResult());
|
||||
}
|
||||
|
||||
if (!string.Equals(header.Scheme, "Bearer", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(header.Scheme, SchemeName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.NoResult());
|
||||
}
|
||||
|
||||
if (!string.Equals(header.Parameter, "test-token", StringComparison.Ordinal))
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.Fail("Invalid test token."));
|
||||
}
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(StellaOpsClaimTypes.Subject, "test-user")
|
||||
};
|
||||
|
||||
if (Request.Headers.TryGetValue("X-Tenant-Id", out var tenantValue) &&
|
||||
Guid.TryParse(tenantValue.ToString(), out var tenantId))
|
||||
{
|
||||
claims.Add(new Claim(StellaOpsClaimTypes.Tenant, tenantId.ToString("D")));
|
||||
}
|
||||
|
||||
if (Request.Headers.TryGetValue("X-Scopes", out var scopesValue))
|
||||
{
|
||||
var scopes = scopesValue
|
||||
.ToString()
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (scopes.Length > 0)
|
||||
{
|
||||
claims.Add(new Claim(StellaOpsClaimTypes.Scope, string.Join(' ', scopes)));
|
||||
}
|
||||
}
|
||||
|
||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
|
||||
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
|
||||
{
|
||||
Response.Headers["WWW-Authenticate"] = "Bearer";
|
||||
return base.HandleChallengeAsync(properties);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
@@ -23,20 +21,13 @@ namespace StellaOps.Findings.Ledger.Tests;
|
||||
/// </summary>
|
||||
public sealed class FindingsLedgerWebServiceContractTests : IDisposable
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly FindingsLedgerWebApplicationFactory _factory;
|
||||
private readonly HttpClient _client;
|
||||
private bool _disposed;
|
||||
|
||||
public FindingsLedgerWebServiceContractTests()
|
||||
{
|
||||
_factory = new WebApplicationFactory<Program>()
|
||||
.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// Configure test services as needed
|
||||
});
|
||||
});
|
||||
_factory = new FindingsLedgerWebApplicationFactory();
|
||||
_client = _factory.CreateClient();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<None Include="**/tools/**/*" Pack="false" CopyToOutputDirectory="Never" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Findings.Ledger.WebService/StellaOps.Findings.Ledger.WebService.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
@@ -26,4 +28,4 @@
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -8,3 +8,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| AUDIT-0344-M | DONE | Revalidated 2026-01-07; maintainability audit for Findings.Ledger.Tests. |
|
||||
| AUDIT-0344-T | DONE | Revalidated 2026-01-07; test coverage audit for Findings.Ledger.Tests. |
|
||||
| AUDIT-0344-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
| LEDGER-TESTS-0001 | DOING | Stabilize Findings Ledger WebService test harness (config/auth + stubs). |
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
@@ -14,12 +15,20 @@ public static class FindingSummaryEndpoints
|
||||
.RequireAuthorization();
|
||||
|
||||
// GET /api/v1/findings/{findingId}/summary
|
||||
group.MapGet("/{findingId:guid}/summary", async Task<Results<Ok<FindingSummary>, NotFound>> (
|
||||
Guid findingId,
|
||||
group.MapGet("/{findingId}/summary", async Task<Results<Ok<FindingSummary>, NotFound, ProblemHttpResult>> (
|
||||
string findingId,
|
||||
IFindingSummaryService service,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var summary = await service.GetSummaryAsync(findingId, ct);
|
||||
if (!Guid.TryParse(findingId, out var parsedId))
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "invalid_finding_id",
|
||||
detail: "findingId must be a valid GUID.");
|
||||
}
|
||||
|
||||
var summary = await service.GetSummaryAsync(parsedId, ct);
|
||||
return summary is not null
|
||||
? TypedResults.Ok(summary)
|
||||
: TypedResults.NotFound();
|
||||
@@ -27,6 +36,7 @@ public static class FindingSummaryEndpoints
|
||||
.WithName("GetFindingSummary")
|
||||
.WithDescription("Get condensed finding summary for vulnerability-first UX")
|
||||
.Produces<FindingSummary>(200)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest)
|
||||
.Produces(404);
|
||||
|
||||
// GET /api/v1/findings/summaries
|
||||
|
||||
@@ -213,6 +213,17 @@ builder.Services.AddSingleton<StellaOps.Findings.Ledger.Infrastructure.Snapshot.
|
||||
builder.Services.AddSingleton<SnapshotService>();
|
||||
builder.Services.AddSingleton<VexConsensusService>();
|
||||
|
||||
// Finding summary, evidence graph, reachability, and runtime timeline endpoints
|
||||
builder.Services.AddSingleton<IFindingSummaryBuilder, FindingSummaryBuilder>();
|
||||
builder.Services.AddSingleton<IFindingRepository, InMemoryFindingRepository>();
|
||||
builder.Services.AddSingleton<IFindingSummaryService, FindingSummaryService>();
|
||||
builder.Services.AddSingleton<IEvidenceRepository, NullEvidenceRepository>();
|
||||
builder.Services.AddSingleton<IAttestationVerifier, NullAttestationVerifier>();
|
||||
builder.Services.AddSingleton<IEvidenceGraphBuilder, EvidenceGraphBuilder>();
|
||||
builder.Services.AddSingleton<IEvidenceContentService, NullEvidenceContentService>();
|
||||
builder.Services.AddSingleton<IReachabilityMapService, NullReachabilityMapService>();
|
||||
builder.Services.AddSingleton<IRuntimeTimelineService, NullRuntimeTimelineService>();
|
||||
|
||||
// Alert and Decision services (SPRINT_3602)
|
||||
builder.Services.AddSingleton<IAlertService, AlertService>();
|
||||
builder.Services.AddSingleton<IDecisionService, DecisionService>();
|
||||
@@ -1909,6 +1920,12 @@ app.MapPatch("/api/v1/findings/{findingId}/state", async Task<Results<Ok<StateTr
|
||||
// Refresh Router endpoint cache
|
||||
app.TryRefreshStellaRouterEndpoints(routerOptions);
|
||||
|
||||
// Findings summary, evidence graph, reachability, and runtime timeline endpoints
|
||||
app.MapFindingSummaryEndpoints();
|
||||
app.MapEvidenceGraphEndpoints();
|
||||
app.MapReachabilityMapEndpoints();
|
||||
app.MapRuntimeTimelineEndpoints();
|
||||
|
||||
// Map EWS scoring and webhook endpoints (SPRINT_8200.0012.0004)
|
||||
app.MapScoringEndpoints();
|
||||
app.MapWebhookEndpoints();
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Findings.Ledger.WebService.Endpoints;
|
||||
using StellaOps.Scanner.Analyzers.Native.RuntimeCapture.Timeline;
|
||||
using StellaOps.Scanner.Reachability.MiniMap;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.WebService.Services;
|
||||
|
||||
internal sealed class InMemoryFindingRepository : IFindingRepository
|
||||
{
|
||||
public Task<FindingData?> GetByIdAsync(Guid id, CancellationToken ct)
|
||||
=> Task.FromResult<FindingData?>(null);
|
||||
|
||||
public Task<(IReadOnlyList<FindingData> findings, int totalCount)> GetPagedAsync(
|
||||
int page,
|
||||
int pageSize,
|
||||
string? status,
|
||||
string? severity,
|
||||
decimal? minConfidence,
|
||||
CancellationToken ct)
|
||||
{
|
||||
IReadOnlyList<FindingData> findings = Array.Empty<FindingData>();
|
||||
return Task.FromResult((findings, 0));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class NullEvidenceRepository : IEvidenceRepository
|
||||
{
|
||||
public Task<FullEvidence?> GetFullEvidenceAsync(Guid findingId, CancellationToken ct)
|
||||
=> Task.FromResult<FullEvidence?>(null);
|
||||
}
|
||||
|
||||
internal sealed class NullAttestationVerifier : IAttestationVerifier
|
||||
{
|
||||
private static readonly AttestationVerificationResult DefaultResult = new()
|
||||
{
|
||||
IsValid = false,
|
||||
SignerIdentity = null,
|
||||
SignedAt = null,
|
||||
KeyId = null,
|
||||
RekorLogIndex = null
|
||||
};
|
||||
|
||||
public Task<AttestationVerificationResult> VerifyAsync(string digest, CancellationToken ct)
|
||||
=> Task.FromResult(DefaultResult);
|
||||
}
|
||||
|
||||
internal sealed class NullEvidenceContentService : IEvidenceContentService
|
||||
{
|
||||
public Task<object?> GetContentAsync(Guid findingId, string nodeId, CancellationToken ct)
|
||||
=> Task.FromResult<object?>(null);
|
||||
}
|
||||
|
||||
internal sealed class NullReachabilityMapService : IReachabilityMapService
|
||||
{
|
||||
public Task<ReachabilityMiniMap?> GetMiniMapAsync(Guid findingId, int maxPaths, CancellationToken ct)
|
||||
=> Task.FromResult<ReachabilityMiniMap?>(null);
|
||||
}
|
||||
|
||||
internal sealed class NullRuntimeTimelineService : IRuntimeTimelineService
|
||||
{
|
||||
public Task<RuntimeTimeline?> GetTimelineAsync(Guid findingId, TimelineOptions options, CancellationToken ct)
|
||||
=> Task.FromResult<RuntimeTimeline?>(null);
|
||||
}
|
||||
@@ -8,3 +8,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| AUDIT-0345-M | DONE | Revalidated 2026-01-07; maintainability audit for Findings.Ledger.WebService. |
|
||||
| AUDIT-0345-T | DONE | Revalidated 2026-01-07; test coverage audit for Findings.Ledger.WebService. |
|
||||
| AUDIT-0345-A | TODO | Pending approval (non-test project; revalidated 2026-01-07). |
|
||||
| LEDGER-TESTS-0001 | DOING | Stabilize Findings Ledger WebService test harness (config/auth + stubs). |
|
||||
|
||||
@@ -10,8 +10,6 @@ using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Xunit;
|
||||
|
||||
using LedgerProgram = StellaOps.Findings.Ledger.WebService.Program;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests.Integration;
|
||||
|
||||
/// <summary>
|
||||
@@ -19,11 +17,11 @@ namespace StellaOps.Findings.Ledger.Tests.Integration;
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Sprint", "3602")]
|
||||
public sealed class EvidenceDecisionApiIntegrationTests : IClassFixture<WebApplicationFactory<LedgerProgram>>
|
||||
public sealed class EvidenceDecisionApiIntegrationTests : IClassFixture<FindingsLedgerWebApplicationFactory>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public EvidenceDecisionApiIntegrationTests(WebApplicationFactory<LedgerProgram> factory)
|
||||
public EvidenceDecisionApiIntegrationTests(FindingsLedgerWebApplicationFactory factory)
|
||||
{
|
||||
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
|
||||
{
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
|
||||
using LedgerProgram = StellaOps.Findings.Ledger.WebService.Program;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests.Integration;
|
||||
|
||||
public sealed class FindingsLedgerWebApplicationFactory : WebApplicationFactory<LedgerProgram>
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, string?> DefaultEnvironment =
|
||||
new Dictionary<string, string?>(StringComparer.Ordinal)
|
||||
{
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__DATABASE__CONNECTIONSTRING"] = "Host=localhost;Database=stellaops_test;Username=stella;Password=stella",
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__ATTACHMENTS__ENCRYPTIONKEY"] = "test-encryption-key",
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__ATTACHMENTS__SIGNEDURLSECRET"] = "test-signed-url-secret",
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__ATTACHMENTS__CSRFSHAREDSECRET"] = "test-csrf-secret",
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__AUTHORITY__ISSUER"] = "https://authority.local",
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__AUTHORITY__REQUIREHTTPSMETADATA"] = "false",
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__AUTHORITY__REQUIREDSCOPES__0"] = StellaOpsScopes.FindingsRead,
|
||||
["FINDINGS_LEDGER_FINDINGS__LEDGER__AUTHORITY__REQUIREDSCOPES__1"] = "findings:write"
|
||||
};
|
||||
|
||||
private readonly Dictionary<string, string?> originalEnvironment = new(StringComparer.Ordinal);
|
||||
|
||||
public FindingsLedgerWebApplicationFactory()
|
||||
{
|
||||
foreach (var pair in DefaultEnvironment)
|
||||
{
|
||||
originalEnvironment[pair.Key] = Environment.GetEnvironmentVariable(pair.Key);
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.UseEnvironment("Production");
|
||||
builder.UseDefaultServiceProvider(options =>
|
||||
{
|
||||
options.ValidateScopes = false;
|
||||
options.ValidateOnBuild = false;
|
||||
});
|
||||
|
||||
builder.ConfigureAppConfiguration((context, configBuilder) =>
|
||||
{
|
||||
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["findings:ledger:database:connectionString"] = "Host=localhost;Database=stellaops_test;Username=stella;Password=stella",
|
||||
["findings:ledger:attachments:encryptionKey"] = "test-encryption-key",
|
||||
["findings:ledger:attachments:signedUrlSecret"] = "test-signed-url-secret",
|
||||
["findings:ledger:attachments:csrfSharedSecret"] = "test-csrf-secret",
|
||||
["findings:ledger:authority:issuer"] = "https://authority.local",
|
||||
["findings:ledger:authority:requireHttpsMetadata"] = "false",
|
||||
["findings:ledger:authority:requiredScopes:0"] = StellaOpsScopes.FindingsRead,
|
||||
["findings:ledger:authority:requiredScopes:1"] = "findings:write"
|
||||
});
|
||||
});
|
||||
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.RemoveAll<IHostedService>();
|
||||
services.RemoveAll<IConfigureOptions<AuthenticationOptions>>();
|
||||
services.RemoveAll<IPostConfigureOptions<AuthenticationOptions>>();
|
||||
services.RemoveAll<IConfigureOptions<JwtBearerOptions>>();
|
||||
services.RemoveAll<IPostConfigureOptions<JwtBearerOptions>>();
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = FindingsLedgerTestAuthHandler.SchemeName;
|
||||
options.DefaultChallengeScheme = FindingsLedgerTestAuthHandler.SchemeName;
|
||||
})
|
||||
.AddScheme<AuthenticationSchemeOptions, FindingsLedgerTestAuthHandler>(
|
||||
FindingsLedgerTestAuthHandler.SchemeName,
|
||||
_ => { })
|
||||
.AddScheme<AuthenticationSchemeOptions, FindingsLedgerTestAuthHandler>(
|
||||
StellaOpsAuthenticationDefaults.AuthenticationScheme,
|
||||
_ => { });
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var pair in originalEnvironment)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FindingsLedgerTestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
internal const string SchemeName = "FindingsLedgerTest";
|
||||
|
||||
public FindingsLedgerTestAuthHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder)
|
||||
: base(options, logger, encoder)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (!Request.Headers.TryGetValue("Authorization", out var rawHeader) ||
|
||||
!AuthenticationHeaderValue.TryParse(rawHeader, out var header))
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.NoResult());
|
||||
}
|
||||
|
||||
if (!string.Equals(header.Scheme, "Bearer", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(header.Scheme, SchemeName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.NoResult());
|
||||
}
|
||||
|
||||
if (!string.Equals(header.Parameter, "test-token", StringComparison.Ordinal))
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.Fail("Invalid test token."));
|
||||
}
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(StellaOpsClaimTypes.Subject, "test-user")
|
||||
};
|
||||
|
||||
if (Request.Headers.TryGetValue("X-Tenant-Id", out var tenantValue) &&
|
||||
Guid.TryParse(tenantValue.ToString(), out var tenantId))
|
||||
{
|
||||
claims.Add(new Claim(StellaOpsClaimTypes.Tenant, tenantId.ToString("D")));
|
||||
}
|
||||
|
||||
if (Request.Headers.TryGetValue("X-Scopes", out var scopesValue))
|
||||
{
|
||||
var scopes = scopesValue
|
||||
.ToString()
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (scopes.Length > 0)
|
||||
{
|
||||
claims.Add(new Claim(StellaOpsClaimTypes.Scope, string.Join(' ', scopes)));
|
||||
}
|
||||
}
|
||||
|
||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
|
||||
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
|
||||
{
|
||||
Response.Headers["WWW-Authenticate"] = "Bearer";
|
||||
return base.HandleChallengeAsync(properties);
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,6 @@ using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Xunit;
|
||||
|
||||
using LedgerProgram = StellaOps.Findings.Ledger.WebService.Program;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests.Integration;
|
||||
|
||||
/// <summary>
|
||||
@@ -20,11 +18,11 @@ namespace StellaOps.Findings.Ledger.Tests.Integration;
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Sprint", "8200.0012.0004")]
|
||||
public sealed class ScoringAuthorizationTests : IClassFixture<WebApplicationFactory<LedgerProgram>>
|
||||
public sealed class ScoringAuthorizationTests : IClassFixture<FindingsLedgerWebApplicationFactory>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public ScoringAuthorizationTests(WebApplicationFactory<LedgerProgram> factory)
|
||||
public ScoringAuthorizationTests(FindingsLedgerWebApplicationFactory factory)
|
||||
{
|
||||
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
|
||||
{
|
||||
|
||||
@@ -11,8 +11,6 @@ using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Xunit;
|
||||
|
||||
using LedgerProgram = StellaOps.Findings.Ledger.WebService.Program;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests.Integration;
|
||||
|
||||
/// <summary>
|
||||
@@ -20,11 +18,11 @@ namespace StellaOps.Findings.Ledger.Tests.Integration;
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Sprint", "8200.0012.0004")]
|
||||
public sealed class ScoringEndpointsIntegrationTests : IClassFixture<WebApplicationFactory<LedgerProgram>>
|
||||
public sealed class ScoringEndpointsIntegrationTests : IClassFixture<FindingsLedgerWebApplicationFactory>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public ScoringEndpointsIntegrationTests(WebApplicationFactory<LedgerProgram> factory)
|
||||
public ScoringEndpointsIntegrationTests(FindingsLedgerWebApplicationFactory factory)
|
||||
{
|
||||
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
|
||||
{
|
||||
|
||||
@@ -12,8 +12,6 @@ using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Xunit;
|
||||
|
||||
using LedgerProgram = StellaOps.Findings.Ledger.WebService.Program;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests.Integration;
|
||||
|
||||
/// <summary>
|
||||
@@ -22,11 +20,11 @@ namespace StellaOps.Findings.Ledger.Tests.Integration;
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Sprint", "8200.0012.0004")]
|
||||
public sealed class ScoringObservabilityTests : IClassFixture<WebApplicationFactory<LedgerProgram>>
|
||||
public sealed class ScoringObservabilityTests : IClassFixture<FindingsLedgerWebApplicationFactory>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public ScoringObservabilityTests(WebApplicationFactory<LedgerProgram> factory)
|
||||
public ScoringObservabilityTests(FindingsLedgerWebApplicationFactory factory)
|
||||
{
|
||||
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
|
||||
{
|
||||
|
||||
@@ -11,8 +11,6 @@ using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Xunit;
|
||||
|
||||
using LedgerProgram = StellaOps.Findings.Ledger.WebService.Program;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests.Integration;
|
||||
|
||||
/// <summary>
|
||||
@@ -20,11 +18,11 @@ namespace StellaOps.Findings.Ledger.Tests.Integration;
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Sprint", "8200.0012.0004")]
|
||||
public sealed class WebhookEndpointsIntegrationTests : IClassFixture<WebApplicationFactory<LedgerProgram>>
|
||||
public sealed class WebhookEndpointsIntegrationTests : IClassFixture<FindingsLedgerWebApplicationFactory>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public WebhookEndpointsIntegrationTests(WebApplicationFactory<LedgerProgram> factory)
|
||||
public WebhookEndpointsIntegrationTests(FindingsLedgerWebApplicationFactory factory)
|
||||
{
|
||||
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
|
||||
{
|
||||
|
||||
@@ -8,3 +8,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| AUDIT-0343-M | DONE | Revalidated 2026-01-07; maintainability audit for Findings.Ledger.Tests. |
|
||||
| AUDIT-0343-T | DONE | Revalidated 2026-01-07; test coverage audit for Findings.Ledger.Tests. |
|
||||
| AUDIT-0343-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
| LEDGER-TESTS-0001 | DOING | Stabilize Findings Ledger WebService test harness (config/auth + stubs). |
|
||||
|
||||
Reference in New Issue
Block a user