wip: doctor/cli/docs/api to vector db consolidation; api hardening for descriptions, tenant, and scopes; migrations and conversions of all DALs to EF v10
This commit is contained in:
@@ -93,6 +93,64 @@ public sealed class ContextEndpointsTests : IClassFixture<PlatformWebApplication
|
||||
Assert.Equal(updated.TimeWindow, stored.TimeWindow);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Integration)]
|
||||
[Fact]
|
||||
public async Task Preferences_AreIsolatedPerTenantForSameActor()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "shared-actor");
|
||||
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-context-a");
|
||||
|
||||
var tenantARequest = new PlatformContextPreferencesRequest(
|
||||
Regions: new[] { "us-east" },
|
||||
Environments: new[] { "us-prod" },
|
||||
TimeWindow: "7d");
|
||||
|
||||
var tenantAResponse = await client.PutAsJsonAsync(
|
||||
"/api/v2/context/preferences",
|
||||
tenantARequest,
|
||||
TestContext.Current.CancellationToken);
|
||||
tenantAResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var tenantAPreferences = await tenantAResponse.Content.ReadFromJsonAsync<PlatformContextPreferences>(
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(tenantAPreferences);
|
||||
|
||||
client.DefaultRequestHeaders.Remove("X-StellaOps-Tenant");
|
||||
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-context-b");
|
||||
|
||||
var tenantBDefaults = await client.GetFromJsonAsync<PlatformContextPreferences>(
|
||||
"/api/v2/context/preferences",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(tenantBDefaults);
|
||||
Assert.Equal("tenant-context-b", tenantBDefaults!.TenantId);
|
||||
Assert.NotEqual("7d", tenantBDefaults.TimeWindow);
|
||||
Assert.NotEqual(tenantAPreferences!.Regions.ToArray(), tenantBDefaults.Regions.ToArray());
|
||||
|
||||
var tenantBRequest = new PlatformContextPreferencesRequest(
|
||||
Regions: new[] { "eu-west" },
|
||||
Environments: new[] { "eu-prod" },
|
||||
TimeWindow: "30d");
|
||||
|
||||
var tenantBUpdate = await client.PutAsJsonAsync(
|
||||
"/api/v2/context/preferences",
|
||||
tenantBRequest,
|
||||
TestContext.Current.CancellationToken);
|
||||
tenantBUpdate.EnsureSuccessStatusCode();
|
||||
|
||||
client.DefaultRequestHeaders.Remove("X-StellaOps-Tenant");
|
||||
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-context-a");
|
||||
|
||||
var tenantAReloaded = await client.GetFromJsonAsync<PlatformContextPreferences>(
|
||||
"/api/v2/context/preferences",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(tenantAReloaded);
|
||||
Assert.Equal("tenant-context-a", tenantAReloaded!.TenantId);
|
||||
Assert.Equal(new[] { "us-east" }, tenantAReloaded.Regions.ToArray());
|
||||
Assert.Equal(new[] { "us-prod" }, tenantAReloaded.Environments.ToArray());
|
||||
Assert.Equal("7d", tenantAReloaded.TimeWindow);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ContextEndpoints_WithoutTenantHeader_ReturnBadRequest()
|
||||
|
||||
@@ -13,16 +13,34 @@ public sealed class MigrationAdminEndpointsTests : IClassFixture<PlatformWebAppl
|
||||
{
|
||||
private static readonly string[] ExpectedModules =
|
||||
[
|
||||
"AdvisoryAI",
|
||||
"AirGap",
|
||||
"Attestor",
|
||||
"Authority",
|
||||
"BinaryIndex",
|
||||
"Concelier",
|
||||
"Eventing",
|
||||
"Evidence",
|
||||
"EvidenceLocker",
|
||||
"Excititor",
|
||||
"FindingsLedger",
|
||||
"Graph",
|
||||
"Notify",
|
||||
"Orchestrator",
|
||||
"Platform",
|
||||
"PluginRegistry",
|
||||
"Policy",
|
||||
"ReachGraph",
|
||||
"Remediation",
|
||||
"SbomLineage",
|
||||
"Scanner",
|
||||
"Scheduler",
|
||||
"TimelineIndexer"
|
||||
"Signals",
|
||||
"TimelineIndexer",
|
||||
"Unknowns",
|
||||
"Verdict",
|
||||
"VexHub",
|
||||
"VexLens"
|
||||
];
|
||||
|
||||
private readonly PlatformWebApplicationFactory _factory;
|
||||
@@ -139,4 +157,18 @@ public sealed class MigrationAdminEndpointsTests : IClassFixture<PlatformWebAppl
|
||||
Assert.NotNull(problem);
|
||||
Assert.Equal("Release migration approval required", problem!.Title);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Modules_WithoutTenantHeader_RemainsAccessibleAsSystemEndpoint()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "migration-admin-system-test");
|
||||
|
||||
var response = await client.GetAsync(
|
||||
"/api/v1/admin/migrations/modules",
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using Xunit;
|
||||
@@ -37,4 +38,19 @@ public sealed class OnboardingEndpointsTests : IClassFixture<PlatformWebApplicat
|
||||
state.Steps.OrderBy(item => item.Step, System.StringComparer.Ordinal).Select(item => item.Step),
|
||||
state.Steps.Select(item => item.Step));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Integration)]
|
||||
[Fact]
|
||||
public async Task Onboarding_SetupStatusRoute_RejectsCrossTenantAccess()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-onboarding-a");
|
||||
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "actor-onboarding");
|
||||
|
||||
var response = await client.GetAsync(
|
||||
"/api/v1/platform/tenants/tenant-onboarding-b/setup-status",
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
using StellaOps.TestKit;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Tests;
|
||||
|
||||
public sealed class PlatformRequestContextResolverTests
|
||||
{
|
||||
private readonly PlatformRequestContextResolver _resolver = new();
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryResolve_WithCanonicalClaim_UsesClaimTenant()
|
||||
{
|
||||
var context = CreateContext(
|
||||
claims:
|
||||
[
|
||||
new Claim(StellaOpsClaimTypes.Tenant, "Tenant-A"),
|
||||
new Claim(StellaOpsClaimTypes.Subject, "subject-a"),
|
||||
],
|
||||
headers: new Dictionary<string, string>
|
||||
{
|
||||
[StellaOpsHttpHeaderNames.Tenant] = "tenant-a",
|
||||
});
|
||||
|
||||
var success = _resolver.TryResolve(context, out var requestContext, out var error);
|
||||
|
||||
Assert.True(success);
|
||||
Assert.NotNull(requestContext);
|
||||
Assert.Equal("tenant-a", requestContext!.TenantId);
|
||||
Assert.Equal("subject-a", requestContext.ActorId);
|
||||
Assert.Null(error);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryResolve_WithConflictingClaimAndHeader_ReturnsTenantConflict()
|
||||
{
|
||||
var context = CreateContext(
|
||||
claims:
|
||||
[
|
||||
new Claim(StellaOpsClaimTypes.Tenant, "tenant-a"),
|
||||
new Claim(StellaOpsClaimTypes.Subject, "subject-a"),
|
||||
],
|
||||
headers: new Dictionary<string, string>
|
||||
{
|
||||
[StellaOpsHttpHeaderNames.Tenant] = "tenant-b",
|
||||
});
|
||||
|
||||
var success = _resolver.TryResolve(context, out var requestContext, out var error);
|
||||
|
||||
Assert.False(success);
|
||||
Assert.Null(requestContext);
|
||||
Assert.Equal("tenant_conflict", error);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryResolve_WithLegacyTidClaim_UsesLegacyClaimFallback()
|
||||
{
|
||||
var context = CreateContext(
|
||||
claims:
|
||||
[
|
||||
new Claim("tid", "legacy-tenant"),
|
||||
new Claim(StellaOpsClaimTypes.ClientId, "client-a"),
|
||||
]);
|
||||
|
||||
var success = _resolver.TryResolve(context, out var requestContext, out var error);
|
||||
|
||||
Assert.True(success);
|
||||
Assert.NotNull(requestContext);
|
||||
Assert.Equal("legacy-tenant", requestContext!.TenantId);
|
||||
Assert.Equal("client-a", requestContext.ActorId);
|
||||
Assert.Null(error);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryResolve_WithConflictingCanonicalAndLegacyHeaders_ReturnsTenantConflict()
|
||||
{
|
||||
var context = CreateContext(
|
||||
headers: new Dictionary<string, string>
|
||||
{
|
||||
[StellaOpsHttpHeaderNames.Tenant] = "tenant-a",
|
||||
["X-Stella-Tenant"] = "tenant-b",
|
||||
["X-StellaOps-Actor"] = "actor-a",
|
||||
});
|
||||
|
||||
var success = _resolver.TryResolve(context, out var requestContext, out var error);
|
||||
|
||||
Assert.False(success);
|
||||
Assert.Null(requestContext);
|
||||
Assert.Equal("tenant_conflict", error);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryResolve_WithoutTenant_ReturnsTenantMissing()
|
||||
{
|
||||
var context = CreateContext(
|
||||
headers: new Dictionary<string, string>
|
||||
{
|
||||
["X-StellaOps-Actor"] = "actor-a",
|
||||
});
|
||||
|
||||
var success = _resolver.TryResolve(context, out var requestContext, out var error);
|
||||
|
||||
Assert.False(success);
|
||||
Assert.Null(requestContext);
|
||||
Assert.Equal("tenant_missing", error);
|
||||
}
|
||||
|
||||
private static DefaultHttpContext CreateContext(
|
||||
IReadOnlyList<Claim>? claims = null,
|
||||
IReadOnlyDictionary<string, string>? headers = null)
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
if (claims is not null)
|
||||
{
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
|
||||
}
|
||||
|
||||
if (headers is not null)
|
||||
{
|
||||
foreach (var pair in headers)
|
||||
{
|
||||
context.Request.Headers[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -105,4 +105,23 @@ public sealed class QuotaEndpointsTests : IClassFixture<PlatformWebApplicationFa
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal("quotaId is required.", body!["error"]?.GetValue<string>());
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Integration)]
|
||||
[Fact]
|
||||
public async Task Quotas_TenantRoute_RejectsCrossTenantAccess()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-quotas-a");
|
||||
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "actor-quotas");
|
||||
|
||||
var response = await client.GetAsync(
|
||||
"/api/v1/platform/quotas/tenants/tenant-quotas-b",
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(System.Net.HttpStatusCode.Forbidden, response.StatusCode);
|
||||
|
||||
var body = await response.Content.ReadFromJsonAsync<JsonObject>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal("tenant_forbidden", body!["error"]?.GetValue<string>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,38 @@ public sealed class TopologyReadModelEndpointsTests : IClassFixture<PlatformWebA
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Integration)]
|
||||
[Fact]
|
||||
public async Task TopologyEndpoints_IsolateDataAcrossTenantsWithOverlappingReleaseSlugs()
|
||||
{
|
||||
using var tenantAClient = CreateTenantClient("tenant-topology-a");
|
||||
using var tenantBClient = CreateTenantClient("tenant-topology-b");
|
||||
|
||||
await SeedReleaseAsync(tenantAClient, "shared-release", "Shared Release", "us-prod", "promotion");
|
||||
await SeedReleaseAsync(tenantBClient, "shared-release", "Shared Release", "us-prod", "promotion");
|
||||
|
||||
var tenantATargets = await tenantAClient.GetFromJsonAsync<PlatformListResponse<TopologyTargetProjection>>(
|
||||
"/api/v2/topology/targets?limit=200&offset=0",
|
||||
TestContext.Current.CancellationToken);
|
||||
var tenantBTargets = await tenantBClient.GetFromJsonAsync<PlatformListResponse<TopologyTargetProjection>>(
|
||||
"/api/v2/topology/targets?limit=200&offset=0",
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(tenantATargets);
|
||||
Assert.NotNull(tenantBTargets);
|
||||
Assert.Equal("tenant-topology-a", tenantATargets!.TenantId);
|
||||
Assert.Equal("tenant-topology-b", tenantBTargets!.TenantId);
|
||||
Assert.Equal(2, tenantATargets.Items.Count);
|
||||
Assert.Equal(2, tenantBTargets.Items.Count);
|
||||
|
||||
var tenantBTargetIds = tenantBTargets.Items
|
||||
.Select(item => item.TargetId)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
tenantATargets.Items.Select(item => item.TargetId),
|
||||
targetId => tenantBTargetIds.Contains(targetId));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TopologyEndpoints_RequireExpectedPolicy()
|
||||
|
||||
Reference in New Issue
Block a user