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
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:
@@ -0,0 +1,402 @@
|
||||
namespace StellaOps.Findings.Ledger.Infrastructure.Postgres;
|
||||
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Npgsql;
|
||||
using NpgsqlTypes;
|
||||
using StellaOps.Findings.Ledger.Domain;
|
||||
using StellaOps.Findings.Ledger.Infrastructure.Snapshot;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of snapshot repository.
|
||||
/// </summary>
|
||||
public sealed class PostgresSnapshotRepository : ISnapshotRepository
|
||||
{
|
||||
private readonly NpgsqlDataSource _dataSource;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public PostgresSnapshotRepository(NpgsqlDataSource dataSource)
|
||||
{
|
||||
_dataSource = dataSource;
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<LedgerSnapshot> CreateAsync(
|
||||
string tenantId,
|
||||
CreateSnapshotInput input,
|
||||
long currentSequence,
|
||||
DateTimeOffset currentTimestamp,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var snapshotId = Guid.NewGuid();
|
||||
var createdAt = DateTimeOffset.UtcNow;
|
||||
var expiresAt = input.ExpiresIn.HasValue
|
||||
? createdAt.Add(input.ExpiresIn.Value)
|
||||
: (DateTimeOffset?)null;
|
||||
|
||||
var sequenceNumber = input.AtSequence ?? currentSequence;
|
||||
var timestamp = input.AtTimestamp ?? currentTimestamp;
|
||||
|
||||
var initialStats = new SnapshotStatistics(0, 0, 0, 0, 0, 0);
|
||||
var metadataJson = input.Metadata != null
|
||||
? JsonSerializer.Serialize(input.Metadata, _jsonOptions)
|
||||
: null;
|
||||
var entityTypesJson = input.IncludeEntityTypes != null
|
||||
? JsonSerializer.Serialize(input.IncludeEntityTypes.Select(e => e.ToString()).ToList(), _jsonOptions)
|
||||
: null;
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO ledger_snapshots (
|
||||
tenant_id, snapshot_id, label, description, status,
|
||||
created_at, expires_at, sequence_number, snapshot_timestamp,
|
||||
findings_count, vex_statements_count, advisories_count,
|
||||
sboms_count, events_count, size_bytes,
|
||||
merkle_root, dsse_digest, metadata, include_entity_types, sign_requested
|
||||
) VALUES (
|
||||
@tenantId, @snapshotId, @label, @description, @status,
|
||||
@createdAt, @expiresAt, @sequenceNumber, @timestamp,
|
||||
@findingsCount, @vexCount, @advisoriesCount,
|
||||
@sbomsCount, @eventsCount, @sizeBytes,
|
||||
@merkleRoot, @dsseDigest, @metadata::jsonb, @entityTypes::jsonb, @sign
|
||||
)
|
||||
""";
|
||||
|
||||
await using var cmd = _dataSource.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
||||
cmd.Parameters.AddWithValue("snapshotId", snapshotId);
|
||||
cmd.Parameters.AddWithValue("label", (object?)input.Label ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("description", (object?)input.Description ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("status", SnapshotStatus.Creating.ToString());
|
||||
cmd.Parameters.AddWithValue("createdAt", createdAt);
|
||||
cmd.Parameters.AddWithValue("expiresAt", (object?)expiresAt ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("sequenceNumber", sequenceNumber);
|
||||
cmd.Parameters.AddWithValue("timestamp", timestamp);
|
||||
cmd.Parameters.AddWithValue("findingsCount", initialStats.FindingsCount);
|
||||
cmd.Parameters.AddWithValue("vexCount", initialStats.VexStatementsCount);
|
||||
cmd.Parameters.AddWithValue("advisoriesCount", initialStats.AdvisoriesCount);
|
||||
cmd.Parameters.AddWithValue("sbomsCount", initialStats.SbomsCount);
|
||||
cmd.Parameters.AddWithValue("eventsCount", initialStats.EventsCount);
|
||||
cmd.Parameters.AddWithValue("sizeBytes", initialStats.SizeBytes);
|
||||
cmd.Parameters.AddWithValue("merkleRoot", DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("dsseDigest", DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("metadata", (object?)metadataJson ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("entityTypes", (object?)entityTypesJson ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("sign", input.Sign);
|
||||
|
||||
await cmd.ExecuteNonQueryAsync(ct);
|
||||
|
||||
return new LedgerSnapshot(
|
||||
tenantId,
|
||||
snapshotId,
|
||||
input.Label,
|
||||
input.Description,
|
||||
SnapshotStatus.Creating,
|
||||
createdAt,
|
||||
expiresAt,
|
||||
sequenceNumber,
|
||||
timestamp,
|
||||
initialStats,
|
||||
null,
|
||||
null,
|
||||
input.Metadata);
|
||||
}
|
||||
|
||||
public async Task<LedgerSnapshot?> GetByIdAsync(
|
||||
string tenantId,
|
||||
Guid snapshotId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT tenant_id, snapshot_id, label, description, status,
|
||||
created_at, expires_at, sequence_number, snapshot_timestamp,
|
||||
findings_count, vex_statements_count, advisories_count,
|
||||
sboms_count, events_count, size_bytes,
|
||||
merkle_root, dsse_digest, metadata
|
||||
FROM ledger_snapshots
|
||||
WHERE tenant_id = @tenantId AND snapshot_id = @snapshotId
|
||||
""";
|
||||
|
||||
await using var cmd = _dataSource.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
||||
cmd.Parameters.AddWithValue("snapshotId", snapshotId);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
if (!await reader.ReadAsync(ct))
|
||||
return null;
|
||||
|
||||
return MapSnapshot(reader);
|
||||
}
|
||||
|
||||
public async Task<(IReadOnlyList<LedgerSnapshot> Snapshots, string? NextPageToken)> ListAsync(
|
||||
SnapshotListQuery query,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var sql = new StringBuilder("""
|
||||
SELECT tenant_id, snapshot_id, label, description, status,
|
||||
created_at, expires_at, sequence_number, snapshot_timestamp,
|
||||
findings_count, vex_statements_count, advisories_count,
|
||||
sboms_count, events_count, size_bytes,
|
||||
merkle_root, dsse_digest, metadata
|
||||
FROM ledger_snapshots
|
||||
WHERE tenant_id = @tenantId
|
||||
""");
|
||||
|
||||
var parameters = new List<NpgsqlParameter>
|
||||
{
|
||||
new("tenantId", query.TenantId)
|
||||
};
|
||||
|
||||
if (query.Status.HasValue)
|
||||
{
|
||||
sql.Append(" AND status = @status");
|
||||
parameters.Add(new NpgsqlParameter("status", query.Status.Value.ToString()));
|
||||
}
|
||||
|
||||
if (query.CreatedAfter.HasValue)
|
||||
{
|
||||
sql.Append(" AND created_at >= @createdAfter");
|
||||
parameters.Add(new NpgsqlParameter("createdAfter", query.CreatedAfter.Value));
|
||||
}
|
||||
|
||||
if (query.CreatedBefore.HasValue)
|
||||
{
|
||||
sql.Append(" AND created_at < @createdBefore");
|
||||
parameters.Add(new NpgsqlParameter("createdBefore", query.CreatedBefore.Value));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(query.PageToken))
|
||||
{
|
||||
if (Guid.TryParse(query.PageToken, out var lastId))
|
||||
{
|
||||
sql.Append(" AND snapshot_id > @lastId");
|
||||
parameters.Add(new NpgsqlParameter("lastId", lastId));
|
||||
}
|
||||
}
|
||||
|
||||
sql.Append(" ORDER BY created_at DESC, snapshot_id");
|
||||
sql.Append(" LIMIT @limit");
|
||||
parameters.Add(new NpgsqlParameter("limit", query.PageSize + 1));
|
||||
|
||||
await using var cmd = _dataSource.CreateCommand(sql.ToString());
|
||||
cmd.Parameters.AddRange(parameters.ToArray());
|
||||
|
||||
var snapshots = new List<LedgerSnapshot>();
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
|
||||
while (await reader.ReadAsync(ct) && snapshots.Count < query.PageSize)
|
||||
{
|
||||
snapshots.Add(MapSnapshot(reader));
|
||||
}
|
||||
|
||||
string? nextPageToken = null;
|
||||
if (await reader.ReadAsync(ct))
|
||||
{
|
||||
nextPageToken = snapshots.Last().SnapshotId.ToString();
|
||||
}
|
||||
|
||||
return (snapshots, nextPageToken);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateStatusAsync(
|
||||
string tenantId,
|
||||
Guid snapshotId,
|
||||
SnapshotStatus newStatus,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE ledger_snapshots
|
||||
SET status = @status, updated_at = @updatedAt
|
||||
WHERE tenant_id = @tenantId AND snapshot_id = @snapshotId
|
||||
""";
|
||||
|
||||
await using var cmd = _dataSource.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
||||
cmd.Parameters.AddWithValue("snapshotId", snapshotId);
|
||||
cmd.Parameters.AddWithValue("status", newStatus.ToString());
|
||||
cmd.Parameters.AddWithValue("updatedAt", DateTimeOffset.UtcNow);
|
||||
|
||||
return await cmd.ExecuteNonQueryAsync(ct) > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateStatisticsAsync(
|
||||
string tenantId,
|
||||
Guid snapshotId,
|
||||
SnapshotStatistics statistics,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE ledger_snapshots
|
||||
SET findings_count = @findingsCount,
|
||||
vex_statements_count = @vexCount,
|
||||
advisories_count = @advisoriesCount,
|
||||
sboms_count = @sbomsCount,
|
||||
events_count = @eventsCount,
|
||||
size_bytes = @sizeBytes,
|
||||
updated_at = @updatedAt
|
||||
WHERE tenant_id = @tenantId AND snapshot_id = @snapshotId
|
||||
""";
|
||||
|
||||
await using var cmd = _dataSource.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
||||
cmd.Parameters.AddWithValue("snapshotId", snapshotId);
|
||||
cmd.Parameters.AddWithValue("findingsCount", statistics.FindingsCount);
|
||||
cmd.Parameters.AddWithValue("vexCount", statistics.VexStatementsCount);
|
||||
cmd.Parameters.AddWithValue("advisoriesCount", statistics.AdvisoriesCount);
|
||||
cmd.Parameters.AddWithValue("sbomsCount", statistics.SbomsCount);
|
||||
cmd.Parameters.AddWithValue("eventsCount", statistics.EventsCount);
|
||||
cmd.Parameters.AddWithValue("sizeBytes", statistics.SizeBytes);
|
||||
cmd.Parameters.AddWithValue("updatedAt", DateTimeOffset.UtcNow);
|
||||
|
||||
return await cmd.ExecuteNonQueryAsync(ct) > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> SetMerkleRootAsync(
|
||||
string tenantId,
|
||||
Guid snapshotId,
|
||||
string merkleRoot,
|
||||
string? dsseDigest,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE ledger_snapshots
|
||||
SET merkle_root = @merkleRoot,
|
||||
dsse_digest = @dsseDigest,
|
||||
updated_at = @updatedAt
|
||||
WHERE tenant_id = @tenantId AND snapshot_id = @snapshotId
|
||||
""";
|
||||
|
||||
await using var cmd = _dataSource.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
||||
cmd.Parameters.AddWithValue("snapshotId", snapshotId);
|
||||
cmd.Parameters.AddWithValue("merkleRoot", merkleRoot);
|
||||
cmd.Parameters.AddWithValue("dsseDigest", (object?)dsseDigest ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("updatedAt", DateTimeOffset.UtcNow);
|
||||
|
||||
return await cmd.ExecuteNonQueryAsync(ct) > 0;
|
||||
}
|
||||
|
||||
public async Task<int> ExpireSnapshotsAsync(
|
||||
DateTimeOffset cutoff,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE ledger_snapshots
|
||||
SET status = @expiredStatus, updated_at = @updatedAt
|
||||
WHERE expires_at IS NOT NULL
|
||||
AND expires_at < @cutoff
|
||||
AND status = @availableStatus
|
||||
""";
|
||||
|
||||
await using var cmd = _dataSource.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue("expiredStatus", SnapshotStatus.Expired.ToString());
|
||||
cmd.Parameters.AddWithValue("availableStatus", SnapshotStatus.Available.ToString());
|
||||
cmd.Parameters.AddWithValue("cutoff", cutoff);
|
||||
cmd.Parameters.AddWithValue("updatedAt", DateTimeOffset.UtcNow);
|
||||
|
||||
return await cmd.ExecuteNonQueryAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(
|
||||
string tenantId,
|
||||
Guid snapshotId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE ledger_snapshots
|
||||
SET status = @deletedStatus, updated_at = @updatedAt
|
||||
WHERE tenant_id = @tenantId AND snapshot_id = @snapshotId
|
||||
""";
|
||||
|
||||
await using var cmd = _dataSource.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
||||
cmd.Parameters.AddWithValue("snapshotId", snapshotId);
|
||||
cmd.Parameters.AddWithValue("deletedStatus", SnapshotStatus.Deleted.ToString());
|
||||
cmd.Parameters.AddWithValue("updatedAt", DateTimeOffset.UtcNow);
|
||||
|
||||
return await cmd.ExecuteNonQueryAsync(ct) > 0;
|
||||
}
|
||||
|
||||
public async Task<LedgerSnapshot?> GetLatestAsync(
|
||||
string tenantId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT tenant_id, snapshot_id, label, description, status,
|
||||
created_at, expires_at, sequence_number, snapshot_timestamp,
|
||||
findings_count, vex_statements_count, advisories_count,
|
||||
sboms_count, events_count, size_bytes,
|
||||
merkle_root, dsse_digest, metadata
|
||||
FROM ledger_snapshots
|
||||
WHERE tenant_id = @tenantId AND status = @status
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
await using var cmd = _dataSource.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
||||
cmd.Parameters.AddWithValue("status", SnapshotStatus.Available.ToString());
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
if (!await reader.ReadAsync(ct))
|
||||
return null;
|
||||
|
||||
return MapSnapshot(reader);
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(
|
||||
string tenantId,
|
||||
Guid snapshotId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT 1 FROM ledger_snapshots
|
||||
WHERE tenant_id = @tenantId AND snapshot_id = @snapshotId
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
await using var cmd = _dataSource.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
||||
cmd.Parameters.AddWithValue("snapshotId", snapshotId);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
return await reader.ReadAsync(ct);
|
||||
}
|
||||
|
||||
private LedgerSnapshot MapSnapshot(NpgsqlDataReader reader)
|
||||
{
|
||||
var metadataJson = reader.IsDBNull(reader.GetOrdinal("metadata"))
|
||||
? null
|
||||
: reader.GetString(reader.GetOrdinal("metadata"));
|
||||
|
||||
Dictionary<string, object>? metadata = null;
|
||||
if (!string.IsNullOrEmpty(metadataJson))
|
||||
{
|
||||
metadata = JsonSerializer.Deserialize<Dictionary<string, object>>(metadataJson, _jsonOptions);
|
||||
}
|
||||
|
||||
return new LedgerSnapshot(
|
||||
TenantId: reader.GetString(reader.GetOrdinal("tenant_id")),
|
||||
SnapshotId: reader.GetGuid(reader.GetOrdinal("snapshot_id")),
|
||||
Label: reader.IsDBNull(reader.GetOrdinal("label")) ? null : reader.GetString(reader.GetOrdinal("label")),
|
||||
Description: reader.IsDBNull(reader.GetOrdinal("description")) ? null : reader.GetString(reader.GetOrdinal("description")),
|
||||
Status: Enum.Parse<SnapshotStatus>(reader.GetString(reader.GetOrdinal("status"))),
|
||||
CreatedAt: reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("created_at")),
|
||||
ExpiresAt: reader.IsDBNull(reader.GetOrdinal("expires_at")) ? null : reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("expires_at")),
|
||||
SequenceNumber: reader.GetInt64(reader.GetOrdinal("sequence_number")),
|
||||
Timestamp: reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("snapshot_timestamp")),
|
||||
Statistics: new SnapshotStatistics(
|
||||
FindingsCount: reader.GetInt64(reader.GetOrdinal("findings_count")),
|
||||
VexStatementsCount: reader.GetInt64(reader.GetOrdinal("vex_statements_count")),
|
||||
AdvisoriesCount: reader.GetInt64(reader.GetOrdinal("advisories_count")),
|
||||
SbomsCount: reader.GetInt64(reader.GetOrdinal("sboms_count")),
|
||||
EventsCount: reader.GetInt64(reader.GetOrdinal("events_count")),
|
||||
SizeBytes: reader.GetInt64(reader.GetOrdinal("size_bytes"))),
|
||||
MerkleRoot: reader.IsDBNull(reader.GetOrdinal("merkle_root")) ? null : reader.GetString(reader.GetOrdinal("merkle_root")),
|
||||
DsseDigest: reader.IsDBNull(reader.GetOrdinal("dsse_digest")) ? null : reader.GetString(reader.GetOrdinal("dsse_digest")),
|
||||
Metadata: metadata);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user