Fix release health multi-scope evidence contracts

This commit is contained in:
master
2026-03-07 05:13:36 +02:00
parent afa23fc504
commit b70457712b
15 changed files with 842 additions and 36 deletions

View File

@@ -0,0 +1,150 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Platform.WebService.Constants;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.TestKit;
using System.Linq;
using System.Net;
using System.Net.Http.Json;
namespace StellaOps.Platform.WebService.Tests;
public sealed class EvidenceReadModelEndpointsTests : IClassFixture<PlatformWebApplicationFactory>
{
private readonly PlatformWebApplicationFactory _factory;
public EvidenceReadModelEndpointsTests(PlatformWebApplicationFactory factory)
{
_factory = factory;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EvidencePacksEndpoint_ReturnsDeterministicFilteredProjections()
{
var tenantId = Guid.NewGuid().ToString("D");
using var client = CreateTenantClient(tenantId);
var search = await SeedReleaseAsync(client, "search-evidence", "Search Evidence", "us-prod", "critical-fix");
var catalog = await SeedReleaseAsync(client, "catalog-evidence", "Catalog Evidence", "eu-prod", "policy-review");
var first = await client.GetFromJsonAsync<PlatformListResponse<EvidencePackProjection>>(
"/api/v2/evidence/packs?region=us-east,eu-west&environment=us-prod,eu-prod&limit=50&offset=0",
TestContext.Current.CancellationToken);
var second = await client.GetFromJsonAsync<PlatformListResponse<EvidencePackProjection>>(
"/api/v2/evidence/packs?region=us-east,eu-west&environment=us-prod,eu-prod&limit=50&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(first);
Assert.NotNull(second);
Assert.NotEmpty(first!.Items);
Assert.Equal(
first.Items.Select(item => item.CapsuleId).ToArray(),
second!.Items.Select(item => item.CapsuleId).ToArray());
Assert.Contains(first.Items, item => item.ReleaseId == search.Bundle.Id.ToString("D"));
Assert.Contains(first.Items, item => item.ReleaseId == catalog.Bundle.Id.ToString("D"));
Assert.All(first.Items, item =>
{
Assert.Contains(item.Region, new[] { "us-east", "eu-west" });
Assert.Contains(item.Environment, new[] { "us-prod", "eu-prod" });
Assert.False(string.IsNullOrWhiteSpace(item.CapsuleRoute));
});
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EvidencePacksEndpoint_WithoutTenantHeader_ReturnsBadRequest()
{
using var client = _factory.CreateClient();
var response = await client.GetAsync("/api/v2/evidence/packs", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void EvidencePacksEndpoint_RequiresReleaseControlReadPolicy()
{
var endpoints = _factory.Services
.GetRequiredService<EndpointDataSource>()
.Endpoints
.OfType<RouteEndpoint>()
.ToArray();
var endpoint = endpoints.Single(candidate =>
string.Equals(candidate.RoutePattern.RawText, "/api/v2/evidence/packs", StringComparison.Ordinal)
&& candidate.Metadata.GetMetadata<HttpMethodMetadata>()?.HttpMethods.Contains("GET", StringComparer.OrdinalIgnoreCase) == true);
var policies = endpoint.Metadata
.GetOrderedMetadata<IAuthorizeData>()
.Select(metadata => metadata.Policy)
.Where(static policy => !string.IsNullOrWhiteSpace(policy))
.ToArray();
Assert.Contains(PlatformPolicies.ReleaseControlRead, policies);
}
private static async Task<SeededRelease> SeedReleaseAsync(
HttpClient client,
string slug,
string name,
string targetEnvironment,
string reason)
{
var createResponse = await client.PostAsJsonAsync(
"/api/v1/release-control/bundles",
new CreateReleaseControlBundleRequest(slug, name, $"{name} description"),
TestContext.Current.CancellationToken);
createResponse.EnsureSuccessStatusCode();
var bundle = await createResponse.Content.ReadFromJsonAsync<ReleaseControlBundleDetail>(
TestContext.Current.CancellationToken);
Assert.NotNull(bundle);
var publishResponse = await client.PostAsJsonAsync(
$"/api/v1/release-control/bundles/{bundle!.Id:D}/versions",
new PublishReleaseControlBundleVersionRequest(
Changelog: "baseline",
Components:
[
new ReleaseControlBundleComponentInput(
ComponentVersionId: $"{slug}@1.0.0",
ComponentName: slug,
ImageDigest: "sha256:1111111111111111111111111111111111111111111111111111111111111111",
DeployOrder: 10,
MetadataJson: "{\"track\":\"stable\"}")
]),
TestContext.Current.CancellationToken);
publishResponse.EnsureSuccessStatusCode();
var version = await publishResponse.Content.ReadFromJsonAsync<ReleaseControlBundleVersionDetail>(
TestContext.Current.CancellationToken);
Assert.NotNull(version);
var materializeResponse = await client.PostAsJsonAsync(
$"/api/v1/release-control/bundles/{bundle.Id:D}/versions/{version!.Id:D}/materialize",
new MaterializeReleaseControlBundleVersionRequest(targetEnvironment, reason, Guid.NewGuid().ToString("N")),
TestContext.Current.CancellationToken);
materializeResponse.EnsureSuccessStatusCode();
var run = await materializeResponse.Content.ReadFromJsonAsync<ReleaseControlBundleMaterializationRun>(
TestContext.Current.CancellationToken);
Assert.NotNull(run);
return new SeededRelease(bundle, version, run!);
}
private HttpClient CreateTenantClient(string tenantId)
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId);
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "evidence-v2-tests");
return client;
}
private sealed record SeededRelease(
ReleaseControlBundleDetail Bundle,
ReleaseControlBundleVersionDetail Version,
ReleaseControlBundleMaterializationRun Run);
}

View File

@@ -133,6 +133,27 @@ public sealed class ReleaseReadModelEndpointsTests : IClassFixture<PlatformWebAp
Assert.All(euApprovals.Items, item => Assert.Equal("eu-west", item.TargetRegion));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ReleasesEndpoints_AcceptCommaDelimitedRegionAndEnvironmentFilters()
{
var tenantId = Guid.NewGuid().ToString("D");
using var client = CreateTenantClient(tenantId);
await SeedReleaseAsync(client, "search-hotfix", "Search Hotfix", "us-prod", "critical-fix");
await SeedReleaseAsync(client, "catalog-release", "Catalog Release", "eu-prod", "policy-review");
var activity = await client.GetFromJsonAsync<PlatformListResponse<ReleaseActivityProjection>>(
"/api/v2/releases/activity?region=us-east,eu-west&environment=us-prod,eu-prod&limit=50&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(activity);
Assert.NotEmpty(activity!.Items);
Assert.Contains(activity.Items, item => item.TargetRegion == "us-east");
Assert.Contains(activity.Items, item => item.TargetRegion == "eu-west");
Assert.All(activity.Items, item => Assert.Contains(item.TargetEnvironment, new[] { "us-prod", "eu-prod" }));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ReleasesEndpoints_WithoutTenantHeader_ReturnBadRequest()