Add unit tests and implementations for MongoDB index models and OpenAPI metadata

- Implemented `MongoIndexModelTests` to verify index models for various stores.
- Created `OpenApiMetadataFactory` with methods to generate OpenAPI metadata.
- Added tests for `OpenApiMetadataFactory` to ensure expected defaults and URL overrides.
- Introduced `ObserverSurfaceSecrets` and `WebhookSurfaceSecrets` for managing secrets.
- Developed `RuntimeSurfaceFsClient` and `WebhookSurfaceFsClient` for manifest retrieval.
- Added dependency injection tests for `SurfaceEnvironmentRegistration` in both Observer and Webhook contexts.
- Implemented tests for secret resolution in `ObserverSurfaceSecretsTests` and `WebhookSurfaceSecretsTests`.
- Created `EnsureLinkNotMergeCollectionsMigrationTests` to validate MongoDB migration logic.
- Added project files for MongoDB tests and NuGet package mirroring.
This commit is contained in:
master
2025-11-17 21:21:56 +02:00
parent d3128aec24
commit 9075bad2d9
146 changed files with 152183 additions and 82 deletions

View File

@@ -1,24 +1,141 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using StellaOps.Notify.Storage.Mongo;
using StellaOps.Notifier.WebService.Setup;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables(prefix: "NOTIFIER_");
var mongoSection = builder.Configuration.GetSection("notifier:storage:mongo");
builder.Services.AddNotifyMongoStorage(mongoSection);
builder.Services.AddHealthChecks();
builder.Services.AddHostedService<MongoInitializationHostedService>();
var app = builder.Build();
app.MapHealthChecks("/healthz");
app.Run();
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using StellaOps.Notifier.WebService.Contracts;
using StellaOps.Notifier.WebService.Setup;
using StellaOps.Notify.Storage.Mongo;
using StellaOps.Notify.Storage.Mongo.Documents;
using StellaOps.Notify.Storage.Mongo.Repositories;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables(prefix: "NOTIFIER_");
var mongoSection = builder.Configuration.GetSection("notifier:storage:mongo");
builder.Services.AddNotifyMongoStorage(mongoSection);
builder.Services.AddSingleton<OpenApiDocumentCache>();
builder.Services.AddHealthChecks();
builder.Services.AddHostedService<MongoInitializationHostedService>();
var app = builder.Build();
app.MapHealthChecks("/healthz");
// Deprecation headers for retiring v1 APIs (RFC 8594 / IETF Sunset)
app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/api/v1", StringComparison.OrdinalIgnoreCase))
{
context.Response.Headers["Deprecation"] = "true";
context.Response.Headers["Sunset"] = "Tue, 31 Mar 2026 00:00:00 GMT";
context.Response.Headers["Link"] =
"<https://docs.stellaops.example.com/notify/deprecations>; rel=\"deprecation\"; type=\"text/html\"";
}
await next().ConfigureAwait(false);
});
app.MapPost("/api/v1/notify/pack-approvals", async (
HttpContext context,
PackApprovalRequest request,
INotifyLockRepository locks,
INotifyPackApprovalRepository packApprovals,
INotifyAuditRepository audit,
TimeProvider timeProvider) =>
{
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
}
var idempotencyKey = context.Request.Headers["Idempotency-Key"].ToString();
if (string.IsNullOrWhiteSpace(idempotencyKey))
{
return Results.BadRequest(Error("idempotency_key_missing", "Idempotency-Key header is required.", context));
}
if (request.EventId == Guid.Empty || string.IsNullOrWhiteSpace(request.PackId) ||
string.IsNullOrWhiteSpace(request.Kind) || string.IsNullOrWhiteSpace(request.Decision) ||
string.IsNullOrWhiteSpace(request.Actor))
{
return Results.BadRequest(Error("invalid_request", "eventId, packId, kind, decision, actor are required.", context));
}
var lockKey = $"pack-approvals|{tenantId}|{idempotencyKey}";
var ttl = TimeSpan.FromMinutes(15);
var reserved = await locks.TryAcquireAsync(tenantId, lockKey, "pack-approvals", ttl, context.RequestAborted)
.ConfigureAwait(false);
if (!reserved)
{
return Results.StatusCode(StatusCodes.Status200OK);
}
var document = new PackApprovalDocument
{
TenantId = tenantId,
EventId = request.EventId,
PackId = request.PackId,
Kind = request.Kind,
Decision = request.Decision,
Actor = request.Actor,
IssuedAt = request.IssuedAt,
PolicyId = request.Policy?.Id,
PolicyVersion = request.Policy?.Version,
ResumeToken = request.ResumeToken,
Summary = request.Summary,
Labels = request.Labels,
CreatedAt = timeProvider.GetUtcNow()
};
await packApprovals.UpsertAsync(document, context.RequestAborted).ConfigureAwait(false);
var auditEntry = new NotifyAuditEntryDocument
{
TenantId = tenantId,
Actor = request.Actor,
Action = "pack.approval.ingested",
EntityId = request.PackId,
EntityType = "pack-approval",
Timestamp = timeProvider.GetUtcNow(),
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(JsonSerializer.Serialize(request))
};
await audit.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(request.ResumeToken))
{
context.Response.Headers["X-Resume-After"] = request.ResumeToken;
}
return Results.Accepted();
});
app.MapGet("/.well-known/openapi", (HttpContext context, OpenApiDocumentCache cache) =>
{
context.Response.Headers.CacheControl = "public, max-age=300";
context.Response.Headers["X-OpenAPI-Scope"] = "notify";
context.Response.Headers.ETag = $"\"{cache.Sha256}\"";
return Results.Content(cache.Document, "application/yaml");
});
app.Run();
public partial class Program;
static object Error(string code, string message, HttpContext context) => new
{
error = new
{
code,
message,
traceId = context.TraceIdentifier
}
};