ui progressing

This commit is contained in:
master
2026-02-20 23:32:20 +02:00
parent ca5e7888d6
commit 1ec797d5e8
191 changed files with 32771 additions and 6504 deletions

View File

@@ -0,0 +1,154 @@
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 ContextEndpointsTests : IClassFixture<PlatformWebApplicationFactory>
{
private readonly PlatformWebApplicationFactory _factory;
public ContextEndpointsTests(PlatformWebApplicationFactory factory)
{
_factory = factory;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RegionsAndEnvironments_ReturnDeterministicOrdering()
{
using var client = CreateTenantClient("tenant-context-1");
var regionsFirst = await client.GetFromJsonAsync<PlatformContextRegion[]>(
"/api/v2/context/regions",
TestContext.Current.CancellationToken);
var regionsSecond = await client.GetFromJsonAsync<PlatformContextRegion[]>(
"/api/v2/context/regions",
TestContext.Current.CancellationToken);
Assert.NotNull(regionsFirst);
Assert.NotNull(regionsSecond);
Assert.Equal(
new[] { "us-east", "eu-west", "apac" },
regionsFirst!.Select(region => region.RegionId).ToArray());
Assert.Equal(
regionsFirst.Select(region => region.RegionId).ToArray(),
regionsSecond!.Select(region => region.RegionId).ToArray());
var environments = await client.GetFromJsonAsync<PlatformContextEnvironment[]>(
"/api/v2/context/environments?regions=eu-west,us-east,eu-west",
TestContext.Current.CancellationToken);
Assert.NotNull(environments);
Assert.Equal(
new[] { "us-prod", "us-uat", "eu-prod", "eu-stage" },
environments!.Select(environment => environment.EnvironmentId).ToArray());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Preferences_DefaultAndRoundTrip_AreDeterministic()
{
using var client = CreateTenantClient("tenant-context-2");
var defaults = await client.GetFromJsonAsync<PlatformContextPreferences>(
"/api/v2/context/preferences",
TestContext.Current.CancellationToken);
Assert.NotNull(defaults);
Assert.Equal(new[] { "us-east", "eu-west", "apac" }, defaults!.Regions.ToArray());
Assert.Empty(defaults.Environments);
Assert.Equal("24h", defaults.TimeWindow);
var request = new PlatformContextPreferencesRequest(
Regions: new[] { "eu-west", "us-east", "unknown", "US-EAST" },
Environments: new[] { "eu-stage", "us-prod", "unknown", "apac-prod", "eu-prod", "US-PROD" },
TimeWindow: "7d");
var response = await client.PutAsJsonAsync(
"/api/v2/context/preferences",
request,
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var updated = await response.Content.ReadFromJsonAsync<PlatformContextPreferences>(
TestContext.Current.CancellationToken);
Assert.NotNull(updated);
Assert.Equal(new[] { "us-east", "eu-west" }, updated!.Regions.ToArray());
Assert.Equal(new[] { "us-prod", "eu-prod", "eu-stage" }, updated.Environments.ToArray());
Assert.Equal("7d", updated.TimeWindow);
var stored = await client.GetFromJsonAsync<PlatformContextPreferences>(
"/api/v2/context/preferences",
TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal(updated.Regions.ToArray(), stored!.Regions.ToArray());
Assert.Equal(updated.Environments.ToArray(), stored.Environments.ToArray());
Assert.Equal(updated.TimeWindow, stored.TimeWindow);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ContextEndpoints_WithoutTenantHeader_ReturnBadRequest()
{
using var client = _factory.CreateClient();
var response = await client.GetAsync(
"/api/v2/context/regions",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ContextEndpoints_RequireExpectedPolicies()
{
var endpoints = _factory.Services
.GetRequiredService<EndpointDataSource>()
.Endpoints
.OfType<RouteEndpoint>()
.ToArray();
AssertPolicy(endpoints, "/api/v2/context/regions", "GET", PlatformPolicies.ContextRead);
AssertPolicy(endpoints, "/api/v2/context/environments", "GET", PlatformPolicies.ContextRead);
AssertPolicy(endpoints, "/api/v2/context/preferences", "GET", PlatformPolicies.ContextRead);
AssertPolicy(endpoints, "/api/v2/context/preferences", "PUT", PlatformPolicies.ContextWrite);
}
private static void AssertPolicy(
IReadOnlyList<RouteEndpoint> endpoints,
string routePattern,
string method,
string expectedPolicy)
{
var endpoint = endpoints.Single(candidate =>
string.Equals(candidate.RoutePattern.RawText, routePattern, StringComparison.Ordinal)
&& candidate.Metadata
.GetMetadata<HttpMethodMetadata>()?
.HttpMethods
.Contains(method, StringComparer.OrdinalIgnoreCase) == true);
var policies = endpoint.Metadata
.GetOrderedMetadata<IAuthorizeData>()
.Select(metadata => metadata.Policy)
.Where(static policy => !string.IsNullOrWhiteSpace(policy))
.ToArray();
Assert.Contains(expectedPolicy, policies);
}
private HttpClient CreateTenantClient(string tenantId)
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId);
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "context-tests");
return client;
}
}

View File

@@ -0,0 +1,70 @@
using StellaOps.TestKit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class ContextMigrationScriptTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration047_DefinesGlobalContextSchemaObjects()
{
var scriptPath = GetMigrationPath("047_GlobalContextAndFilters.sql");
var sql = File.ReadAllText(scriptPath);
Assert.Contains("CREATE TABLE IF NOT EXISTS platform.context_regions", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS platform.context_environments", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS platform.ui_context_preferences", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (time_window IN ('1h', '24h', '7d', '30d', '90d'))", sql, StringComparison.Ordinal);
Assert.Contains("INSERT INTO platform.context_regions", sql, StringComparison.Ordinal);
Assert.Contains("INSERT INTO platform.context_environments", sql, StringComparison.Ordinal);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration047_IsPresentInReleaseMigrationSequence()
{
var migrationsDir = GetMigrationsDirectory();
var migrationNames = Directory.GetFiles(migrationsDir, "*.sql")
.Select(Path.GetFileName)
.Where(static name => name is not null)
.Select(static name => name!)
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
var index046 = Array.IndexOf(migrationNames, "046_TrustSigningAdministration.sql");
var index047 = Array.IndexOf(migrationNames, "047_GlobalContextAndFilters.sql");
Assert.True(index046 >= 0, "Expected migration 046 to exist.");
Assert.True(index047 > index046, "Expected migration 047 to appear after migration 046.");
}
private static string GetMigrationPath(string fileName)
{
return Path.Combine(GetMigrationsDirectory(), fileName);
}
private static string GetMigrationsDirectory()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
var candidate = Path.Combine(
current.FullName,
"src",
"Platform",
"__Libraries",
"StellaOps.Platform.Database",
"Migrations",
"Release");
if (Directory.Exists(candidate))
{
return candidate;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Could not locate Platform release migrations directory.");
}
}

View File

@@ -0,0 +1,72 @@
using StellaOps.TestKit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class IntegrationSourceHealthMigrationScriptTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration051_DefinesIntegrationSourceHealthProjectionObjects()
{
var scriptPath = GetMigrationPath("051_IntegrationSourceHealth.sql");
var sql = File.ReadAllText(scriptPath);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.integration_feed_source_health", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.integration_vex_source_health", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.integration_source_sync_watermarks", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (source_type IN ('advisory_feed'))", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (source_type IN ('vex_source'))", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (status IN ('healthy', 'degraded', 'offline'))", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (freshness IN ('fresh', 'stale', 'unknown'))", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (statement_format IN ('openvex', 'csaf_vex'))", sql, StringComparison.Ordinal);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration051_IsPresentInReleaseMigrationSequence()
{
var migrationsDir = GetMigrationsDirectory();
var migrationNames = Directory.GetFiles(migrationsDir, "*.sql")
.Select(Path.GetFileName)
.Where(static name => name is not null)
.Select(static name => name!)
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
var index050 = Array.IndexOf(migrationNames, "050_SecurityDispositionProjection.sql");
var index051 = Array.IndexOf(migrationNames, "051_IntegrationSourceHealth.sql");
Assert.True(index050 >= 0, "Expected migration 050 to exist.");
Assert.True(index051 > index050, "Expected migration 051 to appear after migration 050.");
}
private static string GetMigrationPath(string fileName)
{
return Path.Combine(GetMigrationsDirectory(), fileName);
}
private static string GetMigrationsDirectory()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
var candidate = Path.Combine(
current.FullName,
"src",
"Platform",
"__Libraries",
"StellaOps.Platform.Database",
"Migrations",
"Release");
if (Directory.Exists(candidate))
{
return candidate;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Could not locate Platform release migrations directory.");
}
}

View File

@@ -0,0 +1,233 @@
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 IntegrationsReadModelEndpointsTests : IClassFixture<PlatformWebApplicationFactory>
{
private readonly PlatformWebApplicationFactory _factory;
public IntegrationsReadModelEndpointsTests(PlatformWebApplicationFactory factory)
{
_factory = factory;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task IntegrationsEndpoints_ReturnDeterministicFeedAndVexSourceHealth_WithSecurityAndDashboardConsumerMetadata()
{
var tenantId = Guid.NewGuid().ToString("D");
using var client = CreateTenantClient(tenantId);
await SeedReleaseAsync(client, "checkout-integrations", "Checkout Integrations", "us-prod", "feed-refresh");
await SeedReleaseAsync(client, "billing-integrations", "Billing Integrations", "eu-prod", "vex-sync");
var feedsFirst = await client.GetFromJsonAsync<PlatformListResponse<IntegrationFeedProjection>>(
"/api/v2/integrations/feeds?limit=200&offset=0",
TestContext.Current.CancellationToken);
var feedsSecond = await client.GetFromJsonAsync<PlatformListResponse<IntegrationFeedProjection>>(
"/api/v2/integrations/feeds?limit=200&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(feedsFirst);
Assert.NotNull(feedsSecond);
Assert.NotEmpty(feedsFirst!.Items);
Assert.Equal(
feedsFirst.Items.Select(item => item.SourceId).ToArray(),
feedsSecond!.Items.Select(item => item.SourceId).ToArray());
Assert.All(feedsFirst.Items, item =>
{
Assert.Equal("advisory_feed", item.SourceType);
Assert.Contains(item.Status, new[] { "healthy", "degraded", "offline" });
Assert.Contains(item.Freshness, new[] { "fresh", "stale", "unknown" });
Assert.Contains("security-findings", item.ConsumerDomains);
Assert.Contains("dashboard-posture", item.ConsumerDomains);
});
var usFeeds = await client.GetFromJsonAsync<PlatformListResponse<IntegrationFeedProjection>>(
"/api/v2/integrations/feeds?region=us-east&sourceType=advisory_feed&limit=200&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(usFeeds);
Assert.NotEmpty(usFeeds!.Items);
Assert.All(usFeeds.Items, item =>
{
Assert.Equal("us-east", item.Region);
Assert.Equal("advisory_feed", item.SourceType);
});
var usProdFeeds = await client.GetFromJsonAsync<PlatformListResponse<IntegrationFeedProjection>>(
"/api/v2/integrations/feeds?environment=us-prod&limit=200&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(usProdFeeds);
Assert.NotEmpty(usProdFeeds!.Items);
Assert.All(usProdFeeds.Items, item =>
{
Assert.Equal("us-prod", item.Environment);
Assert.NotNull(item.LastSyncAt);
Assert.NotNull(item.FreshnessMinutes);
});
var vexFirst = await client.GetFromJsonAsync<PlatformListResponse<IntegrationVexSourceProjection>>(
"/api/v2/integrations/vex-sources?limit=200&offset=0",
TestContext.Current.CancellationToken);
var vexSecond = await client.GetFromJsonAsync<PlatformListResponse<IntegrationVexSourceProjection>>(
"/api/v2/integrations/vex-sources?limit=200&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(vexFirst);
Assert.NotNull(vexSecond);
Assert.NotEmpty(vexFirst!.Items);
Assert.Equal(
vexFirst.Items.Select(item => item.SourceId).ToArray(),
vexSecond!.Items.Select(item => item.SourceId).ToArray());
Assert.All(vexFirst.Items, item =>
{
Assert.Equal("vex_source", item.SourceType);
Assert.Equal("openvex", item.StatementFormat);
Assert.Contains(item.Status, new[] { "healthy", "degraded", "offline" });
Assert.Contains(item.Freshness, new[] { "fresh", "stale", "unknown" });
Assert.Contains("security-disposition", item.ConsumerDomains);
Assert.Contains("dashboard-posture", item.ConsumerDomains);
Assert.True(item.DocumentCount24h >= 20);
});
var euVex = await client.GetFromJsonAsync<PlatformListResponse<IntegrationVexSourceProjection>>(
"/api/v2/integrations/vex-sources?environment=eu-prod&limit=200&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(euVex);
Assert.NotEmpty(euVex!.Items);
Assert.All(euVex.Items, item => Assert.Equal("eu-prod", item.Environment));
var offlineVex = await client.GetFromJsonAsync<PlatformListResponse<IntegrationVexSourceProjection>>(
"/api/v2/integrations/vex-sources?environment=us-uat&status=offline&limit=200&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(offlineVex);
Assert.NotEmpty(offlineVex!.Items);
Assert.All(offlineVex.Items, item =>
{
Assert.Equal("us-uat", item.Environment);
Assert.Equal("offline", item.Status);
Assert.Equal("unknown", item.Freshness);
Assert.Null(item.LastSyncAt);
});
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task IntegrationsEndpoints_WithoutTenantHeader_ReturnBadRequest()
{
using var client = _factory.CreateClient();
var response = await client.GetAsync("/api/v2/integrations/feeds", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void IntegrationsEndpoints_RequireExpectedPolicies()
{
var endpoints = _factory.Services
.GetRequiredService<EndpointDataSource>()
.Endpoints
.OfType<RouteEndpoint>()
.ToArray();
AssertPolicy(endpoints, "/api/v2/integrations/feeds", "GET", PlatformPolicies.IntegrationsRead);
AssertPolicy(endpoints, "/api/v2/integrations/vex-sources", "GET", PlatformPolicies.IntegrationsVexRead);
}
private static void AssertPolicy(
IReadOnlyList<RouteEndpoint> endpoints,
string routePattern,
string method,
string expectedPolicy)
{
static string NormalizePattern(string value) =>
string.IsNullOrWhiteSpace(value) ? "/" : value.TrimEnd('/');
var expectedPattern = NormalizePattern(routePattern);
var endpoint = endpoints.Single(candidate =>
string.Equals(
NormalizePattern(candidate.RoutePattern.RawText ?? string.Empty),
expectedPattern,
StringComparison.Ordinal)
&& candidate.Metadata
.GetMetadata<HttpMethodMetadata>()?
.HttpMethods
.Contains(method, StringComparer.OrdinalIgnoreCase) == true);
var policies = endpoint.Metadata
.GetOrderedMetadata<IAuthorizeData>()
.Select(metadata => metadata.Policy)
.Where(static policy => !string.IsNullOrWhiteSpace(policy))
.ToArray();
Assert.Contains(expectedPolicy, policies);
}
private static async Task 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}-api",
ImageDigest: "sha256:1111111111111111111111111111111111111111111111111111111111111111",
DeployOrder: 10,
MetadataJson: "{\"runtime\":\"docker\"}"),
new ReleaseControlBundleComponentInput(
ComponentVersionId: $"{slug}@1.0.1",
ComponentName: $"{slug}-worker",
ImageDigest: "sha256:2222222222222222222222222222222222222222222222222222222222222222",
DeployOrder: 20,
MetadataJson: "{\"runtime\":\"compose\"}")
]),
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();
}
private HttpClient CreateTenantClient(string tenantId)
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId);
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "integrations-v2-tests");
return client;
}
}

View File

@@ -0,0 +1,193 @@
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using StellaOps.TestKit;
using System.Net;
using System.Net.Http.Json;
namespace StellaOps.Platform.WebService.Tests;
public sealed class LegacyAliasCompatibilityTelemetryTests : IClassFixture<PlatformWebApplicationFactory>
{
private readonly PlatformWebApplicationFactory _factory;
public LegacyAliasCompatibilityTelemetryTests(PlatformWebApplicationFactory factory)
{
_factory = factory;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CriticalPack22Surfaces_ExposeV1Aliases_AndEmitDeterministicDeprecationTelemetry()
{
var tenantId = Guid.NewGuid().ToString("D");
var telemetry = _factory.Services.GetRequiredService<LegacyAliasTelemetry>();
telemetry.Clear();
using var client = CreateTenantClient(tenantId);
await SeedReleaseAsync(client, "alias-release", "Alias Release", "us-prod", "alias-check");
var v1Context = await client.GetAsync("/api/v1/context/regions", TestContext.Current.CancellationToken);
var v2Context = await client.GetAsync("/api/v2/context/regions", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, v1Context.StatusCode);
Assert.Equal(HttpStatusCode.OK, v2Context.StatusCode);
var v1Releases = await client.GetFromJsonAsync<PlatformListResponse<ReleaseProjection>>(
"/api/v1/releases?limit=20&offset=0",
TestContext.Current.CancellationToken);
var v2Releases = await client.GetFromJsonAsync<PlatformListResponse<ReleaseProjection>>(
"/api/v2/releases?limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(v1Releases);
Assert.NotNull(v2Releases);
Assert.NotEmpty(v1Releases!.Items);
Assert.NotEmpty(v2Releases!.Items);
var v1Runs = await client.GetFromJsonAsync<PlatformListResponse<ReleaseRunProjection>>(
"/api/v1/releases/runs?limit=20&offset=0",
TestContext.Current.CancellationToken);
var v2Runs = await client.GetFromJsonAsync<PlatformListResponse<ReleaseRunProjection>>(
"/api/v2/releases/runs?limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(v1Runs);
Assert.NotNull(v2Runs);
Assert.NotEmpty(v1Runs!.Items);
Assert.NotEmpty(v2Runs!.Items);
var sampleRunId = v1Runs.Items[0].RunId;
var v1RunTimeline = await client.GetFromJsonAsync<PlatformItemResponse<ReleaseRunTimelineProjection>>(
$"/api/v1/releases/runs/{sampleRunId}/timeline",
TestContext.Current.CancellationToken);
var v2RunTimeline = await client.GetFromJsonAsync<PlatformItemResponse<ReleaseRunTimelineProjection>>(
$"/api/v2/releases/runs/{sampleRunId}/timeline",
TestContext.Current.CancellationToken);
Assert.NotNull(v1RunTimeline);
Assert.NotNull(v2RunTimeline);
Assert.Equal(sampleRunId, v1RunTimeline!.Item.RunId);
Assert.Equal(sampleRunId, v2RunTimeline!.Item.RunId);
var v1Topology = await client.GetFromJsonAsync<PlatformListResponse<TopologyRegionProjection>>(
"/api/v1/topology/regions?limit=20&offset=0",
TestContext.Current.CancellationToken);
var v2Topology = await client.GetFromJsonAsync<PlatformListResponse<TopologyRegionProjection>>(
"/api/v2/topology/regions?limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(v1Topology);
Assert.NotNull(v2Topology);
Assert.NotEmpty(v1Topology!.Items);
Assert.NotEmpty(v2Topology!.Items);
var v1Security = await client.GetFromJsonAsync<SecurityFindingsResponse>(
"/api/v1/security/findings?limit=20&offset=0",
TestContext.Current.CancellationToken);
var v2Security = await client.GetFromJsonAsync<SecurityFindingsResponse>(
"/api/v2/security/findings?limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(v1Security);
Assert.NotNull(v2Security);
Assert.NotEmpty(v1Security!.Items);
Assert.NotEmpty(v2Security!.Items);
var v1Integrations = await client.GetFromJsonAsync<PlatformListResponse<IntegrationFeedProjection>>(
"/api/v1/integrations/feeds?limit=20&offset=0",
TestContext.Current.CancellationToken);
var v2Integrations = await client.GetFromJsonAsync<PlatformListResponse<IntegrationFeedProjection>>(
"/api/v2/integrations/feeds?limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(v1Integrations);
Assert.NotNull(v2Integrations);
Assert.NotEmpty(v1Integrations!.Items);
Assert.NotEmpty(v2Integrations!.Items);
var usage = telemetry.Snapshot();
Assert.Contains(usage, item =>
string.Equals(item.AliasRoute, "/api/v1/context/regions", StringComparison.Ordinal)
&& string.Equals(item.CanonicalRoute, "/api/v2/context/regions", StringComparison.Ordinal)
&& string.Equals(item.EventKey, "alias_get_api_v1_context_regions", StringComparison.Ordinal));
Assert.Contains(usage, item =>
string.Equals(item.AliasRoute, "/api/v1/releases", StringComparison.Ordinal)
&& string.Equals(item.CanonicalRoute, "/api/v2/releases", StringComparison.Ordinal)
&& string.Equals(item.EventKey, "alias_get_api_v1_releases", StringComparison.Ordinal));
Assert.Contains(usage, item =>
item.AliasRoute.StartsWith("/api/v1/releases/runs", StringComparison.Ordinal)
&& item.CanonicalRoute.StartsWith("/api/v2/releases/runs", StringComparison.Ordinal)
&& item.EventKey.StartsWith("alias_get_api_v1_releases_runs", StringComparison.Ordinal));
Assert.Contains(usage, item =>
item.AliasRoute.Contains("/api/v1/releases/runs/", StringComparison.Ordinal)
&& item.AliasRoute.EndsWith("/timeline", StringComparison.Ordinal)
&& item.CanonicalRoute.Contains("/api/v2/releases/runs/", StringComparison.Ordinal)
&& item.CanonicalRoute.EndsWith("/timeline", StringComparison.Ordinal)
&& item.EventKey.StartsWith("alias_get_api_v1_releases_runs", StringComparison.Ordinal));
Assert.Contains(usage, item =>
string.Equals(item.AliasRoute, "/api/v1/topology/regions", StringComparison.Ordinal)
&& string.Equals(item.CanonicalRoute, "/api/v2/topology/regions", StringComparison.Ordinal)
&& string.Equals(item.EventKey, "alias_get_api_v1_topology_regions", StringComparison.Ordinal));
Assert.Contains(usage, item =>
string.Equals(item.AliasRoute, "/api/v1/security/findings", StringComparison.Ordinal)
&& string.Equals(item.CanonicalRoute, "/api/v2/security/findings", StringComparison.Ordinal)
&& string.Equals(item.EventKey, "alias_get_api_v1_security_findings", StringComparison.Ordinal));
Assert.Contains(usage, item =>
string.Equals(item.AliasRoute, "/api/v1/integrations/feeds", StringComparison.Ordinal)
&& string.Equals(item.CanonicalRoute, "/api/v2/integrations/feeds", StringComparison.Ordinal)
&& string.Equals(item.EventKey, "alias_get_api_v1_integrations_feeds", StringComparison.Ordinal));
Assert.DoesNotContain(usage, item =>
item.TenantHash is not null
&& item.TenantHash.Contains(tenantId, StringComparison.OrdinalIgnoreCase));
Assert.All(
usage.Where(item => item.AliasRoute.StartsWith("/api/v1/", StringComparison.Ordinal)),
item => Assert.StartsWith("alias_", item.EventKey));
}
private static async Task 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}-api",
ImageDigest: "sha256:1111111111111111111111111111111111111111111111111111111111111111",
DeployOrder: 10,
MetadataJson: "{\"runtime\":\"docker\"}")
]),
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();
}
private HttpClient CreateTenantClient(string tenantId)
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId);
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "legacy-alias-tests");
return client;
}
}

View File

@@ -0,0 +1,252 @@
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 ReleaseReadModelEndpointsTests : IClassFixture<PlatformWebApplicationFactory>
{
private readonly PlatformWebApplicationFactory _factory;
public ReleaseReadModelEndpointsTests(PlatformWebApplicationFactory factory)
{
_factory = factory;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ReleasesListDetailActivityAndApprovals_ReturnDeterministicProjections()
{
var tenantId = Guid.NewGuid().ToString("D");
using var client = CreateTenantClient(tenantId);
var checkout = await SeedReleaseAsync(
client,
"checkout-hotfix",
"Checkout Hotfix",
"us-prod",
"critical-fix");
var payments = await SeedReleaseAsync(
client,
"payments-release",
"Payments Release",
"eu-prod",
"policy-review");
var releasesFirst = await client.GetFromJsonAsync<PlatformListResponse<ReleaseProjection>>(
"/api/v2/releases?limit=20&offset=0",
TestContext.Current.CancellationToken);
var releasesSecond = await client.GetFromJsonAsync<PlatformListResponse<ReleaseProjection>>(
"/api/v2/releases?limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(releasesFirst);
Assert.NotNull(releasesSecond);
Assert.True(releasesFirst!.Items.Count >= 2);
Assert.Equal(
releasesFirst.Items.Select(item => item.ReleaseId).ToArray(),
releasesSecond!.Items.Select(item => item.ReleaseId).ToArray());
var hotfix = releasesFirst.Items.Single(item => item.ReleaseId == checkout.Bundle.Id.ToString("D"));
Assert.Equal("hotfix", hotfix.ReleaseType);
Assert.Equal("pending_approval", hotfix.Status);
Assert.Equal("us-prod", hotfix.TargetEnvironment);
Assert.Equal("us-east", hotfix.TargetRegion);
var detail = await client.GetFromJsonAsync<PlatformItemResponse<ReleaseDetailProjection>>(
$"/api/v2/releases/{checkout.Bundle.Id:D}",
TestContext.Current.CancellationToken);
Assert.NotNull(detail);
Assert.Equal(checkout.Bundle.Id.ToString("D"), detail!.Item.Summary.ReleaseId);
Assert.NotEmpty(detail.Item.Versions);
Assert.Contains(detail.Item.Approvals, approval =>
string.Equals(approval.TargetEnvironment, "us-prod", StringComparison.Ordinal));
var activityFirst = await client.GetFromJsonAsync<PlatformListResponse<ReleaseActivityProjection>>(
"/api/v2/releases/activity?limit=50&offset=0",
TestContext.Current.CancellationToken);
var activitySecond = await client.GetFromJsonAsync<PlatformListResponse<ReleaseActivityProjection>>(
"/api/v2/releases/activity?limit=50&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(activityFirst);
Assert.NotNull(activitySecond);
Assert.Contains(activityFirst!.Items, item => item.ReleaseId == checkout.Bundle.Id.ToString("D"));
Assert.Contains(activityFirst.Items, item => item.ReleaseId == payments.Bundle.Id.ToString("D"));
Assert.Equal(
activityFirst.Items.Select(item => item.ActivityId).ToArray(),
activitySecond!.Items.Select(item => item.ActivityId).ToArray());
var approvalsFirst = await client.GetFromJsonAsync<PlatformListResponse<ReleaseApprovalProjection>>(
"/api/v2/releases/approvals?limit=20&offset=0",
TestContext.Current.CancellationToken);
var approvalsSecond = await client.GetFromJsonAsync<PlatformListResponse<ReleaseApprovalProjection>>(
"/api/v2/releases/approvals?limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(approvalsFirst);
Assert.NotNull(approvalsSecond);
Assert.True(approvalsFirst!.Items.Count >= 2);
Assert.All(approvalsFirst.Items, item => Assert.Equal("pending", item.Status));
Assert.Equal(
approvalsFirst.Items.Select(item => item.ApprovalId).ToArray(),
approvalsSecond!.Items.Select(item => item.ApprovalId).ToArray());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ReleasesEndpoints_ApplyRegionEnvironmentAndStatusFilters()
{
var tenantId = Guid.NewGuid().ToString("D");
using var client = CreateTenantClient(tenantId);
await SeedReleaseAsync(client, "orders-hotfix", "Orders Hotfix", "us-prod", "critical-fix");
await SeedReleaseAsync(client, "inventory-release", "Inventory Release", "eu-prod", "policy-review");
var usReleases = await client.GetFromJsonAsync<PlatformListResponse<ReleaseProjection>>(
"/api/v2/releases?region=us-east&limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(usReleases);
Assert.NotEmpty(usReleases!.Items);
Assert.All(usReleases.Items, item => Assert.Equal("us-east", item.TargetRegion));
var euActivity = await client.GetFromJsonAsync<PlatformListResponse<ReleaseActivityProjection>>(
"/api/v2/releases/activity?environment=eu-prod&limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(euActivity);
Assert.NotEmpty(euActivity!.Items);
Assert.All(euActivity.Items, item => Assert.Equal("eu-prod", item.TargetEnvironment));
var euApprovals = await client.GetFromJsonAsync<PlatformListResponse<ReleaseApprovalProjection>>(
"/api/v2/releases/approvals?status=pending&region=eu-west&limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(euApprovals);
Assert.NotEmpty(euApprovals!.Items);
Assert.All(euApprovals.Items, item => Assert.Equal("eu-west", item.TargetRegion));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ReleasesEndpoints_WithoutTenantHeader_ReturnBadRequest()
{
using var client = _factory.CreateClient();
var response = await client.GetAsync("/api/v2/releases", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReleasesEndpoints_RequireExpectedPolicies()
{
var endpoints = _factory.Services
.GetRequiredService<EndpointDataSource>()
.Endpoints
.OfType<RouteEndpoint>()
.ToArray();
AssertPolicy(endpoints, "/api/v2/releases", "GET", PlatformPolicies.ReleaseControlRead);
AssertPolicy(endpoints, "/api/v2/releases/{releaseId:guid}", "GET", PlatformPolicies.ReleaseControlRead);
AssertPolicy(endpoints, "/api/v2/releases/activity", "GET", PlatformPolicies.ReleaseControlRead);
AssertPolicy(endpoints, "/api/v2/releases/approvals", "GET", PlatformPolicies.ReleaseControlRead);
}
private static void AssertPolicy(
IReadOnlyList<RouteEndpoint> endpoints,
string routePattern,
string method,
string expectedPolicy)
{
static string NormalizePattern(string value) =>
string.IsNullOrWhiteSpace(value) ? "/" : value.TrimEnd('/');
var expectedPattern = NormalizePattern(routePattern);
var endpoint = endpoints.Single(candidate =>
string.Equals(
NormalizePattern(candidate.RoutePattern.RawText ?? string.Empty),
expectedPattern,
StringComparison.Ordinal)
&& candidate.Metadata
.GetMetadata<HttpMethodMetadata>()?
.HttpMethods
.Contains(method, StringComparer.OrdinalIgnoreCase) == true);
var policies = endpoint.Metadata
.GetOrderedMetadata<IAuthorizeData>()
.Select(metadata => metadata.Policy)
.Where(static policy => !string.IsNullOrWhiteSpace(policy))
.ToArray();
Assert.Contains(expectedPolicy, 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", "releases-v2-tests");
return client;
}
private sealed record SeededRelease(
ReleaseControlBundleDetail Bundle,
ReleaseControlBundleVersionDetail Version,
ReleaseControlBundleMaterializationRun Run);
}

View File

@@ -0,0 +1,70 @@
using StellaOps.TestKit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class ReleaseReadModelMigrationScriptTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration048_DefinesReleaseReadModelProjectionObjects()
{
var scriptPath = GetMigrationPath("048_ReleaseReadModels.sql");
var sql = File.ReadAllText(scriptPath);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.release_read_model", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.release_activity_projection", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.release_approvals_projection", sql, StringComparison.Ordinal);
Assert.Contains("correlation_key", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (release_type IN ('standard', 'hotfix'))", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (approval_status IN ('pending', 'approved', 'rejected'))", sql, StringComparison.Ordinal);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration048_IsPresentInReleaseMigrationSequence()
{
var migrationsDir = GetMigrationsDirectory();
var migrationNames = Directory.GetFiles(migrationsDir, "*.sql")
.Select(Path.GetFileName)
.Where(static name => name is not null)
.Select(static name => name!)
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
var index047 = Array.IndexOf(migrationNames, "047_GlobalContextAndFilters.sql");
var index048 = Array.IndexOf(migrationNames, "048_ReleaseReadModels.sql");
Assert.True(index047 >= 0, "Expected migration 047 to exist.");
Assert.True(index048 > index047, "Expected migration 048 to appear after migration 047.");
}
private static string GetMigrationPath(string fileName)
{
return Path.Combine(GetMigrationsDirectory(), fileName);
}
private static string GetMigrationsDirectory()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
var candidate = Path.Combine(
current.FullName,
"src",
"Platform",
"__Libraries",
"StellaOps.Platform.Database",
"Migrations",
"Release");
if (Directory.Exists(candidate))
{
return candidate;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Could not locate Platform release migrations directory.");
}
}

View File

@@ -0,0 +1,244 @@
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 ReleaseRunEndpointsTests : IClassFixture<PlatformWebApplicationFactory>
{
private readonly PlatformWebApplicationFactory factory;
public ReleaseRunEndpointsTests(PlatformWebApplicationFactory factory)
{
this.factory = factory;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RunEndpoints_ReturnDeterministicRunCentricContractsAcrossTabs()
{
var tenantId = Guid.NewGuid().ToString("D");
using var client = CreateTenantClient(tenantId);
var hotfix = await SeedReleaseAsync(client, "checkout-hotfix", "Checkout Hotfix", "us-prod", "stale-integrity-window");
await SeedReleaseAsync(client, "payments-release", "Payments Release", "eu-prod", "routine-promotion");
var runsFirst = await client.GetFromJsonAsync<PlatformListResponse<ReleaseRunProjection>>(
"/api/v2/releases/runs?limit=50&offset=0",
TestContext.Current.CancellationToken);
var runsSecond = await client.GetFromJsonAsync<PlatformListResponse<ReleaseRunProjection>>(
"/api/v2/releases/runs?limit=50&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(runsFirst);
Assert.NotNull(runsSecond);
Assert.True(runsFirst!.Items.Count >= 2);
Assert.Equal(
runsFirst.Items.Select(item => item.RunId).ToArray(),
runsSecond!.Items.Select(item => item.RunId).ToArray());
var hotfixProjection = runsFirst.Items.Single(item => item.RunId == hotfix.Run.RunId.ToString("D"));
Assert.Equal("hotfix", hotfixProjection.Lane);
Assert.Equal("us-prod", hotfixProjection.TargetEnvironment);
Assert.Equal("us-east", hotfixProjection.TargetRegion);
Assert.True(hotfixProjection.BlockedByDataIntegrity);
var detail = await client.GetFromJsonAsync<PlatformItemResponse<ReleaseRunDetailProjection>>(
$"/api/v2/releases/runs/{hotfix.Run.RunId:D}",
TestContext.Current.CancellationToken);
Assert.NotNull(detail);
Assert.Equal(hotfix.Run.RunId.ToString("D"), detail!.Item.RunId);
Assert.Equal("connect", detail.Item.Process[0].StepId);
Assert.Equal(5, detail.Item.Process.Count);
var timeline = await client.GetFromJsonAsync<PlatformItemResponse<ReleaseRunTimelineProjection>>(
$"/api/v2/releases/runs/{hotfix.Run.RunId:D}/timeline",
TestContext.Current.CancellationToken);
Assert.NotNull(timeline);
Assert.Equal(hotfix.Run.RunId.ToString("D"), timeline!.Item.RunId);
Assert.True(timeline.Item.Events.Count >= 6);
Assert.Contains(timeline.Item.Correlations, item => string.Equals(item.Type, "snapshot_id", StringComparison.Ordinal));
var gate = await client.GetFromJsonAsync<PlatformItemResponse<ReleaseRunGateDecisionProjection>>(
$"/api/v2/releases/runs/{hotfix.Run.RunId:D}/gate-decision",
TestContext.Current.CancellationToken);
Assert.NotNull(gate);
Assert.Equal(hotfix.Run.RunId.ToString("D"), gate!.Item.RunId);
Assert.NotEmpty(gate.Item.PolicyPackVersion);
Assert.NotEmpty(gate.Item.MachineReasonCodes);
var approvals = await client.GetFromJsonAsync<PlatformItemResponse<ReleaseRunApprovalsProjection>>(
$"/api/v2/releases/runs/{hotfix.Run.RunId:D}/approvals",
TestContext.Current.CancellationToken);
Assert.NotNull(approvals);
Assert.Equal(new[] { 1, 2 }, approvals!.Item.Checkpoints.Select(item => item.Order).ToArray());
var deployments = await client.GetFromJsonAsync<PlatformItemResponse<ReleaseRunDeploymentsProjection>>(
$"/api/v2/releases/runs/{hotfix.Run.RunId:D}/deployments",
TestContext.Current.CancellationToken);
Assert.NotNull(deployments);
Assert.NotEmpty(deployments!.Item.Targets);
Assert.NotEmpty(deployments.Item.RollbackTriggers);
var securityInputs = await client.GetFromJsonAsync<PlatformItemResponse<ReleaseRunSecurityInputsProjection>>(
$"/api/v2/releases/runs/{hotfix.Run.RunId:D}/security-inputs",
TestContext.Current.CancellationToken);
Assert.NotNull(securityInputs);
Assert.InRange(securityInputs!.Item.ReachabilityCoveragePercent, 0, 100);
Assert.NotEmpty(securityInputs.Item.Drilldowns);
var evidence = await client.GetFromJsonAsync<PlatformItemResponse<ReleaseRunEvidenceProjection>>(
$"/api/v2/releases/runs/{hotfix.Run.RunId:D}/evidence",
TestContext.Current.CancellationToken);
Assert.NotNull(evidence);
Assert.Contains("capsule-", evidence!.Item.DecisionCapsuleId, StringComparison.Ordinal);
var rollback = await client.GetFromJsonAsync<PlatformItemResponse<ReleaseRunRollbackProjection>>(
$"/api/v2/releases/runs/{hotfix.Run.RunId:D}/rollback",
TestContext.Current.CancellationToken);
Assert.NotNull(rollback);
Assert.NotEmpty(rollback!.Item.KnownGoodReferences);
var replay = await client.GetFromJsonAsync<PlatformItemResponse<ReleaseRunReplayProjection>>(
$"/api/v2/releases/runs/{hotfix.Run.RunId:D}/replay",
TestContext.Current.CancellationToken);
Assert.NotNull(replay);
Assert.Contains(replay!.Item.Verdict, new[] { "match", "mismatch", "not_available" });
var audit = await client.GetFromJsonAsync<PlatformItemResponse<ReleaseRunAuditProjection>>(
$"/api/v2/releases/runs/{hotfix.Run.RunId:D}/audit",
TestContext.Current.CancellationToken);
Assert.NotNull(audit);
Assert.NotEmpty(audit!.Item.Entries);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RunEndpoints_WithoutTenantHeader_ReturnBadRequest()
{
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v2/releases/runs", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RunEndpoints_RequireExpectedPolicies()
{
var endpoints = factory.Services
.GetRequiredService<EndpointDataSource>()
.Endpoints
.OfType<RouteEndpoint>()
.ToArray();
AssertPolicy(endpoints, "/api/v2/releases/runs", "GET", PlatformPolicies.ReleaseControlRead);
AssertPolicy(endpoints, "/api/v2/releases/runs/{runId:guid}", "GET", PlatformPolicies.ReleaseControlRead);
AssertPolicy(endpoints, "/api/v2/releases/runs/{runId:guid}/timeline", "GET", PlatformPolicies.ReleaseControlRead);
AssertPolicy(endpoints, "/api/v2/releases/runs/{runId:guid}/gate-decision", "GET", PlatformPolicies.ReleaseControlRead);
AssertPolicy(endpoints, "/api/v2/releases/runs/{runId:guid}/approvals", "GET", PlatformPolicies.ReleaseControlRead);
AssertPolicy(endpoints, "/api/v2/releases/runs/{runId:guid}/deployments", "GET", PlatformPolicies.ReleaseControlRead);
AssertPolicy(endpoints, "/api/v2/releases/runs/{runId:guid}/security-inputs", "GET", PlatformPolicies.ReleaseControlRead);
AssertPolicy(endpoints, "/api/v2/releases/runs/{runId:guid}/evidence", "GET", PlatformPolicies.ReleaseControlRead);
AssertPolicy(endpoints, "/api/v2/releases/runs/{runId:guid}/rollback", "GET", PlatformPolicies.ReleaseControlRead);
AssertPolicy(endpoints, "/api/v2/releases/runs/{runId:guid}/replay", "GET", PlatformPolicies.ReleaseControlRead);
AssertPolicy(endpoints, "/api/v2/releases/runs/{runId:guid}/audit", "GET", PlatformPolicies.ReleaseControlRead);
}
private static void AssertPolicy(
IReadOnlyList<RouteEndpoint> endpoints,
string routePattern,
string method,
string expectedPolicy)
{
static string NormalizePattern(string value) =>
string.IsNullOrWhiteSpace(value) ? "/" : value.TrimEnd('/');
var expectedPattern = NormalizePattern(routePattern);
var endpoint = endpoints.Single(candidate =>
string.Equals(
NormalizePattern(candidate.RoutePattern.RawText ?? string.Empty),
expectedPattern,
StringComparison.Ordinal)
&& candidate.Metadata
.GetMetadata<HttpMethodMetadata>()?
.HttpMethods
.Contains(method, StringComparer.OrdinalIgnoreCase) == true);
var policies = endpoint.Metadata
.GetOrderedMetadata<IAuthorizeData>()
.Select(metadata => metadata.Policy)
.Where(static policy => !string.IsNullOrWhiteSpace(policy))
.ToArray();
Assert.Contains(expectedPolicy, 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", "release-runs-v2-tests");
return client;
}
private sealed record SeededRelease(
ReleaseControlBundleDetail Bundle,
ReleaseControlBundleVersionDetail Version,
ReleaseControlBundleMaterializationRun Run);
}

View File

@@ -0,0 +1,69 @@
using StellaOps.TestKit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class RunApprovalCheckpointsMigrationScriptTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration054_DefinesRunApprovalCheckpointObjects()
{
var scriptPath = GetMigrationPath("054_RunApprovalCheckpoints.sql");
var sql = File.ReadAllText(scriptPath);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.run_approval_checkpoints", sql, StringComparison.Ordinal);
Assert.Contains("ux_release_run_approval_checkpoints_order", sql, StringComparison.Ordinal);
Assert.Contains("signature_algorithm", sql, StringComparison.Ordinal);
Assert.Contains("signature_value", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (status IN ('pending', 'approved', 'rejected', 'skipped'))", sql, StringComparison.Ordinal);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration054_IsPresentInReleaseMigrationSequence()
{
var migrationsDir = GetMigrationsDirectory();
var migrationNames = Directory.GetFiles(migrationsDir, "*.sql")
.Select(Path.GetFileName)
.Where(static name => name is not null)
.Select(static name => name!)
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
var index053 = Array.IndexOf(migrationNames, "053_RunGateDecisionLedger.sql");
var index054 = Array.IndexOf(migrationNames, "054_RunApprovalCheckpoints.sql");
Assert.True(index053 >= 0, "Expected migration 053 to exist.");
Assert.True(index054 > index053, "Expected migration 054 to appear after migration 053.");
}
private static string GetMigrationPath(string fileName)
{
return Path.Combine(GetMigrationsDirectory(), fileName);
}
private static string GetMigrationsDirectory()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
var candidate = Path.Combine(
current.FullName,
"src",
"Platform",
"__Libraries",
"StellaOps.Platform.Database",
"Migrations",
"Release");
if (Directory.Exists(candidate))
{
return candidate;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Could not locate Platform release migrations directory.");
}
}

View File

@@ -0,0 +1,70 @@
using StellaOps.TestKit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class RunCapsuleReplayLinkageMigrationScriptTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration056_DefinesRunCapsuleReplayLinkageObjects()
{
var scriptPath = GetMigrationPath("056_RunCapsuleReplayLinkage.sql");
var sql = File.ReadAllText(scriptPath);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.run_capsule_replay_linkage", sql, StringComparison.Ordinal);
Assert.Contains("ux_release_run_capsule_replay_linkage_run", sql, StringComparison.Ordinal);
Assert.Contains("ix_release_run_capsule_replay_linkage_verdict", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (signature_status IN ('signed', 'unsigned', 'invalid'))", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (replay_verdict IN ('match', 'mismatch', 'not_available'))", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (replay_verdict <> 'mismatch' OR replay_mismatch_report_ref IS NOT NULL)", sql, StringComparison.Ordinal);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration056_IsPresentInReleaseMigrationSequence()
{
var migrationsDir = GetMigrationsDirectory();
var migrationNames = Directory.GetFiles(migrationsDir, "*.sql")
.Select(Path.GetFileName)
.Where(static name => name is not null)
.Select(static name => name!)
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
var index055 = Array.IndexOf(migrationNames, "055_RunDeploymentTimeline.sql");
var index056 = Array.IndexOf(migrationNames, "056_RunCapsuleReplayLinkage.sql");
Assert.True(index055 >= 0, "Expected migration 055 to exist.");
Assert.True(index056 > index055, "Expected migration 056 to appear after migration 055.");
}
private static string GetMigrationPath(string fileName)
{
return Path.Combine(GetMigrationsDirectory(), fileName);
}
private static string GetMigrationsDirectory()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
var candidate = Path.Combine(
current.FullName,
"src",
"Platform",
"__Libraries",
"StellaOps.Platform.Database",
"Migrations",
"Release");
if (Directory.Exists(candidate))
{
return candidate;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Could not locate Platform release migrations directory.");
}
}

View File

@@ -0,0 +1,70 @@
using StellaOps.TestKit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class RunDeploymentTimelineMigrationScriptTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration055_DefinesRunDeploymentTimelineObjects()
{
var scriptPath = GetMigrationPath("055_RunDeploymentTimeline.sql");
var sql = File.ReadAllText(scriptPath);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.run_deployment_timeline", sql, StringComparison.Ordinal);
Assert.Contains("ix_release_run_deployment_timeline_run", sql, StringComparison.Ordinal);
Assert.Contains("ix_release_run_deployment_timeline_filters", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (strategy IN ('canary', 'rolling', 'blue_green', 'recreate'))", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (phase IN ('queued', 'precheck', 'deploying', 'verifying', 'completed', 'rollback'))", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (status IN ('pending', 'running', 'succeeded', 'failed', 'rolled_back'))", sql, StringComparison.Ordinal);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration055_IsPresentInReleaseMigrationSequence()
{
var migrationsDir = GetMigrationsDirectory();
var migrationNames = Directory.GetFiles(migrationsDir, "*.sql")
.Select(Path.GetFileName)
.Where(static name => name is not null)
.Select(static name => name!)
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
var index054 = Array.IndexOf(migrationNames, "054_RunApprovalCheckpoints.sql");
var index055 = Array.IndexOf(migrationNames, "055_RunDeploymentTimeline.sql");
Assert.True(index054 >= 0, "Expected migration 054 to exist.");
Assert.True(index055 > index054, "Expected migration 055 to appear after migration 054.");
}
private static string GetMigrationPath(string fileName)
{
return Path.Combine(GetMigrationsDirectory(), fileName);
}
private static string GetMigrationsDirectory()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
var candidate = Path.Combine(
current.FullName,
"src",
"Platform",
"__Libraries",
"StellaOps.Platform.Database",
"Migrations",
"Release");
if (Directory.Exists(candidate))
{
return candidate;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Could not locate Platform release migrations directory.");
}
}

View File

@@ -0,0 +1,69 @@
using StellaOps.TestKit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class RunGateDecisionLedgerMigrationScriptTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration053_DefinesRunGateDecisionLedgerObjects()
{
var scriptPath = GetMigrationPath("053_RunGateDecisionLedger.sql");
var sql = File.ReadAllText(scriptPath);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.run_gate_decision_ledger", sql, StringComparison.Ordinal);
Assert.Contains("risk_budget_delta", sql, StringComparison.Ordinal);
Assert.Contains("risk_budget_contributors", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (verdict IN ('allow', 'review', 'block'))", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (staleness_verdict IN ('fresh', 'stale', 'unknown'))", sql, StringComparison.Ordinal);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration053_IsPresentInReleaseMigrationSequence()
{
var migrationsDir = GetMigrationsDirectory();
var migrationNames = Directory.GetFiles(migrationsDir, "*.sql")
.Select(Path.GetFileName)
.Where(static name => name is not null)
.Select(static name => name!)
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
var index052 = Array.IndexOf(migrationNames, "052_RunInputSnapshots.sql");
var index053 = Array.IndexOf(migrationNames, "053_RunGateDecisionLedger.sql");
Assert.True(index052 >= 0, "Expected migration 052 to exist.");
Assert.True(index053 > index052, "Expected migration 053 to appear after migration 052.");
}
private static string GetMigrationPath(string fileName)
{
return Path.Combine(GetMigrationsDirectory(), fileName);
}
private static string GetMigrationsDirectory()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
var candidate = Path.Combine(
current.FullName,
"src",
"Platform",
"__Libraries",
"StellaOps.Platform.Database",
"Migrations",
"Release");
if (Directory.Exists(candidate))
{
return candidate;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Could not locate Platform release migrations directory.");
}
}

View File

@@ -0,0 +1,69 @@
using StellaOps.TestKit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class RunInputSnapshotsMigrationScriptTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration052_DefinesRunInputSnapshotObjects()
{
var scriptPath = GetMigrationPath("052_RunInputSnapshots.sql");
var sql = File.ReadAllText(scriptPath);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.run_input_snapshots", sql, StringComparison.Ordinal);
Assert.Contains("ux_release_run_input_snapshots_tenant_run", sql, StringComparison.Ordinal);
Assert.Contains("ix_release_run_input_snapshots_tenant_bundle_version", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (feed_freshness_status IN ('fresh', 'stale', 'unknown'))", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (reachability_coverage_percent BETWEEN 0 AND 100)", sql, StringComparison.Ordinal);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration052_IsPresentInReleaseMigrationSequence()
{
var migrationsDir = GetMigrationsDirectory();
var migrationNames = Directory.GetFiles(migrationsDir, "*.sql")
.Select(Path.GetFileName)
.Where(static name => name is not null)
.Select(static name => name!)
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
var index051 = Array.IndexOf(migrationNames, "051_IntegrationSourceHealth.sql");
var index052 = Array.IndexOf(migrationNames, "052_RunInputSnapshots.sql");
Assert.True(index051 >= 0, "Expected migration 051 to exist.");
Assert.True(index052 > index051, "Expected migration 052 to appear after migration 051.");
}
private static string GetMigrationPath(string fileName)
{
return Path.Combine(GetMigrationsDirectory(), fileName);
}
private static string GetMigrationsDirectory()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
var candidate = Path.Combine(
current.FullName,
"src",
"Platform",
"__Libraries",
"StellaOps.Platform.Database",
"Migrations",
"Release");
if (Directory.Exists(candidate))
{
return candidate;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Could not locate Platform release migrations directory.");
}
}

View File

@@ -0,0 +1,71 @@
using StellaOps.TestKit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class SecurityDispositionMigrationScriptTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration050_DefinesSecurityDispositionProjectionObjects()
{
var scriptPath = GetMigrationPath("050_SecurityDispositionProjection.sql");
var sql = File.ReadAllText(scriptPath);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.security_finding_projection", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.security_disposition_projection", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.security_sbom_component_projection", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.security_sbom_graph_projection", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (severity IN ('critical', 'high', 'medium', 'low', 'info'))", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (vex_status IN ('affected', 'not_affected', 'under_investigation'))", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (exception_status IN ('none', 'pending', 'approved', 'rejected'))", sql, StringComparison.Ordinal);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration050_IsPresentInReleaseMigrationSequence()
{
var migrationsDir = GetMigrationsDirectory();
var migrationNames = Directory.GetFiles(migrationsDir, "*.sql")
.Select(Path.GetFileName)
.Where(static name => name is not null)
.Select(static name => name!)
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
var index049 = Array.IndexOf(migrationNames, "049_TopologyInventory.sql");
var index050 = Array.IndexOf(migrationNames, "050_SecurityDispositionProjection.sql");
Assert.True(index049 >= 0, "Expected migration 049 to exist.");
Assert.True(index050 > index049, "Expected migration 050 to appear after migration 049.");
}
private static string GetMigrationPath(string fileName)
{
return Path.Combine(GetMigrationsDirectory(), fileName);
}
private static string GetMigrationsDirectory()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
var candidate = Path.Combine(
current.FullName,
"src",
"Platform",
"__Libraries",
"StellaOps.Platform.Database",
"Migrations",
"Release");
if (Directory.Exists(candidate))
{
return candidate;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Could not locate Platform release migrations directory.");
}
}

View File

@@ -0,0 +1,224 @@
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 SecurityReadModelEndpointsTests : IClassFixture<PlatformWebApplicationFactory>
{
private readonly PlatformWebApplicationFactory _factory;
public SecurityReadModelEndpointsTests(PlatformWebApplicationFactory factory)
{
_factory = factory;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SecurityEndpoints_ReturnDeterministicFindingsDispositionAndSbomExplorer()
{
var tenantId = Guid.NewGuid().ToString("D");
using var client = CreateTenantClient(tenantId);
var checkout = await SeedReleaseAsync(client, "checkout-security-release", "Checkout Security Release", "us-prod", "security-exception");
var billing = await SeedReleaseAsync(client, "billing-security-release", "Billing Security Release", "eu-prod", "policy-review");
var findingsFirst = await client.GetFromJsonAsync<SecurityFindingsResponse>(
"/api/v2/security/findings?pivot=component&limit=50&offset=0",
TestContext.Current.CancellationToken);
var findingsSecond = await client.GetFromJsonAsync<SecurityFindingsResponse>(
"/api/v2/security/findings?pivot=component&limit=50&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(findingsFirst);
Assert.NotNull(findingsSecond);
Assert.NotEmpty(findingsFirst!.Items);
Assert.NotEmpty(findingsFirst.PivotBuckets);
Assert.NotEmpty(findingsFirst.Facets);
Assert.Equal(
findingsFirst.Items.Select(item => item.FindingId).ToArray(),
findingsSecond!.Items.Select(item => item.FindingId).ToArray());
var usFindings = await client.GetFromJsonAsync<SecurityFindingsResponse>(
"/api/v2/security/findings?region=us-east&severity=critical&limit=50&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(usFindings);
Assert.All(usFindings!.Items, item => Assert.Equal("us-east", item.Region));
Assert.All(usFindings.Items, item => Assert.Equal("critical", item.Severity));
var dispositionList = await client.GetFromJsonAsync<PlatformListResponse<SecurityDispositionProjection>>(
"/api/v2/security/disposition?limit=50&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(dispositionList);
Assert.NotEmpty(dispositionList!.Items);
Assert.All(dispositionList.Items, item =>
{
Assert.Equal("vex", item.Vex.SourceModel);
Assert.Equal("exceptions", item.Exception.SourceModel);
});
var firstFindingId = dispositionList.Items[0].FindingId;
var dispositionDetail = await client.GetFromJsonAsync<PlatformItemResponse<SecurityDispositionProjection>>(
$"/api/v2/security/disposition/{firstFindingId}",
TestContext.Current.CancellationToken);
Assert.NotNull(dispositionDetail);
Assert.Equal(firstFindingId, dispositionDetail!.Item.FindingId);
var sbomTable = await client.GetFromJsonAsync<SecuritySbomExplorerResponse>(
"/api/v2/security/sbom-explorer?mode=table&limit=50&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(sbomTable);
Assert.Equal("table", sbomTable!.Mode);
Assert.NotEmpty(sbomTable.Table);
Assert.Empty(sbomTable.GraphNodes);
Assert.Empty(sbomTable.GraphEdges);
var sbomGraph = await client.GetFromJsonAsync<SecuritySbomExplorerResponse>(
"/api/v2/security/sbom-explorer?mode=graph&limit=50&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(sbomGraph);
Assert.Equal("graph", sbomGraph!.Mode);
Assert.NotEmpty(sbomGraph.GraphNodes);
Assert.NotEmpty(sbomGraph.GraphEdges);
var sbomDiff = await client.GetFromJsonAsync<SecuritySbomExplorerResponse>(
$"/api/v2/security/sbom-explorer?mode=diff&leftReleaseId={checkout.Bundle.Id:D}&rightReleaseId={billing.Bundle.Id:D}&limit=50&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(sbomDiff);
Assert.Equal("diff", sbomDiff!.Mode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SecurityEndpoints_WithoutTenantHeader_ReturnBadRequest()
{
using var client = _factory.CreateClient();
var response = await client.GetAsync("/api/v2/security/findings", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SecurityEndpoints_RequireExpectedPolicies_AndDoNotExposeCombinedWriteRoute()
{
var endpoints = _factory.Services
.GetRequiredService<EndpointDataSource>()
.Endpoints
.OfType<RouteEndpoint>()
.ToArray();
AssertPolicy(endpoints, "/api/v2/security/findings", "GET", PlatformPolicies.SecurityRead);
AssertPolicy(endpoints, "/api/v2/security/disposition", "GET", PlatformPolicies.SecurityRead);
AssertPolicy(endpoints, "/api/v2/security/disposition/{findingId}", "GET", PlatformPolicies.SecurityRead);
AssertPolicy(endpoints, "/api/v2/security/sbom-explorer", "GET", PlatformPolicies.SecurityRead);
var hasCombinedWrite = endpoints.Any(candidate =>
string.Equals(candidate.RoutePattern.RawText, "/api/v2/security/disposition/exceptions", StringComparison.Ordinal)
&& candidate.Metadata.GetMetadata<HttpMethodMetadata>()?.HttpMethods.Contains("POST", StringComparer.OrdinalIgnoreCase) == true);
Assert.False(hasCombinedWrite);
}
private static void AssertPolicy(
IReadOnlyList<RouteEndpoint> endpoints,
string routePattern,
string method,
string expectedPolicy)
{
static string NormalizePattern(string value) =>
string.IsNullOrWhiteSpace(value) ? "/" : value.TrimEnd('/');
var expectedPattern = NormalizePattern(routePattern);
var endpoint = endpoints.Single(candidate =>
string.Equals(
NormalizePattern(candidate.RoutePattern.RawText ?? string.Empty),
expectedPattern,
StringComparison.Ordinal)
&& candidate.Metadata
.GetMetadata<HttpMethodMetadata>()?
.HttpMethods
.Contains(method, StringComparer.OrdinalIgnoreCase) == true);
var policies = endpoint.Metadata
.GetOrderedMetadata<IAuthorizeData>()
.Select(metadata => metadata.Policy)
.Where(static policy => !string.IsNullOrWhiteSpace(policy))
.ToArray();
Assert.Contains(expectedPolicy, 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}-api",
ImageDigest: "sha256:1111111111111111111111111111111111111111111111111111111111111111",
DeployOrder: 10,
MetadataJson: "{\"runtime\":\"docker\"}"),
new ReleaseControlBundleComponentInput(
ComponentVersionId: $"{slug}@1.0.1",
ComponentName: $"{slug}-worker",
ImageDigest: "sha256:2222222222222222222222222222222222222222222222222222222222222222",
DeployOrder: 20,
MetadataJson: "{\"runtime\":\"compose\"}")
]),
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", "security-v2-tests");
return client;
}
private sealed record SeededRelease(
ReleaseControlBundleDetail Bundle,
ReleaseControlBundleVersionDetail Version,
ReleaseControlBundleMaterializationRun Run);
}

View File

@@ -7,6 +7,12 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| --- | --- | --- |
| PACK-ADM-01-T | DONE | Added/verified `PackAdapterEndpointsTests` coverage for `/api/v1/administration/{summary,identity-access,tenant-branding,notifications,usage-limits,policy-governance,trust-signing,system}` and deterministic alias ordering assertions. |
| PACK-ADM-02-T | DONE | Added `AdministrationTrustSigningMutationEndpointsTests` covering trust-owner key/issuer/certificate/transparency lifecycle plus route metadata policy bindings for `platform.trust.read`, `platform.trust.write`, and `platform.trust.admin`. |
| B22-01-T | DONE | Sprint `docs/implplan/SPRINT_20260220_018_Platform_pack22_backend_contracts_and_migrations.md`: added `ContextEndpointsTests` + `ContextMigrationScriptTests` for `/api/v2/context/*` deterministic ordering, preference round-trip behavior, and migration `047` coverage. |
| B22-02-T | DONE | Sprint `docs/implplan/SPRINT_20260220_018_Platform_pack22_backend_contracts_and_migrations.md`: added `ReleaseReadModelEndpointsTests` + `ReleaseReadModelMigrationScriptTests` for `/api/v2/releases{,/activity,/approvals,/{releaseId}}` deterministic projection behavior and migration `048` coverage. |
| B22-03-T | DONE | Sprint `docs/implplan/SPRINT_20260220_018_Platform_pack22_backend_contracts_and_migrations.md`: added `TopologyReadModelEndpointsTests` + `TopologyInventoryMigrationScriptTests` for `/api/v2/topology/*` deterministic ordering/filter behavior and migration `049` coverage. |
| B22-04-T | DONE | Sprint `docs/implplan/SPRINT_20260220_018_Platform_pack22_backend_contracts_and_migrations.md`: added `SecurityReadModelEndpointsTests` + `SecurityDispositionMigrationScriptTests` for `/api/v2/security/{findings,disposition,sbom-explorer}` deterministic behavior, policy metadata, write-boundary checks, and migration `050` coverage. |
| B22-05-T | DONE | Sprint `docs/implplan/SPRINT_20260220_018_Platform_pack22_backend_contracts_and_migrations.md`: added `IntegrationsReadModelEndpointsTests` + `IntegrationSourceHealthMigrationScriptTests` for `/api/v2/integrations/{feeds,vex-sources}` deterministic behavior, policy metadata, consumer compatibility, and migration `051` coverage. |
| B22-06-T | DONE | Sprint `docs/implplan/SPRINT_20260220_018_Platform_pack22_backend_contracts_and_migrations.md`: added compatibility+telemetry contract tests covering both `/api/v1/*` aliases and `/api/v2/*` canonical routes for critical Pack 22 surfaces. |
| AUDIT-0762-M | DONE | Revalidated 2026-01-07 (test project). |
| AUDIT-0762-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0762-A | DONE | Waived (test project; revalidated 2026-01-07). |

View File

@@ -0,0 +1,75 @@
using StellaOps.TestKit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class TopologyInventoryMigrationScriptTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration049_DefinesTopologyInventoryProjectionObjects()
{
var scriptPath = GetMigrationPath("049_TopologyInventory.sql");
var sql = File.ReadAllText(scriptPath);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.topology_region_inventory", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.topology_environment_inventory", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.topology_target_inventory", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.topology_host_inventory", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.topology_agent_inventory", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.topology_promotion_path_inventory", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.topology_workflow_inventory", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.topology_gate_profile_inventory", sql, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS release.topology_sync_watermarks", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (target_type IN ('docker_host', 'compose_host', 'ecs_service', 'nomad_job', 'ssh_host'))", sql, StringComparison.Ordinal);
Assert.Contains("CHECK (path_status IN ('idle', 'pending', 'running', 'failed', 'succeeded'))", sql, StringComparison.Ordinal);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration049_IsPresentInReleaseMigrationSequence()
{
var migrationsDir = GetMigrationsDirectory();
var migrationNames = Directory.GetFiles(migrationsDir, "*.sql")
.Select(Path.GetFileName)
.Where(static name => name is not null)
.Select(static name => name!)
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
var index048 = Array.IndexOf(migrationNames, "048_ReleaseReadModels.sql");
var index049 = Array.IndexOf(migrationNames, "049_TopologyInventory.sql");
Assert.True(index048 >= 0, "Expected migration 048 to exist.");
Assert.True(index049 > index048, "Expected migration 049 to appear after migration 048.");
}
private static string GetMigrationPath(string fileName)
{
return Path.Combine(GetMigrationsDirectory(), fileName);
}
private static string GetMigrationsDirectory()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
var candidate = Path.Combine(
current.FullName,
"src",
"Platform",
"__Libraries",
"StellaOps.Platform.Database",
"Migrations",
"Release");
if (Directory.Exists(candidate))
{
return candidate;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Could not locate Platform release migrations directory.");
}
}

View File

@@ -0,0 +1,257 @@
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 TopologyReadModelEndpointsTests : IClassFixture<PlatformWebApplicationFactory>
{
private readonly PlatformWebApplicationFactory _factory;
public TopologyReadModelEndpointsTests(PlatformWebApplicationFactory factory)
{
_factory = factory;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TopologyEndpoints_ReturnDeterministicInventoryAndSupportFilters()
{
var tenantId = Guid.NewGuid().ToString("D");
using var client = CreateTenantClient(tenantId);
await SeedReleaseAsync(client, "orders-release", "Orders Release", "us-prod", "promotion");
await SeedReleaseAsync(client, "billing-release", "Billing Release", "eu-prod", "promotion");
var regions = await client.GetFromJsonAsync<PlatformListResponse<TopologyRegionProjection>>(
"/api/v2/topology/regions?limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(regions);
Assert.Equal(new[] { "us-east", "eu-west", "apac" }, regions!.Items.Select(item => item.RegionId).ToArray());
var environments = await client.GetFromJsonAsync<PlatformListResponse<TopologyEnvironmentProjection>>(
"/api/v2/topology/environments?region=us-east,eu-west&limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(environments);
Assert.Equal(
new[] { "us-prod", "us-uat", "eu-prod", "eu-stage" },
environments!.Items.Select(item => item.EnvironmentId).ToArray());
var targetsFirst = await client.GetFromJsonAsync<PlatformListResponse<TopologyTargetProjection>>(
"/api/v2/topology/targets?limit=20&offset=0",
TestContext.Current.CancellationToken);
var targetsSecond = await client.GetFromJsonAsync<PlatformListResponse<TopologyTargetProjection>>(
"/api/v2/topology/targets?limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(targetsFirst);
Assert.NotNull(targetsSecond);
Assert.NotEmpty(targetsFirst!.Items);
Assert.Equal(
targetsFirst.Items.Select(item => item.TargetId).ToArray(),
targetsSecond!.Items.Select(item => item.TargetId).ToArray());
var hostsFirst = await client.GetFromJsonAsync<PlatformListResponse<TopologyHostProjection>>(
"/api/v2/topology/hosts?limit=20&offset=0",
TestContext.Current.CancellationToken);
var hostsSecond = await client.GetFromJsonAsync<PlatformListResponse<TopologyHostProjection>>(
"/api/v2/topology/hosts?limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(hostsFirst);
Assert.NotNull(hostsSecond);
Assert.NotEmpty(hostsFirst!.Items);
Assert.Equal(
hostsFirst.Items.Select(item => item.HostId).ToArray(),
hostsSecond!.Items.Select(item => item.HostId).ToArray());
var agentsFirst = await client.GetFromJsonAsync<PlatformListResponse<TopologyAgentProjection>>(
"/api/v2/topology/agents?limit=20&offset=0",
TestContext.Current.CancellationToken);
var agentsSecond = await client.GetFromJsonAsync<PlatformListResponse<TopologyAgentProjection>>(
"/api/v2/topology/agents?limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(agentsFirst);
Assert.NotNull(agentsSecond);
Assert.NotEmpty(agentsFirst!.Items);
Assert.Equal(
agentsFirst.Items.Select(item => item.AgentId).ToArray(),
agentsSecond!.Items.Select(item => item.AgentId).ToArray());
var paths = await client.GetFromJsonAsync<PlatformListResponse<TopologyPromotionPathProjection>>(
"/api/v2/topology/promotion-paths?limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(paths);
Assert.NotEmpty(paths!.Items);
var workflows = await client.GetFromJsonAsync<PlatformListResponse<TopologyWorkflowProjection>>(
"/api/v2/topology/workflows?limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(workflows);
Assert.NotEmpty(workflows!.Items);
var profiles = await client.GetFromJsonAsync<PlatformListResponse<TopologyGateProfileProjection>>(
"/api/v2/topology/gate-profiles?limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(profiles);
Assert.NotEmpty(profiles!.Items);
var usTargets = await client.GetFromJsonAsync<PlatformListResponse<TopologyTargetProjection>>(
"/api/v2/topology/targets?region=us-east&limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(usTargets);
Assert.NotEmpty(usTargets!.Items);
Assert.All(usTargets.Items, item => Assert.Equal("us-east", item.RegionId));
var euHosts = await client.GetFromJsonAsync<PlatformListResponse<TopologyHostProjection>>(
"/api/v2/topology/hosts?environment=eu-prod&limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(euHosts);
Assert.NotEmpty(euHosts!.Items);
Assert.All(euHosts.Items, item => Assert.Equal("eu-prod", item.EnvironmentId));
var usPaths = await client.GetFromJsonAsync<PlatformListResponse<TopologyPromotionPathProjection>>(
"/api/v2/topology/promotion-paths?environment=us-prod&limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(usPaths);
Assert.NotEmpty(usPaths!.Items);
Assert.All(usPaths.Items, item =>
Assert.True(
string.Equals(item.SourceEnvironmentId, "us-prod", StringComparison.Ordinal)
|| string.Equals(item.TargetEnvironmentId, "us-prod", StringComparison.Ordinal)));
var euWorkflows = await client.GetFromJsonAsync<PlatformListResponse<TopologyWorkflowProjection>>(
"/api/v2/topology/workflows?environment=eu-prod&limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(euWorkflows);
Assert.NotEmpty(euWorkflows!.Items);
Assert.All(euWorkflows.Items, item => Assert.Equal("eu-prod", item.EnvironmentId));
var euProfiles = await client.GetFromJsonAsync<PlatformListResponse<TopologyGateProfileProjection>>(
"/api/v2/topology/gate-profiles?region=eu-west&limit=20&offset=0",
TestContext.Current.CancellationToken);
Assert.NotNull(euProfiles);
Assert.NotEmpty(euProfiles!.Items);
Assert.All(euProfiles.Items, item => Assert.Equal("eu-west", item.RegionId));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TopologyEndpoints_WithoutTenantHeader_ReturnBadRequest()
{
using var client = _factory.CreateClient();
var response = await client.GetAsync("/api/v2/topology/regions", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TopologyEndpoints_RequireExpectedPolicy()
{
var endpoints = _factory.Services
.GetRequiredService<EndpointDataSource>()
.Endpoints
.OfType<RouteEndpoint>()
.ToArray();
AssertPolicy(endpoints, "/api/v2/topology/regions", "GET", PlatformPolicies.TopologyRead);
AssertPolicy(endpoints, "/api/v2/topology/environments", "GET", PlatformPolicies.TopologyRead);
AssertPolicy(endpoints, "/api/v2/topology/targets", "GET", PlatformPolicies.TopologyRead);
AssertPolicy(endpoints, "/api/v2/topology/hosts", "GET", PlatformPolicies.TopologyRead);
AssertPolicy(endpoints, "/api/v2/topology/agents", "GET", PlatformPolicies.TopologyRead);
AssertPolicy(endpoints, "/api/v2/topology/promotion-paths", "GET", PlatformPolicies.TopologyRead);
AssertPolicy(endpoints, "/api/v2/topology/workflows", "GET", PlatformPolicies.TopologyRead);
AssertPolicy(endpoints, "/api/v2/topology/gate-profiles", "GET", PlatformPolicies.TopologyRead);
}
private static void AssertPolicy(
IReadOnlyList<RouteEndpoint> endpoints,
string routePattern,
string method,
string expectedPolicy)
{
static string NormalizePattern(string value) =>
string.IsNullOrWhiteSpace(value) ? "/" : value.TrimEnd('/');
var expectedPattern = NormalizePattern(routePattern);
var endpoint = endpoints.Single(candidate =>
string.Equals(
NormalizePattern(candidate.RoutePattern.RawText ?? string.Empty),
expectedPattern,
StringComparison.Ordinal)
&& candidate.Metadata
.GetMetadata<HttpMethodMetadata>()?
.HttpMethods
.Contains(method, StringComparer.OrdinalIgnoreCase) == true);
var policies = endpoint.Metadata
.GetOrderedMetadata<IAuthorizeData>()
.Select(metadata => metadata.Policy)
.Where(static policy => !string.IsNullOrWhiteSpace(policy))
.ToArray();
Assert.Contains(expectedPolicy, policies);
}
private static async Task 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}-api",
ImageDigest: "sha256:1111111111111111111111111111111111111111111111111111111111111111",
DeployOrder: 10,
MetadataJson: "{\"runtime\":\"docker\"}"),
new ReleaseControlBundleComponentInput(
ComponentVersionId: $"{slug}@1.0.1",
ComponentName: $"{slug}-worker",
ImageDigest: "sha256:2222222222222222222222222222222222222222222222222222222222222222",
DeployOrder: 20,
MetadataJson: "{\"runtime\":\"compose\"}")
]),
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();
}
private HttpClient CreateTenantClient(string tenantId)
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId);
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "topology-v2-tests");
return client;
}
}