stabilize tests

This commit is contained in:
master
2026-02-01 21:37:40 +02:00
parent 55744f6a39
commit 5d5e80b2e4
6435 changed files with 33984 additions and 13802 deletions

View File

@@ -1,6 +1,7 @@
using StellaOps.Notify.Engine;
using System;
using System.Collections.Generic;
using StellaOps.Notify.Engine;
namespace StellaOps.Notify.WebService.Contracts;

View File

@@ -1,6 +1,7 @@
using StellaOps.Notify.Models;
using System;
using System.Collections.Generic;
using StellaOps.Notify.Models;
namespace StellaOps.Notify.WebService.Contracts;

View File

@@ -1,7 +1,8 @@
using Microsoft.Extensions.Configuration;
using System.IO;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

View File

@@ -1,7 +1,8 @@
using System;
using System.IO;
using StellaOps.Notify.WebService.Options;
using StellaOps.Plugin.Hosting;
using System;
using System.IO;
namespace StellaOps.Notify.WebService.Hosting;

View File

@@ -1,7 +1,8 @@
using System;
using System.Threading;
using Microsoft.Extensions.Logging;
using StellaOps.Plugin.Hosting;
using System;
using System.Threading;
namespace StellaOps.Notify.WebService.Plugins;

View File

@@ -1,45 +1,46 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Globalization;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Tokens;
using Serilog;
using Serilog.Events;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Configuration;
using System.Collections.Immutable;
using StellaOps.Notify.Models;
using StellaOps.Notify.Persistence.Extensions;
using StellaOps.Notify.Persistence.Postgres;
using StellaOps.Notify.Persistence.Postgres.Models;
using StellaOps.Notify.Persistence.Postgres.Repositories;
using StellaOps.Notify.WebService.Contracts;
using StellaOps.Notify.WebService.Diagnostics;
using StellaOps.Notify.WebService.Extensions;
using StellaOps.Notify.WebService.Hosting;
using StellaOps.Notify.WebService.Internal;
using StellaOps.Notify.WebService.Options;
using StellaOps.Notify.WebService.Plugins;
using StellaOps.Notify.WebService.Security;
using StellaOps.Notify.WebService.Services;
using StellaOps.Notify.WebService.Internal;
using StellaOps.Plugin.DependencyInjection;
using StellaOps.Notify.WebService.Contracts;
using StellaOps.Router.AspNet;
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
@@ -250,8 +251,6 @@ static void ConfigureAuthentication(WebApplicationBuilder builder, NotifyWebServ
static void ConfigureRateLimiting(WebApplicationBuilder builder, NotifyWebServiceOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var tenantHeader = options.Api.TenantHeader;
var limits = options.Api.RateLimits;
builder.Services.AddRateLimiter(rateLimiterOptions =>
{
@@ -262,19 +261,22 @@ static void ConfigureRateLimiting(WebApplicationBuilder builder, NotifyWebServic
return ValueTask.CompletedTask;
};
ConfigurePolicy(rateLimiterOptions, NotifyRateLimitPolicies.DeliveryHistory, limits.DeliveryHistory, tenantHeader, "deliveries");
ConfigurePolicy(rateLimiterOptions, NotifyRateLimitPolicies.TestSend, limits.TestSend, tenantHeader, "channel-test");
ConfigurePolicy(rateLimiterOptions, NotifyRateLimitPolicies.DeliveryHistory, o => o.Api.RateLimits.DeliveryHistory, "deliveries");
ConfigurePolicy(rateLimiterOptions, NotifyRateLimitPolicies.TestSend, o => o.Api.RateLimits.TestSend, "channel-test");
});
static void ConfigurePolicy(
RateLimiterOptions rateLimiterOptions,
string policyName,
NotifyWebServiceOptions.RateLimitPolicyOptions policy,
string tenantHeader,
Func<NotifyWebServiceOptions, NotifyWebServiceOptions.RateLimitPolicyOptions> policySelector,
string prefix)
{
rateLimiterOptions.AddPolicy(policyName, httpContext =>
{
var opts = httpContext.RequestServices.GetRequiredService<IOptions<NotifyWebServiceOptions>>().Value;
var policy = policySelector(opts);
var tenantHeader = opts.Api.TenantHeader;
if (policy is null || !policy.Enabled)
{
return RateLimitPartition.GetNoLimiter("notify-disabled");
@@ -439,7 +441,16 @@ static void ConfigureEndpoints(WebApplication app)
return Results.BadRequest(new { error = "Request body is required." });
}
var ruleModel = service.UpgradeRule(body);
NotifyRule ruleModel;
try
{
ruleModel = service.UpgradeRule(body);
}
catch (Exception ex) when (ex is JsonException or InvalidOperationException or KeyNotFoundException or ArgumentException or FormatException)
{
return Results.BadRequest(new { error = $"Invalid rule payload: {ex.Message}" });
}
if (!string.Equals(ruleModel.TenantId, tenant, StringComparison.Ordinal))
{
return Results.BadRequest(new { error = "Tenant mismatch between header and payload." });
@@ -477,8 +488,8 @@ static void ConfigureEndpoints(WebApplication app)
return Results.BadRequest(new { error = "ruleId must be a GUID." });
}
await repository.DeleteAsync(tenant, ruleGuid, cancellationToken).ConfigureAwait(false);
return Results.NoContent();
var deleted = await repository.DeleteAsync(tenant, ruleGuid, cancellationToken).ConfigureAwait(false);
return deleted ? Results.NoContent() : Results.NotFound();
})
.RequireAuthorization(NotifyPolicies.Operator);
@@ -523,7 +534,16 @@ static void ConfigureEndpoints(WebApplication app)
return Results.BadRequest(new { error = "Request body is required." });
}
var channelModel = service.UpgradeChannel(body);
NotifyChannel channelModel;
try
{
channelModel = service.UpgradeChannel(body);
}
catch (Exception ex) when (ex is System.Text.Json.JsonException or InvalidOperationException or KeyNotFoundException or ArgumentException or FormatException or NotSupportedException)
{
return Results.BadRequest(new { error = $"Invalid channel payload: {ex.Message}" });
}
if (!string.Equals(channelModel.TenantId, tenant, StringComparison.Ordinal))
{
return Results.BadRequest(new { error = "Tenant mismatch between header and payload." });
@@ -549,6 +569,55 @@ static void ConfigureEndpoints(WebApplication app)
})
.RequireAuthorization(NotifyPolicies.Operator);
apiGroup.MapPost("/channels/{channelId}/test", async (
string channelId,
ChannelTestSendRequest? request,
IChannelRepository repository,
INotifyChannelTestService testService,
HttpContext context,
CancellationToken cancellationToken) =>
{
if (!TryResolveTenant(context, tenantHeader, out var tenant, out var error))
{
return error!;
}
if (request is null)
{
return Results.BadRequest(new { error = "Request body is required." });
}
if (!TryParseGuid(channelId, out var channelGuid))
{
return Results.BadRequest(new { error = "channelId must be a GUID." });
}
var channelEntity = await repository.GetByIdAsync(tenant, channelGuid, cancellationToken)
.ConfigureAwait(false);
if (channelEntity is null)
{
return Results.NotFound();
}
var channel = ToNotifyChannel(channelEntity);
try
{
var response = await testService.SendAsync(
tenant,
channel,
request,
context.TraceIdentifier,
cancellationToken).ConfigureAwait(false);
return Results.Accepted(value: response);
}
catch (ChannelTestSendValidationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
})
.RequireAuthorization(NotifyPolicies.Operator)
.RequireRateLimiting(NotifyRateLimitPolicies.TestSend);
apiGroup.MapDelete("/channels/{channelId}", async (string channelId, IChannelRepository repository, HttpContext context, CancellationToken cancellationToken) =>
{
if (!TryResolveTenant(context, tenantHeader, out var tenant, out var error))

View File

@@ -1,9 +1,10 @@
using System;
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Security.Claims;
using System.Text.Encodings.Web;
namespace StellaOps.Notify.WebService.Security;

View File

@@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Notify.Engine;
using StellaOps.Notify.Models;
using StellaOps.Notify.WebService.Contracts;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Notify.WebService.Services;

View File

@@ -1,10 +1,11 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Extensions.Logging;
using StellaOps.Notify.Engine;
using StellaOps.Notify.Models;
using StellaOps.Notify.WebService.Contracts;
using System;
using System.Collections.Generic;
using System.Threading;
namespace StellaOps.Notify.WebService.Services;

View File

@@ -1,6 +1,7 @@
using StellaOps.Notify.Models;
using System;
using System.Text.Json.Nodes;
using StellaOps.Notify.Models;
namespace StellaOps.Notify.WebService.Services;

View File

@@ -8,3 +8,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0416-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Notify.WebService. |
| AUDIT-0416-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Notify.WebService. |
| AUDIT-0416-A | TODO | Revalidated 2026-01-07 (open findings). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |