search and ai stabilization work, localization stablized.
This commit is contained in:
@@ -13,6 +13,7 @@ using StellaOps.Policy.Exceptions.Repositories;
|
||||
using StellaOps.Policy.Gateway.Contracts;
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Claims;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Policy.Gateway.Endpoints;
|
||||
|
||||
@@ -102,7 +103,7 @@ public static class ExceptionEndpoints
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Exception not found",
|
||||
Title = _t("policy.error.exception_not_found"),
|
||||
Status = 404,
|
||||
Detail = $"No exception found with ID: {id}"
|
||||
});
|
||||
@@ -281,7 +282,7 @@ public static class ExceptionEndpoints
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails { Title = "Exception not found", Status = 404 });
|
||||
return Results.NotFound(new ProblemDetails { Title = _t("policy.error.exception_not_found"), Status = 404 });
|
||||
}
|
||||
|
||||
if (existing.Status != ExceptionStatus.Proposed)
|
||||
@@ -336,7 +337,7 @@ public static class ExceptionEndpoints
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails { Title = "Exception not found", Status = 404 });
|
||||
return Results.NotFound(new ProblemDetails { Title = _t("policy.error.exception_not_found"), Status = 404 });
|
||||
}
|
||||
|
||||
if (existing.Status != ExceptionStatus.Approved)
|
||||
@@ -379,7 +380,7 @@ public static class ExceptionEndpoints
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails { Title = "Exception not found", Status = 404 });
|
||||
return Results.NotFound(new ProblemDetails { Title = _t("policy.error.exception_not_found"), Status = 404 });
|
||||
}
|
||||
|
||||
if (existing.Status != ExceptionStatus.Active)
|
||||
@@ -432,7 +433,7 @@ public static class ExceptionEndpoints
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails { Title = "Exception not found", Status = 404 });
|
||||
return Results.NotFound(new ProblemDetails { Title = _t("policy.error.exception_not_found"), Status = 404 });
|
||||
}
|
||||
|
||||
if (existing.Status is ExceptionStatus.Expired or ExceptionStatus.Revoked)
|
||||
|
||||
@@ -17,6 +17,7 @@ using StellaOps.Auth.ServerIntegration.Tenancy;
|
||||
using StellaOps.Policy.Gates;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using System.Text.Json.Serialization;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Policy.Gateway.Endpoints;
|
||||
|
||||
@@ -180,7 +181,7 @@ public static class GatesEndpoints
|
||||
// Validate required fields
|
||||
if (string.IsNullOrWhiteSpace(request.Justification))
|
||||
{
|
||||
return Results.BadRequest(new { error = "Justification is required" });
|
||||
return Results.BadRequest(new { error = _t("policy.validation.justification_required") });
|
||||
}
|
||||
|
||||
var decodedBomRef = Uri.UnescapeDataString(bomRef);
|
||||
@@ -295,7 +296,7 @@ public static class GatesEndpoints
|
||||
|
||||
if (decision is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "Decision not found", decision_id = decisionId });
|
||||
return Results.NotFound(new { error = _t("policy.error.decision_not_found"), decision_id = decisionId });
|
||||
}
|
||||
|
||||
var response = new GateDecisionDto
|
||||
@@ -337,7 +338,7 @@ public static class GatesEndpoints
|
||||
|
||||
if (decision is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "Decision not found", decision_id = decisionId });
|
||||
return Results.NotFound(new { error = _t("policy.error.decision_not_found"), decision_id = decisionId });
|
||||
}
|
||||
|
||||
var exportFormat = (format?.ToLowerInvariant()) switch
|
||||
|
||||
@@ -15,6 +15,7 @@ using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Auth.ServerIntegration.Tenancy;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Localization;
|
||||
using StellaOps.Policy.Deltas;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Gateway.Clients;
|
||||
@@ -32,6 +33,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
using StellaOps.Router.AspNet;
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -304,6 +306,34 @@ builder.Services.AddHttpClient<IPolicyEngineClient, PolicyEngineClient>((service
|
||||
})
|
||||
.AddPolicyHandler(static (provider, _) => CreatePolicyEngineRetryPolicy(provider));
|
||||
|
||||
builder.Services.AddStellaOpsLocalization(builder.Configuration, options =>
|
||||
{
|
||||
options.DefaultLocale = string.IsNullOrWhiteSpace(options.DefaultLocale) ? "en-US" : options.DefaultLocale;
|
||||
if (options.SupportedLocales.Count == 0)
|
||||
{
|
||||
options.SupportedLocales.Add("en-US");
|
||||
}
|
||||
|
||||
if (!options.SupportedLocales.Contains("de-DE", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
options.SupportedLocales.Add("de-DE");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.RemoteBundleUrl))
|
||||
{
|
||||
var platformUrl = builder.Configuration["STELLAOPS_PLATFORM_URL"] ?? builder.Configuration["Platform:BaseUrl"];
|
||||
if (!string.IsNullOrWhiteSpace(platformUrl))
|
||||
{
|
||||
options.RemoteBundleUrl = platformUrl;
|
||||
}
|
||||
}
|
||||
|
||||
options.EnableRemoteBundles =
|
||||
options.EnableRemoteBundles || !string.IsNullOrWhiteSpace(options.RemoteBundleUrl);
|
||||
});
|
||||
builder.Services.AddTranslationBundle(System.Reflection.Assembly.GetExecutingAssembly());
|
||||
builder.Services.AddRemoteTranslationBundles();
|
||||
|
||||
// Stella Router integration
|
||||
var routerEnabled = builder.Services.AddRouterMicroservice(
|
||||
builder.Configuration,
|
||||
@@ -317,20 +347,23 @@ app.LogStellaOpsLocalHostname("policy-gateway");
|
||||
app.UseExceptionHandler(static appBuilder => appBuilder.Run(async context =>
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
await context.Response.WriteAsJsonAsync(new { error = "Unexpected gateway error." });
|
||||
await context.Response.WriteAsJsonAsync(new { error = _t("policy.error.unexpected_gateway_error") });
|
||||
}));
|
||||
|
||||
app.UseStatusCodePages();
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseStellaOpsLocalization();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseStellaOpsTenantMiddleware();
|
||||
app.TryUseStellaRouter(routerEnabled);
|
||||
|
||||
await app.LoadTranslationsAsync();
|
||||
|
||||
app.MapHealthChecks("/healthz");
|
||||
|
||||
app.MapGet("/readyz", () => Results.Ok(new { status = "ready" }))
|
||||
app.MapGet("/readyz", () => Results.Ok(new { status = _t("policy.status.ready") }))
|
||||
.WithName("Readiness")
|
||||
.AllowAnonymous();
|
||||
|
||||
@@ -388,7 +421,7 @@ policyPacks.MapPost(string.Empty, async Task<IResult> (
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Request body required.",
|
||||
Title = _t("common.error.body_required"),
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
@@ -420,7 +453,7 @@ policyPacks.MapPost("/{packId}/revisions", async Task<IResult> (
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "packId is required.",
|
||||
Title = _t("policy.validation.pack_id_required"),
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
@@ -429,7 +462,7 @@ policyPacks.MapPost("/{packId}/revisions", async Task<IResult> (
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Request body required.",
|
||||
Title = _t("common.error.body_required"),
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
@@ -464,7 +497,7 @@ policyPacks.MapPost("/{packId}/revisions/{version:int}:activate", async Task<IRe
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "packId is required.",
|
||||
Title = _t("policy.validation.pack_id_required"),
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
@@ -473,7 +506,7 @@ policyPacks.MapPost("/{packId}/revisions/{version:int}:activate", async Task<IRe
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Request body required.",
|
||||
Title = _t("common.error.body_required"),
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
@@ -520,7 +553,7 @@ cvss.MapPost("/receipts", async Task<IResult>(
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Request body required.",
|
||||
Title = _t("common.error.body_required"),
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
@@ -574,7 +607,7 @@ cvss.MapPut("/receipts/{receiptId}/amend", async Task<IResult>(
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Request body required.",
|
||||
Title = _t("common.error.body_required"),
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
@@ -668,7 +701,7 @@ app.MapAdvisorySourcePolicyEndpoints();
|
||||
app.MapToolLatticeEndpoints();
|
||||
|
||||
app.TryRefreshStellaRouterEndpoints(routerEnabled);
|
||||
app.Run();
|
||||
await app.RunAsync().ConfigureAwait(false);
|
||||
|
||||
static IAsyncPolicy<HttpResponseMessage> CreateAuthorityRetryPolicy(IServiceProvider provider)
|
||||
{
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Policy.Exceptions/StellaOps.Policy.Exceptions.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Policy.Persistence/StellaOps.Policy.Persistence.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Policy/StellaOps.Policy.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Localization/StellaOps.Localization.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Translations\*.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" />
|
||||
|
||||
@@ -9,3 +9,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0445-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Policy.Gateway. |
|
||||
| AUDIT-0445-A | TODO | Revalidated 2026-01-07 (open findings). |
|
||||
| TASK-033-013 | DONE | Fixed ScoreGateEndpoints duplication, DeltaVerdict references, and Policy.Gateway builds (SPRINT_20260120_033). |
|
||||
| SPRINT-20260224-002-LOC-101 | DONE | `SPRINT_20260224_002_Platform_translation_rollout_phase3_phase4.md`: adopted StellaOps localization runtime bundle loading in Policy Gateway and localized selected validation/error response strings (`en-US`/`de-DE`). |
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"_meta": { "locale": "de-DE", "namespace": "policy", "version": "1.0" },
|
||||
|
||||
"policy.error.unexpected_gateway_error": "Unerwarteter Gateway-Fehler.",
|
||||
"policy.validation.pack_id_required": "packId ist erforderlich.",
|
||||
"policy.status.ready": "bereit"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"_meta": { "locale": "en-US", "namespace": "policy", "version": "1.0" },
|
||||
|
||||
"policy.error.unexpected_gateway_error": "Unexpected gateway error.",
|
||||
"policy.error.exception_not_found": "Exception not found.",
|
||||
"policy.error.decision_not_found": "Decision not found.",
|
||||
|
||||
"policy.validation.pack_id_required": "packId is required.",
|
||||
"policy.validation.justification_required": "Justification is required.",
|
||||
|
||||
"policy.status.ready": "ready"
|
||||
}
|
||||
@@ -21,6 +21,10 @@ public sealed class GatesEndpointsIntegrationTests : IClassFixture<TestPolicyGat
|
||||
{
|
||||
_factory = factory;
|
||||
_client = _factory.CreateClient();
|
||||
// All endpoints are wrapped in RequireTenant(). The bypass network covers scope auth but
|
||||
// tenant resolution still needs a source. Provide it via the canonical header so every
|
||||
// request in this fixture automatically satisfies the tenant filter.
|
||||
_client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", TestPolicyGatewayFactory.DefaultTestTenant);
|
||||
}
|
||||
|
||||
#region GET /gates/{bom_ref} Tests
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Policy.Gateway.Tests;
|
||||
|
||||
public sealed class LocalizationEndpointsTests : IClassFixture<TestPolicyGatewayFactory>
|
||||
{
|
||||
private readonly TestPolicyGatewayFactory _factory;
|
||||
|
||||
public LocalizationEndpointsTests(TestPolicyGatewayFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Intent", "Safety")]
|
||||
public async Task Readyz_WithGermanLocale_ReturnsLocalizedStatus()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "/readyz");
|
||||
request.Headers.TryAddWithoutValidation("X-Locale", "de-DE");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
var payload = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
using var json = JsonDocument.Parse(payload);
|
||||
Assert.Equal(
|
||||
"bereit",
|
||||
json.RootElement.GetProperty("status").GetString());
|
||||
}
|
||||
}
|
||||
@@ -8,3 +8,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0446-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Policy.Gateway.Tests. |
|
||||
| AUDIT-0446-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Policy.Gateway.Tests. |
|
||||
| AUDIT-0446-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
| SPRINT-20260224-002-LOC-101-T | DONE | `SPRINT_20260224_002_Platform_translation_rollout_phase3_phase4.md`: added focused Policy Gateway locale-aware readiness test and validated German locale response text. |
|
||||
|
||||
@@ -111,6 +111,12 @@ public sealed class TestPolicyGatewayFactory : WebApplicationFactory<GatewayProg
|
||||
/// Generates a test JWT token with the specified claims.
|
||||
/// The token is signed with <see cref="TestSigningKey"/> and accepted by the test host.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Default tenant identifier used when no explicit tenant is passed to <see cref="CreateTestJwt"/>.
|
||||
/// All endpoints decorated with <c>RequireTenant()</c> need this claim present in the token.
|
||||
/// </summary>
|
||||
public const string DefaultTestTenant = "test-tenant";
|
||||
|
||||
public static string CreateTestJwt(
|
||||
string[]? scopes = null,
|
||||
string? tenantId = null,
|
||||
@@ -119,10 +125,16 @@ public sealed class TestPolicyGatewayFactory : WebApplicationFactory<GatewayProg
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(TestSigningKey));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
// Resolve effective tenant: use supplied value, or fall back to the shared default.
|
||||
// The stellaops:tenant claim is required by every endpoint wrapped in RequireTenant().
|
||||
var effectiveTenant = tenantId ?? DefaultTestTenant;
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtRegisteredClaimNames.Sub, "test-user"),
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
// Canonical tenant claim consumed by StellaOpsTenantResolver.TryResolve().
|
||||
new("stellaops:tenant", effectiveTenant)
|
||||
};
|
||||
|
||||
if (scopes is { Length: > 0 })
|
||||
@@ -130,11 +142,6 @@ public sealed class TestPolicyGatewayFactory : WebApplicationFactory<GatewayProg
|
||||
claims.Add(new Claim("scope", string.Join(" ", scopes)));
|
||||
}
|
||||
|
||||
if (tenantId is not null)
|
||||
{
|
||||
claims.Add(new Claim("tenant_id", tenantId));
|
||||
}
|
||||
|
||||
var expires = DateTime.UtcNow.Add(expiresIn ?? TimeSpan.FromHours(1));
|
||||
|
||||
var handler = new JsonWebTokenHandler();
|
||||
|
||||
@@ -350,18 +350,19 @@ public sealed class PolicyGatewayIntegrationTests : IAsyncLifetime
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(PolicyGatewayTestFactory.TestSigningKey));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
// Resolve effective tenant: use supplied value, or fall back to the shared default.
|
||||
// The stellaops:tenant claim is required by every endpoint wrapped in RequireTenant().
|
||||
var effectiveTenant = tenantId ?? "test-tenant";
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtRegisteredClaimNames.Sub, "test-user"),
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
new("scope", string.Join(" ", scopes))
|
||||
new("scope", string.Join(" ", scopes)),
|
||||
// Canonical tenant claim consumed by StellaOpsTenantResolver.TryResolve().
|
||||
new("stellaops:tenant", effectiveTenant)
|
||||
};
|
||||
|
||||
if (tenantId != null)
|
||||
{
|
||||
claims.Add(new Claim("tenant_id", tenantId));
|
||||
}
|
||||
|
||||
var expires = DateTime.UtcNow.Add(expiresIn ?? TimeSpan.FromHours(1));
|
||||
|
||||
var handler = new JsonWebTokenHandler();
|
||||
|
||||
Reference in New Issue
Block a user