consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -0,0 +1,162 @@
using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Testing;
using StellaOps.Remediation.WebService.Contracts;
namespace StellaOps.Remediation.WebService.Tests;
[Collection(RemediationEnvironmentCollection.Name)]
public sealed class RemediationSourceEndpointsTests
{
[Fact]
public async Task SourcesEndpoints_PostGetAndList_RoundTripWithPersistedData()
{
using var environment = RemediationEnvironmentScope.InMemoryTestingProfile();
await using var factory = CreateFactory();
using var client = CreateTenantClient(factory, "tenant-a");
var initialList = await client.GetFromJsonAsync<MarketplaceSourceListResponse>("/api/v1/remediation/sources");
Assert.NotNull(initialList);
Assert.Empty(initialList!.Items);
var request = new UpsertMarketplaceSourceRequest(
Key: "Vendor-Feed",
Name: "Vendor Feed",
Url: "https://vendor.example.com/feed",
SourceType: "vendor",
Enabled: true,
TrustScore: 0.91,
LastSyncAt: new DateTimeOffset(2026, 03, 04, 10, 0, 0, TimeSpan.Zero));
var postResponse = await client.PostAsJsonAsync("/api/v1/remediation/sources", request);
Assert.NotEqual(HttpStatusCode.NotImplemented, postResponse.StatusCode);
Assert.Equal(HttpStatusCode.OK, postResponse.StatusCode);
var posted = await postResponse.Content.ReadFromJsonAsync<MarketplaceSourceSummary>();
Assert.NotNull(posted);
Assert.Equal("vendor-feed", posted!.Key);
Assert.Equal("vendor", posted.SourceType);
var byKey = await client.GetFromJsonAsync<MarketplaceSourceSummary>("/api/v1/remediation/sources/vendor-feed");
Assert.NotNull(byKey);
Assert.Equal("Vendor Feed", byKey!.Name);
Assert.Equal(0.91, byKey.TrustScore);
var listed = await client.GetFromJsonAsync<MarketplaceSourceListResponse>("/api/v1/remediation/sources");
Assert.NotNull(listed);
Assert.Single(listed!.Items);
Assert.Equal("vendor-feed", listed.Items[0].Key);
}
[Fact]
public async Task SourcesEndpoints_EnforceTenantIsolation()
{
using var environment = RemediationEnvironmentScope.InMemoryTestingProfile();
await using var factory = CreateFactory();
using var tenantAClient = CreateTenantClient(factory, "tenant-a");
using var tenantBClient = CreateTenantClient(factory, "tenant-b");
var request = new UpsertMarketplaceSourceRequest(
Key: "shared-key",
Name: "Tenant A Source",
Url: "https://a.example.com",
SourceType: "community",
Enabled: true,
TrustScore: 0.5,
LastSyncAt: null);
var post = await tenantAClient.PostAsJsonAsync("/api/v1/remediation/sources", request);
post.EnsureSuccessStatusCode();
var tenantAList = await tenantAClient.GetFromJsonAsync<MarketplaceSourceListResponse>("/api/v1/remediation/sources");
var tenantBList = await tenantBClient.GetFromJsonAsync<MarketplaceSourceListResponse>("/api/v1/remediation/sources");
Assert.NotNull(tenantAList);
Assert.NotNull(tenantBList);
Assert.Single(tenantAList!.Items);
Assert.Empty(tenantBList!.Items);
var tenantBGet = await tenantBClient.GetAsync("/api/v1/remediation/sources/shared-key");
Assert.Equal(HttpStatusCode.NotFound, tenantBGet.StatusCode);
}
[Fact]
public async Task SourcesEndpoints_ListOrderingAndUpsertAreDeterministic()
{
using var environment = RemediationEnvironmentScope.InMemoryTestingProfile();
await using var factory = CreateFactory();
using var client = CreateTenantClient(factory, "tenant-ordering");
await UpsertAsync(client, "zeta", "Zeta", 0.2);
await UpsertAsync(client, "alpha", "Alpha", 0.3);
await UpsertAsync(client, "beta", "Beta", 0.4);
await UpsertAsync(client, "alpha", "Alpha Updated", 0.9);
var listed = await client.GetFromJsonAsync<MarketplaceSourceListResponse>("/api/v1/remediation/sources");
Assert.NotNull(listed);
Assert.Equal(3, listed!.Count);
Assert.Equal(["alpha", "beta", "zeta"], listed.Items.Select(static item => item.Key).ToArray());
Assert.Equal("Alpha Updated", listed.Items[0].Name);
Assert.Equal(0.9, listed.Items[0].TrustScore);
}
[Fact]
public async Task SourcesEndpoints_ReturnDeterministicValidationProblems()
{
using var environment = RemediationEnvironmentScope.InMemoryTestingProfile();
await using var factory = CreateFactory();
using var client = CreateTenantClient(factory, "tenant-validation");
var request = new UpsertMarketplaceSourceRequest(
Key: "Invalid Key",
Name: "",
Url: "not-a-url",
SourceType: "unknown",
Enabled: true,
TrustScore: 1.5,
LastSyncAt: null);
var response = await client.PostAsJsonAsync("/api/v1/remediation/sources", request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var problem = await response.Content.ReadFromJsonAsync<HttpValidationProblemDetails>();
Assert.NotNull(problem);
Assert.Equal(StatusCodes.Status400BadRequest, problem!.Status);
Assert.Contains("key", problem.Errors.Keys);
Assert.Contains("name", problem.Errors.Keys);
Assert.Contains("sourceType", problem.Errors.Keys);
Assert.Contains("trustScore", problem.Errors.Keys);
Assert.Contains("url", problem.Errors.Keys);
}
private static WebApplicationFactory<Program> CreateFactory()
{
return new WebApplicationFactory<Program>();
}
private static HttpClient CreateTenantClient(WebApplicationFactory<Program> factory, string tenantId)
{
var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId);
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "remediation-source-tests");
return client;
}
private static async Task UpsertAsync(HttpClient client, string key, string name, double trustScore)
{
var response = await client.PostAsJsonAsync(
"/api/v1/remediation/sources",
new UpsertMarketplaceSourceRequest(
Key: key,
Name: name,
Url: $"https://example.com/{key}",
SourceType: "community",
Enabled: true,
TrustScore: trustScore,
LastSyncAt: null));
response.EnsureSuccessStatusCode();
}
}

View File

@@ -0,0 +1,68 @@
using System.Net;
using Microsoft.AspNetCore.Mvc.Testing;
namespace StellaOps.Remediation.WebService.Tests;
[Collection(RemediationEnvironmentCollection.Name)]
public sealed class RemediationStartupContractTests
{
[Fact]
public void Startup_FailsWithoutPostgresConnectionString_WhenPostgresDriverSelected()
{
using var environment = RemediationEnvironmentScope.PostgresWithoutConnection("Production");
using var factory = new WebApplicationFactory<Program>();
var exception = Assert.ThrowsAny<Exception>(() =>
{
using var client = factory.CreateClient();
});
Assert.Contains(
"Remediation requires PostgreSQL connection settings when Storage:Driver=postgres.",
exception.ToString(),
StringComparison.Ordinal);
}
[Fact]
public void Startup_RejectsInMemoryDriver_OutsideTestProfiles()
{
using var environment = RemediationEnvironmentScope.InMemoryDriver("Production");
using var factory = new WebApplicationFactory<Program>();
var exception = Assert.ThrowsAny<Exception>(() =>
{
using var client = factory.CreateClient();
});
Assert.Contains(
"Remediation in-memory storage driver is restricted to Test/Testing environments.",
exception.ToString(),
StringComparison.Ordinal);
}
[Fact]
public async Task Startup_AllowsInMemoryDriver_InTestingProfile()
{
using var environment = RemediationEnvironmentScope.InMemoryTestingProfile();
using var factory = new WebApplicationFactory<Program>();
using var client = factory.CreateClient();
var response = await client.GetAsync("/healthz");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task Startup_AllowsPostgresDriver_WhenConnectionStringIsConfigured()
{
using var environment = RemediationEnvironmentScope.PostgresWithConnection(
"Production",
"Host=localhost;Database=stellaops_remediation;Username=stellaops;Password=stellaops");
using var factory = new WebApplicationFactory<Program>();
using var client = factory.CreateClient();
var response = await client.GetAsync("/healthz");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Remediation.WebService\StellaOps.Remediation.WebService.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,88 @@
namespace StellaOps.Remediation.WebService.Tests;
[CollectionDefinition(Name, DisableParallelization = true)]
public sealed class RemediationEnvironmentCollection
{
public const string Name = "RemediationEnvironment";
}
internal sealed class RemediationEnvironmentScope : IDisposable
{
private static readonly string[] ManagedKeys =
[
"DOTNET_ENVIRONMENT",
"ASPNETCORE_ENVIRONMENT",
"REMEDIATION__STORAGE__DRIVER",
"REMEDIATION__STORAGE__POSTGRES__CONNECTIONSTRING",
"CONNECTIONSTRINGS__DEFAULT"
];
private readonly Dictionary<string, string?> _originalValues = new(StringComparer.Ordinal);
private RemediationEnvironmentScope()
{
foreach (var key in ManagedKeys)
{
_originalValues[key] = Environment.GetEnvironmentVariable(key);
}
}
public static RemediationEnvironmentScope InMemoryTestingProfile()
{
var scope = new RemediationEnvironmentScope();
scope.Set("DOTNET_ENVIRONMENT", "Testing");
scope.Set("ASPNETCORE_ENVIRONMENT", "Testing");
scope.Set("REMEDIATION__STORAGE__DRIVER", "inmemory");
scope.Set("REMEDIATION__STORAGE__POSTGRES__CONNECTIONSTRING", null);
scope.Set("CONNECTIONSTRINGS__DEFAULT", null);
return scope;
}
public static RemediationEnvironmentScope PostgresWithoutConnection(string environmentName)
{
var scope = new RemediationEnvironmentScope();
scope.Set("DOTNET_ENVIRONMENT", environmentName);
scope.Set("ASPNETCORE_ENVIRONMENT", environmentName);
scope.Set("REMEDIATION__STORAGE__DRIVER", "postgres");
scope.Set("REMEDIATION__STORAGE__POSTGRES__CONNECTIONSTRING", null);
scope.Set("CONNECTIONSTRINGS__DEFAULT", null);
return scope;
}
public static RemediationEnvironmentScope InMemoryDriver(string environmentName)
{
var scope = new RemediationEnvironmentScope();
scope.Set("DOTNET_ENVIRONMENT", environmentName);
scope.Set("ASPNETCORE_ENVIRONMENT", environmentName);
scope.Set("REMEDIATION__STORAGE__DRIVER", "inmemory");
scope.Set("REMEDIATION__STORAGE__POSTGRES__CONNECTIONSTRING", null);
scope.Set("CONNECTIONSTRINGS__DEFAULT", null);
return scope;
}
public static RemediationEnvironmentScope PostgresWithConnection(
string environmentName,
string connectionString)
{
var scope = new RemediationEnvironmentScope();
scope.Set("DOTNET_ENVIRONMENT", environmentName);
scope.Set("ASPNETCORE_ENVIRONMENT", environmentName);
scope.Set("REMEDIATION__STORAGE__DRIVER", "postgres");
scope.Set("REMEDIATION__STORAGE__POSTGRES__CONNECTIONSTRING", connectionString);
scope.Set("CONNECTIONSTRINGS__DEFAULT", connectionString);
return scope;
}
public void Dispose()
{
foreach (var entry in _originalValues)
{
Environment.SetEnvironmentVariable(entry.Key, entry.Value);
}
}
private void Set(string key, string? value)
{
Environment.SetEnvironmentVariable(key, value);
}
}