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:
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user