consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user