Add unit tests for PhpFrameworkSurface and PhpPharScanner
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled

- Implement comprehensive tests for PhpFrameworkSurface, covering scenarios such as empty surfaces, presence of routes, controllers, middlewares, CLI commands, cron jobs, and event listeners.
- Validate metadata creation for route counts, HTTP methods, protected and public routes, and route patterns.
- Introduce tests for PhpPharScanner, including handling of non-existent files, null or empty paths, invalid PHAR files, and minimal PHAR structures.
- Ensure correct computation of SHA256 for valid PHAR files and validate the properties of PhpPharArchive, PhpPharEntry, and PhpPharScanResult.
This commit is contained in:
StellaOps Bot
2025-12-07 13:44:13 +02:00
parent af30fc322f
commit 965cbf9574
49 changed files with 11935 additions and 152 deletions

View File

@@ -155,6 +155,14 @@ builder.Services.AddHostedService<LedgerMerkleAnchorWorker>();
builder.Services.AddHostedService<LedgerProjectionWorker>();
builder.Services.AddSingleton<ExportQueryService>();
builder.Services.AddSingleton<AttestationQueryService>();
builder.Services.AddSingleton<StellaOps.Findings.Ledger.Infrastructure.Attestation.IAttestationPointerRepository,
StellaOps.Findings.Ledger.Infrastructure.Postgres.PostgresAttestationPointerRepository>();
builder.Services.AddSingleton<AttestationPointerService>();
builder.Services.AddSingleton<StellaOps.Findings.Ledger.Infrastructure.Snapshot.ISnapshotRepository,
StellaOps.Findings.Ledger.Infrastructure.Postgres.PostgresSnapshotRepository>();
builder.Services.AddSingleton<StellaOps.Findings.Ledger.Infrastructure.Snapshot.ITimeTravelRepository,
StellaOps.Findings.Ledger.Infrastructure.Postgres.PostgresTimeTravelRepository>();
builder.Services.AddSingleton<SnapshotService>();
var app = builder.Build();
@@ -633,6 +641,206 @@ app.MapPost("/internal/ledger/airgap-import", async Task<Results<Accepted<Airgap
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status409Conflict);
// Attestation Pointer Endpoints (LEDGER-ATTEST-73-001)
app.MapPost("/v1/ledger/attestation-pointers", async Task<Results<Created<CreateAttestationPointerResponse>, Ok<CreateAttestationPointerResponse>, ProblemHttpResult>> (
HttpContext httpContext,
CreateAttestationPointerRequest request,
AttestationPointerService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
try
{
var input = request.ToInput(tenantId);
var result = await service.CreatePointerAsync(input, cancellationToken).ConfigureAwait(false);
var response = new CreateAttestationPointerResponse(
result.Success,
result.PointerId?.ToString(),
result.LedgerEventId?.ToString(),
result.Error);
if (!result.Success)
{
return TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "attestation_pointer_failed",
detail: result.Error);
}
return TypedResults.Created($"/v1/ledger/attestation-pointers/{result.PointerId}", response);
}
catch (ArgumentException ex)
{
return TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "invalid_request",
detail: ex.Message);
}
})
.WithName("CreateAttestationPointer")
.RequireAuthorization(LedgerWritePolicy)
.Produces(StatusCodes.Status201Created)
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.MapGet("/v1/ledger/attestation-pointers/{pointerId}", async Task<Results<JsonHttpResult<AttestationPointerResponse>, NotFound, ProblemHttpResult>> (
HttpContext httpContext,
string pointerId,
AttestationPointerService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
if (!Guid.TryParse(pointerId, out var pointerGuid))
{
return TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "invalid_pointer_id",
detail: "Pointer ID must be a valid GUID.");
}
var pointer = await service.GetPointerAsync(tenantId, pointerGuid, cancellationToken).ConfigureAwait(false);
if (pointer is null)
{
return TypedResults.NotFound();
}
return TypedResults.Json(pointer.ToResponse());
})
.WithName("GetAttestationPointer")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.MapGet("/v1/ledger/findings/{findingId}/attestation-pointers", async Task<Results<JsonHttpResult<IReadOnlyList<AttestationPointerResponse>>, ProblemHttpResult>> (
HttpContext httpContext,
string findingId,
AttestationPointerService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var pointers = await service.GetPointersAsync(tenantId, findingId, cancellationToken).ConfigureAwait(false);
IReadOnlyList<AttestationPointerResponse> responseList = pointers.Select(p => p.ToResponse()).ToList();
return TypedResults.Json(responseList);
})
.WithName("GetFindingAttestationPointers")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.MapGet("/v1/ledger/findings/{findingId}/attestation-summary", async Task<Results<JsonHttpResult<AttestationSummaryResponse>, ProblemHttpResult>> (
HttpContext httpContext,
string findingId,
AttestationPointerService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var summary = await service.GetSummaryAsync(tenantId, findingId, cancellationToken).ConfigureAwait(false);
return TypedResults.Json(summary.ToResponse());
})
.WithName("GetFindingAttestationSummary")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.MapPost("/v1/ledger/attestation-pointers/search", async Task<Results<JsonHttpResult<AttestationPointerSearchResponse>, ProblemHttpResult>> (
HttpContext httpContext,
AttestationPointerSearchRequest request,
AttestationPointerService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
try
{
var query = request.ToQuery(tenantId);
var pointers = await service.SearchAsync(query, cancellationToken).ConfigureAwait(false);
var response = new AttestationPointerSearchResponse(
pointers.Select(p => p.ToResponse()).ToList(),
pointers.Count);
return TypedResults.Json(response);
}
catch (ArgumentException ex)
{
return TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "invalid_request",
detail: ex.Message);
}
})
.WithName("SearchAttestationPointers")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.MapPut("/v1/ledger/attestation-pointers/{pointerId}/verification", async Task<Results<NoContent, NotFound, ProblemHttpResult>> (
HttpContext httpContext,
string pointerId,
UpdateVerificationResultRequest request,
AttestationPointerService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
if (!Guid.TryParse(pointerId, out var pointerGuid))
{
return TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "invalid_pointer_id",
detail: "Pointer ID must be a valid GUID.");
}
try
{
var verificationResult = request.VerificationResult.ToModel();
var success = await service.UpdateVerificationResultAsync(tenantId, pointerGuid, verificationResult, cancellationToken).ConfigureAwait(false);
if (!success)
{
return TypedResults.NotFound();
}
return TypedResults.NoContent();
}
catch (ArgumentException ex)
{
return TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "invalid_request",
detail: ex.Message);
}
})
.WithName("UpdateAttestationPointerVerification")
.RequireAuthorization(LedgerWritePolicy)
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.MapGet("/.well-known/openapi", () =>
{
var contentRoot = AppContext.BaseDirectory;
@@ -649,6 +857,383 @@ app.MapGet("/.well-known/openapi", () =>
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status500InternalServerError);
// Snapshot Endpoints (LEDGER-PACKS-42-001-DEV)
app.MapPost("/v1/ledger/snapshots", async Task<Results<Created<CreateSnapshotResponse>, ProblemHttpResult>> (
HttpContext httpContext,
CreateSnapshotRequest request,
SnapshotService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var input = request.ToInput(tenantId);
var result = await service.CreateSnapshotAsync(input, cancellationToken).ConfigureAwait(false);
var response = new CreateSnapshotResponse(
result.Success,
result.Snapshot?.ToResponse(),
result.Error);
if (!result.Success)
{
return TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "snapshot_creation_failed",
detail: result.Error);
}
return TypedResults.Created($"/v1/ledger/snapshots/{result.Snapshot!.SnapshotId}", response);
})
.WithName("CreateSnapshot")
.RequireAuthorization(LedgerWritePolicy)
.Produces(StatusCodes.Status201Created)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.MapGet("/v1/ledger/snapshots", async Task<Results<JsonHttpResult<SnapshotListResponse>, ProblemHttpResult>> (
HttpContext httpContext,
SnapshotService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var statusStr = httpContext.Request.Query["status"].ToString();
Domain.SnapshotStatus? status = null;
if (!string.IsNullOrEmpty(statusStr) && Enum.TryParse<Domain.SnapshotStatus>(statusStr, true, out var parsedStatus))
{
status = parsedStatus;
}
var query = new Domain.SnapshotListQuery(
tenantId,
status,
ParseDate(httpContext.Request.Query["created_after"]),
ParseDate(httpContext.Request.Query["created_before"]),
ParseInt(httpContext.Request.Query["page_size"]) ?? 100,
httpContext.Request.Query["page_token"].ToString());
var (snapshots, nextPageToken) = await service.ListSnapshotsAsync(query, cancellationToken).ConfigureAwait(false);
var response = new SnapshotListResponse(
snapshots.Select(s => s.ToResponse()).ToList(),
nextPageToken);
return TypedResults.Json(response);
})
.WithName("ListSnapshots")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.MapGet("/v1/ledger/snapshots/{snapshotId}", async Task<Results<JsonHttpResult<SnapshotResponse>, NotFound, ProblemHttpResult>> (
HttpContext httpContext,
string snapshotId,
SnapshotService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
if (!Guid.TryParse(snapshotId, out var snapshotGuid))
{
return TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "invalid_snapshot_id",
detail: "Snapshot ID must be a valid GUID.");
}
var snapshot = await service.GetSnapshotAsync(tenantId, snapshotGuid, cancellationToken).ConfigureAwait(false);
if (snapshot is null)
{
return TypedResults.NotFound();
}
return TypedResults.Json(snapshot.ToResponse());
})
.WithName("GetSnapshot")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.MapDelete("/v1/ledger/snapshots/{snapshotId}", async Task<Results<NoContent, NotFound, ProblemHttpResult>> (
HttpContext httpContext,
string snapshotId,
SnapshotService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
if (!Guid.TryParse(snapshotId, out var snapshotGuid))
{
return TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "invalid_snapshot_id",
detail: "Snapshot ID must be a valid GUID.");
}
var deleted = await service.DeleteSnapshotAsync(tenantId, snapshotGuid, cancellationToken).ConfigureAwait(false);
if (!deleted)
{
return TypedResults.NotFound();
}
return TypedResults.NoContent();
})
.WithName("DeleteSnapshot")
.RequireAuthorization(LedgerWritePolicy)
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status400BadRequest);
// Time-Travel Query Endpoints
app.MapGet("/v1/ledger/time-travel/findings", async Task<Results<JsonHttpResult<HistoricalQueryApiResponse<FindingHistoryResponse>>, ProblemHttpResult>> (
HttpContext httpContext,
SnapshotService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var request = new HistoricalQueryApiRequest(
AtTimestamp: ParseDate(httpContext.Request.Query["at_timestamp"]),
AtSequence: ParseLong(httpContext.Request.Query["at_sequence"]),
SnapshotId: ParseGuid(httpContext.Request.Query["snapshot_id"]),
Status: httpContext.Request.Query["status"].ToString(),
SeverityMin: ParseDecimal(httpContext.Request.Query["severity_min"]),
SeverityMax: ParseDecimal(httpContext.Request.Query["severity_max"]),
PolicyVersion: httpContext.Request.Query["policy_version"].ToString(),
ArtifactId: httpContext.Request.Query["artifact_id"].ToString(),
VulnId: httpContext.Request.Query["vuln_id"].ToString(),
PageSize: ParseInt(httpContext.Request.Query["page_size"]) ?? 500,
PageToken: httpContext.Request.Query["page_token"].ToString());
var domainRequest = request.ToRequest(tenantId, Domain.EntityType.Finding);
var result = await service.QueryHistoricalFindingsAsync(domainRequest, cancellationToken).ConfigureAwait(false);
var response = new HistoricalQueryApiResponse<FindingHistoryResponse>(
result.QueryPoint.ToResponse(),
"Finding",
result.Items.Select(i => i.ToResponse()).ToList(),
result.NextPageToken,
result.TotalCount);
return TypedResults.Json(response);
})
.WithName("TimeTravelQueryFindings")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.MapGet("/v1/ledger/time-travel/vex", async Task<Results<JsonHttpResult<HistoricalQueryApiResponse<VexHistoryResponse>>, ProblemHttpResult>> (
HttpContext httpContext,
SnapshotService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var request = new HistoricalQueryApiRequest(
AtTimestamp: ParseDate(httpContext.Request.Query["at_timestamp"]),
AtSequence: ParseLong(httpContext.Request.Query["at_sequence"]),
SnapshotId: ParseGuid(httpContext.Request.Query["snapshot_id"]),
PageSize: ParseInt(httpContext.Request.Query["page_size"]) ?? 500,
PageToken: httpContext.Request.Query["page_token"].ToString());
var domainRequest = request.ToRequest(tenantId, Domain.EntityType.Vex);
var result = await service.QueryHistoricalVexAsync(domainRequest, cancellationToken).ConfigureAwait(false);
var response = new HistoricalQueryApiResponse<VexHistoryResponse>(
result.QueryPoint.ToResponse(),
"Vex",
result.Items.Select(i => i.ToResponse()).ToList(),
result.NextPageToken,
result.TotalCount);
return TypedResults.Json(response);
})
.WithName("TimeTravelQueryVex")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.MapGet("/v1/ledger/time-travel/advisories", async Task<Results<JsonHttpResult<HistoricalQueryApiResponse<AdvisoryHistoryResponse>>, ProblemHttpResult>> (
HttpContext httpContext,
SnapshotService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var request = new HistoricalQueryApiRequest(
AtTimestamp: ParseDate(httpContext.Request.Query["at_timestamp"]),
AtSequence: ParseLong(httpContext.Request.Query["at_sequence"]),
SnapshotId: ParseGuid(httpContext.Request.Query["snapshot_id"]),
PageSize: ParseInt(httpContext.Request.Query["page_size"]) ?? 500,
PageToken: httpContext.Request.Query["page_token"].ToString());
var domainRequest = request.ToRequest(tenantId, Domain.EntityType.Advisory);
var result = await service.QueryHistoricalAdvisoriesAsync(domainRequest, cancellationToken).ConfigureAwait(false);
var response = new HistoricalQueryApiResponse<AdvisoryHistoryResponse>(
result.QueryPoint.ToResponse(),
"Advisory",
result.Items.Select(i => i.ToResponse()).ToList(),
result.NextPageToken,
result.TotalCount);
return TypedResults.Json(response);
})
.WithName("TimeTravelQueryAdvisories")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
// Replay Endpoint
app.MapPost("/v1/ledger/replay", async Task<Results<JsonHttpResult<ReplayApiResponse>, ProblemHttpResult>> (
HttpContext httpContext,
ReplayApiRequest request,
SnapshotService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var domainRequest = request.ToRequest(tenantId);
var (events, metadata) = await service.ReplayEventsAsync(domainRequest, cancellationToken).ConfigureAwait(false);
var response = new ReplayApiResponse(
events.Select(e => e.ToResponse()).ToList(),
metadata.ToResponse());
return TypedResults.Json(response);
})
.WithName("ReplayEvents")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
// Diff Endpoint
app.MapPost("/v1/ledger/diff", async Task<Results<JsonHttpResult<DiffApiResponse>, ProblemHttpResult>> (
HttpContext httpContext,
DiffApiRequest request,
SnapshotService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var domainRequest = request.ToRequest(tenantId);
var result = await service.ComputeDiffAsync(domainRequest, cancellationToken).ConfigureAwait(false);
var response = new DiffApiResponse(
result.FromPoint.ToResponse(),
result.ToPoint.ToResponse(),
result.Summary.ToResponse(),
result.Changes?.Select(c => c.ToResponse()).ToList(),
result.NextPageToken);
return TypedResults.Json(response);
})
.WithName("ComputeDiff")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
// Changelog Endpoint
app.MapGet("/v1/ledger/changelog/{entityType}/{entityId}", async Task<Results<JsonHttpResult<IReadOnlyList<ChangeLogEntryResponse>>, ProblemHttpResult>> (
HttpContext httpContext,
string entityType,
string entityId,
SnapshotService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
if (!Enum.TryParse<Domain.EntityType>(entityType, true, out var parsedEntityType))
{
return TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "invalid_entity_type",
detail: "Entity type must be one of: Finding, Vex, Advisory, Sbom, Evidence.");
}
var limit = ParseInt(httpContext.Request.Query["limit"]) ?? 100;
var changelog = await service.GetChangelogAsync(tenantId, parsedEntityType, entityId, limit, cancellationToken).ConfigureAwait(false);
IReadOnlyList<ChangeLogEntryResponse> response = changelog.Select(e => e.ToResponse()).ToList();
return TypedResults.Json(response);
})
.WithName("GetChangelog")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
// Staleness Check Endpoint
app.MapGet("/v1/ledger/staleness", async Task<Results<JsonHttpResult<StalenessResponse>, ProblemHttpResult>> (
HttpContext httpContext,
SnapshotService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var thresholdMinutes = ParseInt(httpContext.Request.Query["threshold_minutes"]) ?? 60;
var threshold = TimeSpan.FromMinutes(thresholdMinutes);
var result = await service.CheckStalenessAsync(tenantId, threshold, cancellationToken).ConfigureAwait(false);
return TypedResults.Json(result.ToResponse());
})
.WithName("CheckStaleness")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
// Current Point Endpoint
app.MapGet("/v1/ledger/current-point", async Task<Results<JsonHttpResult<QueryPointResponse>, ProblemHttpResult>> (
HttpContext httpContext,
SnapshotService service,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var point = await service.GetCurrentPointAsync(tenantId, cancellationToken).ConfigureAwait(false);
return TypedResults.Json(point.ToResponse());
})
.WithName("GetCurrentPoint")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.Run();
static Created<LedgerEventResponse> CreateCreatedResponse(LedgerEventRecord record)
@@ -738,3 +1323,8 @@ static bool? ParseBool(string value)
{
return bool.TryParse(value, out var result) ? result : null;
}
static Guid? ParseGuid(string value)
{
return Guid.TryParse(value, out var result) ? result : null;
}