feat(management): parquet audit export and audit archive list/verify (ScadaBridge#42)
GET /api/audit/export?format=parquet returned 501. It now streams the same 24 columns as csv/jsonl (AuditExportRow, CsvHeader order) as one Parquet file, one 10,000-row row group at a time. Each row group is encoded into a small non-seekable staging buffer and copied to the response asynchronously, because Kestrel rejects the synchronous writes the Parquet writer issues. New management commands, with CLI parity: - ListAuditArchives -> `audit archive list` (AuditArchiveListResult: enabled flag + every manifest). - VerifyAuditArchive(Name) -> `audit archive verify --name` (files, chain, purge anchor); exit 0 verified, 3 not verified, 1 error, 2 unauthorized. Both carry verify-chain's gate: OperationalAudit roles (Administrator, Viewer), site-scoped non-Administrator refused because an archive spans every site. VerifyAuditArchive uses the long-running Ask timeout. Error codes ARCHIVE_NOT_CONFIGURED, INVALID_ARGUMENT, NOT_FOUND. RequiredRoleMatrixTests freezes both gates; ManagementSurfaceParityTests now probes `audit archive list|verify` alongside `audit verify-chain`. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -506,21 +506,43 @@ public class AuditEndpointsTests
|
||||
Assert.Equal(propertyNames, AuditEndpoints.CsvHeader.Split(','));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a Parquet export returns 501.</summary>
|
||||
/// <summary>
|
||||
/// <c>format=parquet</c> (ScadaBridge#42; it used to be a 501) streams one readable Parquet
|
||||
/// file whose columns are the csv/jsonl export's columns, in order, holding every row.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Export_Parquet_Returns501()
|
||||
public async Task Export_Parquet_StreamsReadableParquet_WithTheCsvColumns()
|
||||
{
|
||||
// Parquet archival is deferred to v1.x (Component-AuditLog.md) — no
|
||||
// library is referenced, so the endpoint returns 501 with guidance.
|
||||
var (client, _, host) = await BuildHostAsync(roles: new[] { "Administrator" });
|
||||
var (client, _, host) = await BuildHostAsync(
|
||||
roles: new[] { "Administrator" },
|
||||
queryPages: new[]
|
||||
{
|
||||
(IReadOnlyList<AuditEvent>)new[]
|
||||
{
|
||||
SampleEvent(Guid.Parse("cccccccc-0000-0000-0000-000000000001")),
|
||||
SampleEvent(Guid.Parse("cccccccc-0000-0000-0000-000000000002")),
|
||||
},
|
||||
(IReadOnlyList<AuditEvent>)Array.Empty<AuditEvent>(),
|
||||
});
|
||||
using (host)
|
||||
{
|
||||
var response = await client.SendAsync(Get("/api/audit/export?format=parquet"));
|
||||
var response = await client.SendAsync(Get("/api/audit/export?format=Parquet"));
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains("Parquet export deferred to v1.x", body);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(AuditEndpoints.ParquetContentType, response.Content.Headers.ContentType!.MediaType);
|
||||
Assert.Contains(".parquet", response.Content.Headers.ContentDisposition?.ToString()
|
||||
?? response.Headers.GetValues("Content-Disposition").FirstOrDefault()
|
||||
?? response.Content.Headers.GetValues("Content-Disposition").First());
|
||||
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync();
|
||||
await using var reader = await Parquet.ParquetReader.CreateAsync(new MemoryStream(bytes));
|
||||
Assert.Equal(AuditEndpoints.CsvHeader.Split(','), reader.Schema.GetDataFields().Select(f => f.Name));
|
||||
using var group = reader.OpenRowGroupReader(0);
|
||||
Assert.Equal(2, group.RowCount);
|
||||
var ids = new string?[2];
|
||||
await group.ReadAsync(reader.Schema.GetDataFields()[0], ids!);
|
||||
Assert.Equal(new[] { "cccccccc-0000-0000-0000-000000000001", "cccccccc-0000-0000-0000-000000000002" }, ids);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user