Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism
- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
@@ -65,6 +65,21 @@ public static class ErrorCodes
|
||||
/// <summary>Bundle not found in catalog.</summary>
|
||||
public const string BundleNotFound = "BUNDLE_NOT_FOUND";
|
||||
|
||||
/// <summary>Feed snapshot not found.</summary>
|
||||
public const string SnapshotNotFound = "SNAPSHOT_NOT_FOUND";
|
||||
|
||||
/// <summary>Invalid feed sources specified.</summary>
|
||||
public const string InvalidSources = "INVALID_SOURCES";
|
||||
|
||||
/// <summary>Uploaded file is empty.</summary>
|
||||
public const string EmptyFile = "EMPTY_FILE";
|
||||
|
||||
/// <summary>Uploaded file exceeds size limit.</summary>
|
||||
public const string FileTooLarge = "FILE_TOO_LARGE";
|
||||
|
||||
/// <summary>Feature is disabled.</summary>
|
||||
public const string FeatureDisabled = "FEATURE_DISABLED";
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// AOC (Aggregation-Only Contract) Errors
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -4,6 +4,7 @@ internal static class ProblemTypes
|
||||
{
|
||||
public const string NotFound = "https://stellaops.org/problems/not-found";
|
||||
public const string Validation = "https://stellaops.org/problems/validation";
|
||||
public const string Forbidden = "https://stellaops.org/problems/forbidden";
|
||||
public const string Conflict = "https://stellaops.org/problems/conflict";
|
||||
public const string Locked = "https://stellaops.org/problems/locked";
|
||||
public const string LeaseRejected = "https://stellaops.org/problems/lease-rejected";
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// FeedSnapshotEndpointExtensions.cs
|
||||
// Sprint: SPRINT_20251226_007_BE_determinism_gaps
|
||||
// Task: DET-GAP-03
|
||||
// Description: Feed snapshot endpoint for atomic multi-source snapshots
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Net.Mime;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.WebService.Options;
|
||||
using StellaOps.Concelier.WebService.Results;
|
||||
using StellaOps.Replay.Core.FeedSnapshot;
|
||||
using HttpResults = Microsoft.AspNetCore.Http.Results;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint extensions for feed snapshot functionality.
|
||||
/// Provides atomic multi-source snapshots with composite digest.
|
||||
/// Per DET-GAP-03 in SPRINT_20251226_007_BE_determinism_gaps.
|
||||
/// </summary>
|
||||
internal static class FeedSnapshotEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps feed snapshot endpoints to the application.
|
||||
/// </summary>
|
||||
public static void MapFeedSnapshotEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/v1/feeds/snapshot")
|
||||
.WithTags("FeedSnapshot")
|
||||
.WithOpenApi();
|
||||
|
||||
// POST /api/v1/feeds/snapshot - Create atomic snapshot
|
||||
group.MapPost("/", CreateSnapshotAsync)
|
||||
.WithName("CreateFeedSnapshot")
|
||||
.WithSummary("Create an atomic feed snapshot")
|
||||
.WithDescription("Creates an atomic snapshot of all registered feed sources with a composite digest.");
|
||||
|
||||
// GET /api/v1/feeds/snapshot - List available snapshots
|
||||
group.MapGet("/", ListSnapshotsAsync)
|
||||
.WithName("ListFeedSnapshots")
|
||||
.WithSummary("List available feed snapshots")
|
||||
.WithDescription("Returns a list of available feed snapshots with metadata.");
|
||||
|
||||
// GET /api/v1/feeds/snapshot/{snapshotId} - Get snapshot details
|
||||
group.MapGet("/{snapshotId}", GetSnapshotAsync)
|
||||
.WithName("GetFeedSnapshot")
|
||||
.WithSummary("Get feed snapshot details")
|
||||
.WithDescription("Returns detailed information about a specific feed snapshot.");
|
||||
|
||||
// GET /api/v1/feeds/snapshot/{snapshotId}/export - Export snapshot bundle
|
||||
group.MapGet("/{snapshotId}/export", ExportSnapshotAsync)
|
||||
.WithName("ExportFeedSnapshot")
|
||||
.WithSummary("Export feed snapshot bundle")
|
||||
.WithDescription("Downloads the snapshot bundle as a compressed archive for offline use.");
|
||||
|
||||
// POST /api/v1/feeds/snapshot/import - Import snapshot bundle
|
||||
group.MapPost("/import", ImportSnapshotAsync)
|
||||
.WithName("ImportFeedSnapshot")
|
||||
.WithSummary("Import feed snapshot bundle")
|
||||
.WithDescription("Imports a snapshot bundle from a compressed archive.");
|
||||
|
||||
// GET /api/v1/feeds/snapshot/{snapshotId}/validate - Validate snapshot
|
||||
group.MapGet("/{snapshotId}/validate", ValidateSnapshotAsync)
|
||||
.WithName("ValidateFeedSnapshot")
|
||||
.WithSummary("Validate feed snapshot integrity")
|
||||
.WithDescription("Validates the integrity of a feed snapshot against current feed state.");
|
||||
|
||||
// GET /api/v1/feeds/sources - List registered feed sources
|
||||
group.MapGet("/sources", ListSourcesAsync)
|
||||
.WithName("ListFeedSources")
|
||||
.WithSummary("List registered feed sources")
|
||||
.WithDescription("Returns a list of registered feed sources available for snapshots.");
|
||||
}
|
||||
|
||||
private static async Task<IResult> CreateSnapshotAsync(
|
||||
HttpContext context,
|
||||
IFeedSnapshotCoordinator coordinator,
|
||||
IOptionsMonitor<ConcelierOptions> optionsMonitor,
|
||||
[FromBody] CreateSnapshotRequest? request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var options = optionsMonitor.CurrentValue;
|
||||
|
||||
// Check if feed snapshot feature is enabled
|
||||
if (!options.FeedSnapshot.Enabled)
|
||||
{
|
||||
return ConcelierProblemResultFactory.FeatureDisabled(context, "FeedSnapshot");
|
||||
}
|
||||
|
||||
// Validate requested sources if provided
|
||||
if (request?.Sources is { Length: > 0 })
|
||||
{
|
||||
var registeredSources = coordinator.RegisteredSources;
|
||||
var invalidSources = request.Sources
|
||||
.Where(s => !registeredSources.Contains(s, StringComparer.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
if (invalidSources.Length > 0)
|
||||
{
|
||||
return ConcelierProblemResultFactory.InvalidSources(
|
||||
context,
|
||||
invalidSources,
|
||||
registeredSources);
|
||||
}
|
||||
}
|
||||
|
||||
var bundle = await coordinator.CreateSnapshotAsync(
|
||||
request?.Label,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new CreateSnapshotResponse(
|
||||
bundle.SnapshotId,
|
||||
bundle.CompositeDigest,
|
||||
bundle.CreatedAt,
|
||||
bundle.Sources.Select(s => new SourceSnapshotSummary(
|
||||
s.SourceId,
|
||||
s.Digest,
|
||||
s.ItemCount)).ToArray());
|
||||
|
||||
return HttpResults.Created(
|
||||
$"/api/v1/feeds/snapshot/{bundle.SnapshotId}",
|
||||
response);
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListSnapshotsAsync(
|
||||
HttpContext context,
|
||||
IFeedSnapshotCoordinator coordinator,
|
||||
IOptionsMonitor<ConcelierOptions> optionsMonitor,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? cursor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var options = optionsMonitor.CurrentValue;
|
||||
|
||||
if (!options.FeedSnapshot.Enabled)
|
||||
{
|
||||
return ConcelierProblemResultFactory.FeatureDisabled(context, "FeedSnapshot");
|
||||
}
|
||||
|
||||
var effectiveLimit = Math.Min(
|
||||
Math.Max(limit ?? 50, 1),
|
||||
options.FeedSnapshot.MaxListPageSize);
|
||||
|
||||
var summaries = await coordinator.ListSnapshotsAsync(
|
||||
cursor,
|
||||
effectiveLimit,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new ListSnapshotsResponse(
|
||||
summaries.Select(s => new SnapshotListItem(
|
||||
s.SnapshotId,
|
||||
s.CompositeDigest,
|
||||
s.CreatedAt,
|
||||
s.Label,
|
||||
s.SourceCount,
|
||||
s.TotalItemCount)).ToArray());
|
||||
|
||||
return HttpResults.Ok(response);
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetSnapshotAsync(
|
||||
HttpContext context,
|
||||
IFeedSnapshotCoordinator coordinator,
|
||||
IOptionsMonitor<ConcelierOptions> optionsMonitor,
|
||||
string snapshotId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var options = optionsMonitor.CurrentValue;
|
||||
|
||||
if (!options.FeedSnapshot.Enabled)
|
||||
{
|
||||
return ConcelierProblemResultFactory.FeatureDisabled(context, "FeedSnapshot");
|
||||
}
|
||||
|
||||
var bundle = await coordinator.GetSnapshotAsync(snapshotId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (bundle is null)
|
||||
{
|
||||
return ConcelierProblemResultFactory.SnapshotNotFound(context, snapshotId);
|
||||
}
|
||||
|
||||
var response = new GetSnapshotResponse(
|
||||
bundle.SnapshotId,
|
||||
bundle.CompositeDigest,
|
||||
bundle.CreatedAt,
|
||||
bundle.Label,
|
||||
bundle.Sources.Select(s => new SourceSnapshotDetail(
|
||||
s.SourceId,
|
||||
s.Digest,
|
||||
s.ItemCount,
|
||||
s.CreatedAt)).ToArray());
|
||||
|
||||
return HttpResults.Ok(response);
|
||||
}
|
||||
|
||||
private static async Task<IResult> ExportSnapshotAsync(
|
||||
HttpContext context,
|
||||
IFeedSnapshotCoordinator coordinator,
|
||||
IOptionsMonitor<ConcelierOptions> optionsMonitor,
|
||||
string snapshotId,
|
||||
[FromQuery] string? format,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var options = optionsMonitor.CurrentValue;
|
||||
|
||||
if (!options.FeedSnapshot.Enabled)
|
||||
{
|
||||
return ConcelierProblemResultFactory.FeatureDisabled(context, "FeedSnapshot");
|
||||
}
|
||||
|
||||
var exportOptions = new ExportBundleOptions
|
||||
{
|
||||
Compression = ParseCompression(format),
|
||||
IncludeManifest = true,
|
||||
IncludeChecksums = true
|
||||
};
|
||||
|
||||
var metadata = await coordinator.ExportBundleAsync(
|
||||
snapshotId,
|
||||
exportOptions,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (metadata is null)
|
||||
{
|
||||
return ConcelierProblemResultFactory.SnapshotNotFound(context, snapshotId);
|
||||
}
|
||||
|
||||
context.Response.Headers.ContentDisposition =
|
||||
$"attachment; filename=\"snapshot-{snapshotId}.tar.{GetExtension(exportOptions.Compression)}\"";
|
||||
|
||||
return HttpResults.File(
|
||||
metadata.ExportPath,
|
||||
MediaTypeNames.Application.Octet,
|
||||
$"snapshot-{snapshotId}.tar.{GetExtension(exportOptions.Compression)}");
|
||||
}
|
||||
|
||||
private static async Task<IResult> ImportSnapshotAsync(
|
||||
HttpContext context,
|
||||
IFeedSnapshotCoordinator coordinator,
|
||||
IOptionsMonitor<ConcelierOptions> optionsMonitor,
|
||||
IFormFile file,
|
||||
[FromQuery] bool? validate,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var options = optionsMonitor.CurrentValue;
|
||||
|
||||
if (!options.FeedSnapshot.Enabled)
|
||||
{
|
||||
return ConcelierProblemResultFactory.FeatureDisabled(context, "FeedSnapshot");
|
||||
}
|
||||
|
||||
if (file.Length == 0)
|
||||
{
|
||||
return ConcelierProblemResultFactory.EmptyFile(context);
|
||||
}
|
||||
|
||||
if (file.Length > options.FeedSnapshot.MaxBundleSizeBytes)
|
||||
{
|
||||
return ConcelierProblemResultFactory.FileTooLarge(
|
||||
context,
|
||||
file.Length,
|
||||
options.FeedSnapshot.MaxBundleSizeBytes);
|
||||
}
|
||||
|
||||
await using var stream = file.OpenReadStream();
|
||||
|
||||
var importOptions = new ImportBundleOptions
|
||||
{
|
||||
ValidateDigests = validate ?? true
|
||||
};
|
||||
|
||||
var bundle = await coordinator.ImportBundleAsync(
|
||||
stream,
|
||||
importOptions,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new ImportSnapshotResponse(
|
||||
bundle.SnapshotId,
|
||||
bundle.CompositeDigest,
|
||||
bundle.CreatedAt,
|
||||
bundle.Sources.Count);
|
||||
|
||||
return HttpResults.Created(
|
||||
$"/api/v1/feeds/snapshot/{bundle.SnapshotId}",
|
||||
response);
|
||||
}
|
||||
|
||||
private static async Task<IResult> ValidateSnapshotAsync(
|
||||
HttpContext context,
|
||||
IFeedSnapshotCoordinator coordinator,
|
||||
IOptionsMonitor<ConcelierOptions> optionsMonitor,
|
||||
string snapshotId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var options = optionsMonitor.CurrentValue;
|
||||
|
||||
if (!options.FeedSnapshot.Enabled)
|
||||
{
|
||||
return ConcelierProblemResultFactory.FeatureDisabled(context, "FeedSnapshot");
|
||||
}
|
||||
|
||||
var result = await coordinator.ValidateSnapshotAsync(snapshotId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
return ConcelierProblemResultFactory.SnapshotNotFound(context, snapshotId);
|
||||
}
|
||||
|
||||
var response = new ValidateSnapshotResponse(
|
||||
result.IsValid,
|
||||
result.SnapshotDigest,
|
||||
result.CurrentDigest,
|
||||
result.DriftedSources.Select(d => new DriftedSourceInfo(
|
||||
d.SourceId,
|
||||
d.SnapshotDigest,
|
||||
d.CurrentDigest,
|
||||
d.AddedItems,
|
||||
d.RemovedItems,
|
||||
d.ModifiedItems)).ToArray());
|
||||
|
||||
return HttpResults.Ok(response);
|
||||
}
|
||||
|
||||
private static IResult ListSourcesAsync(
|
||||
HttpContext context,
|
||||
IFeedSnapshotCoordinator coordinator,
|
||||
IOptionsMonitor<ConcelierOptions> optionsMonitor)
|
||||
{
|
||||
var options = optionsMonitor.CurrentValue;
|
||||
|
||||
if (!options.FeedSnapshot.Enabled)
|
||||
{
|
||||
return ConcelierProblemResultFactory.FeatureDisabled(context, "FeedSnapshot");
|
||||
}
|
||||
|
||||
var sources = coordinator.RegisteredSources;
|
||||
return HttpResults.Ok(new ListSourcesResponse(sources.ToArray()));
|
||||
}
|
||||
|
||||
private static CompressionAlgorithm ParseCompression(string? format)
|
||||
{
|
||||
return format?.ToUpperInvariant() switch
|
||||
{
|
||||
"ZSTD" => CompressionAlgorithm.Zstd,
|
||||
"GZIP" or "GZ" => CompressionAlgorithm.Gzip,
|
||||
"NONE" => CompressionAlgorithm.None,
|
||||
_ => CompressionAlgorithm.Zstd // Default to Zstd
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetExtension(CompressionAlgorithm compression)
|
||||
{
|
||||
return compression switch
|
||||
{
|
||||
CompressionAlgorithm.Zstd => "zst",
|
||||
CompressionAlgorithm.Gzip => "gz",
|
||||
_ => "tar"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Request/Response DTOs ----
|
||||
|
||||
/// <summary>Request to create a feed snapshot.</summary>
|
||||
/// <param name="Label">Optional human-readable label for the snapshot.</param>
|
||||
/// <param name="Sources">Optional list of source IDs to include. If null, all registered sources are included.</param>
|
||||
public sealed record CreateSnapshotRequest(
|
||||
string? Label,
|
||||
string[]? Sources);
|
||||
|
||||
/// <summary>Response after creating a feed snapshot.</summary>
|
||||
public sealed record CreateSnapshotResponse(
|
||||
string SnapshotId,
|
||||
string CompositeDigest,
|
||||
DateTimeOffset CreatedAt,
|
||||
SourceSnapshotSummary[] Sources);
|
||||
|
||||
/// <summary>Summary of a source in a snapshot.</summary>
|
||||
public sealed record SourceSnapshotSummary(
|
||||
string SourceId,
|
||||
string Digest,
|
||||
int ItemCount);
|
||||
|
||||
/// <summary>Response listing available snapshots.</summary>
|
||||
public sealed record ListSnapshotsResponse(
|
||||
SnapshotListItem[] Snapshots);
|
||||
|
||||
/// <summary>Item in the snapshot list.</summary>
|
||||
public sealed record SnapshotListItem(
|
||||
string SnapshotId,
|
||||
string CompositeDigest,
|
||||
DateTimeOffset CreatedAt,
|
||||
string? Label,
|
||||
int SourceCount,
|
||||
int TotalItemCount);
|
||||
|
||||
/// <summary>Response with snapshot details.</summary>
|
||||
public sealed record GetSnapshotResponse(
|
||||
string SnapshotId,
|
||||
string CompositeDigest,
|
||||
DateTimeOffset CreatedAt,
|
||||
string? Label,
|
||||
SourceSnapshotDetail[] Sources);
|
||||
|
||||
/// <summary>Detailed info about a source in a snapshot.</summary>
|
||||
public sealed record SourceSnapshotDetail(
|
||||
string SourceId,
|
||||
string Digest,
|
||||
int ItemCount,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
/// <summary>Response after importing a snapshot.</summary>
|
||||
public sealed record ImportSnapshotResponse(
|
||||
string SnapshotId,
|
||||
string CompositeDigest,
|
||||
DateTimeOffset CreatedAt,
|
||||
int SourceCount);
|
||||
|
||||
/// <summary>Response from snapshot validation.</summary>
|
||||
public sealed record ValidateSnapshotResponse(
|
||||
bool IsValid,
|
||||
string SnapshotDigest,
|
||||
string CurrentDigest,
|
||||
DriftedSourceInfo[] DriftedSources);
|
||||
|
||||
/// <summary>Info about a drifted source.</summary>
|
||||
public sealed record DriftedSourceInfo(
|
||||
string SourceId,
|
||||
string SnapshotDigest,
|
||||
string CurrentDigest,
|
||||
int AddedItems,
|
||||
int RemovedItems,
|
||||
int ModifiedItems);
|
||||
|
||||
/// <summary>Response listing registered sources.</summary>
|
||||
public sealed record ListSourcesResponse(
|
||||
string[] Sources);
|
||||
@@ -44,6 +44,12 @@ public sealed class ConcelierOptions
|
||||
/// </summary>
|
||||
public FederationOptions Federation { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Feed snapshot configuration for atomic multi-source snapshots.
|
||||
/// Per DET-GAP-03 in SPRINT_20251226_007_BE_determinism_gaps.
|
||||
/// </summary>
|
||||
public FeedSnapshotOptions FeedSnapshot { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Stella Router integration configuration (disabled by default).
|
||||
/// When enabled, ASP.NET endpoints are automatically registered with the Router.
|
||||
@@ -303,4 +309,41 @@ public sealed class ConcelierOptions
|
||||
/// </summary>
|
||||
public bool RequireSignature { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Feed snapshot options for atomic multi-source snapshots.
|
||||
/// Per DET-GAP-03 in SPRINT_20251226_007_BE_determinism_gaps.
|
||||
/// </summary>
|
||||
public sealed class FeedSnapshotOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable feed snapshot endpoints.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum list page size for snapshot listing.
|
||||
/// </summary>
|
||||
public int MaxListPageSize { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum bundle size in bytes for import (default 1GB).
|
||||
/// </summary>
|
||||
public long MaxBundleSizeBytes { get; set; } = 1024L * 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot retention days. Snapshots older than this are automatically cleaned up.
|
||||
/// </summary>
|
||||
public int RetentionDays { get; set; } = 90;
|
||||
|
||||
/// <summary>
|
||||
/// Path to store snapshot bundles.
|
||||
/// </summary>
|
||||
public string StoragePath { get; set; } = System.IO.Path.Combine("out", "snapshots");
|
||||
|
||||
/// <summary>
|
||||
/// Default compression algorithm for exports.
|
||||
/// </summary>
|
||||
public string DefaultCompression { get; set; } = "zstd";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +199,92 @@ public static class ConcelierProblemResultFactory
|
||||
return NotFound(context, ErrorCodes.BundleSourceNotFound, "Bundle source", sourceId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a 404 Not Found response for snapshot not found.
|
||||
/// Per DET-GAP-03.
|
||||
/// </summary>
|
||||
public static IResult SnapshotNotFound(HttpContext context, string? snapshotId = null)
|
||||
{
|
||||
return NotFound(context, ErrorCodes.SnapshotNotFound, "Feed snapshot", snapshotId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a 400 Bad Request response for invalid feed sources.
|
||||
/// Per DET-GAP-03.
|
||||
/// </summary>
|
||||
public static IResult InvalidSources(
|
||||
HttpContext context,
|
||||
IReadOnlyList<string> invalidSources,
|
||||
IReadOnlyList<string> validSources)
|
||||
{
|
||||
return Problem(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid feed sources",
|
||||
StatusCodes.Status400BadRequest,
|
||||
ErrorCodes.InvalidSources,
|
||||
$"Invalid sources: [{string.Join(", ", invalidSources)}]. Valid sources: [{string.Join(", ", validSources)}].",
|
||||
"sources",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["invalidSources"] = invalidSources,
|
||||
["validSources"] = validSources
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a 400 Bad Request response for empty file.
|
||||
/// Per DET-GAP-03.
|
||||
/// </summary>
|
||||
public static IResult EmptyFile(HttpContext context)
|
||||
{
|
||||
return Problem(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Empty file",
|
||||
StatusCodes.Status400BadRequest,
|
||||
ErrorCodes.EmptyFile,
|
||||
"The uploaded file is empty.",
|
||||
"file");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a 400 Bad Request response for file too large.
|
||||
/// Per DET-GAP-03.
|
||||
/// </summary>
|
||||
public static IResult FileTooLarge(HttpContext context, long actualSize, long maxSize)
|
||||
{
|
||||
return Problem(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"File too large",
|
||||
StatusCodes.Status400BadRequest,
|
||||
ErrorCodes.FileTooLarge,
|
||||
$"File size ({actualSize} bytes) exceeds maximum allowed ({maxSize} bytes).",
|
||||
"file",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["actualSize"] = actualSize,
|
||||
["maxSize"] = maxSize
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a 403 Forbidden response for feature disabled.
|
||||
/// Per DET-GAP-03.
|
||||
/// </summary>
|
||||
public static IResult FeatureDisabled(HttpContext context, string featureName)
|
||||
{
|
||||
return Problem(
|
||||
context,
|
||||
ProblemTypes.Forbidden,
|
||||
"Feature disabled",
|
||||
StatusCodes.Status403Forbidden,
|
||||
ErrorCodes.FeatureDisabled,
|
||||
$"The {featureName} feature is not enabled. Enable it in configuration to use this endpoint.",
|
||||
featureName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a 404 Not Found response for bundle not found.
|
||||
/// </summary>
|
||||
|
||||
@@ -45,5 +45,6 @@
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Router.AspNet/StellaOps.Router.AspNet.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Replay.Core/StellaOps.Replay.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user