stabilize tests
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
|
||||
using StellaOps.Notify.Engine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using StellaOps.Notify.Engine;
|
||||
|
||||
namespace StellaOps.Notify.WebService.Contracts;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
using StellaOps.Notify.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notify.WebService.Contracts;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
using StellaOps.Notify.Models;
|
||||
using System;
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notify.WebService.Services;
|
||||
|
||||
|
||||
@@ -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. |
|
||||
|
||||
Reference in New Issue
Block a user