Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Serialization;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
public sealed class PlatformEventSamplesTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[InlineData("scanner.event.report.ready@1.sample.json", OrchestratorEventKinds.ScannerReportReady)]
|
||||
[InlineData("scanner.event.scan.completed@1.sample.json", OrchestratorEventKinds.ScannerScanCompleted)]
|
||||
public void PlatformEventSamplesStayCanonical(string fileName, string expectedKind)
|
||||
{
|
||||
var json = LoadSample(fileName);
|
||||
var orchestratorEvent = DeserializeOrchestratorEvent(json, expectedKind);
|
||||
|
||||
Assert.NotNull(orchestratorEvent);
|
||||
Assert.Equal(expectedKind, orchestratorEvent.Kind);
|
||||
Assert.Equal(1, orchestratorEvent.Version);
|
||||
Assert.NotEqual(Guid.Empty, orchestratorEvent.EventId);
|
||||
Assert.NotNull(orchestratorEvent.Payload);
|
||||
|
||||
AssertCanonical(json, orchestratorEvent);
|
||||
AssertReportConsistency(orchestratorEvent);
|
||||
}
|
||||
|
||||
private static void AssertCanonical(string originalJson, OrchestratorEvent orchestratorEvent)
|
||||
{
|
||||
var canonicalJson = OrchestratorEventSerializer.Serialize(orchestratorEvent);
|
||||
var originalNode = JsonNode.Parse(originalJson) ?? throw new InvalidOperationException("Sample JSON must not be null.");
|
||||
var canonicalNode = JsonNode.Parse(canonicalJson) ?? throw new InvalidOperationException("Canonical JSON must not be null.");
|
||||
|
||||
if (!JsonNode.DeepEquals(originalNode, canonicalNode))
|
||||
{
|
||||
throw new Xunit.Sdk.XunitException($"Platform event sample must remain canonical.\nOriginal: {originalJson}\nCanonical: {canonicalJson}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertReportConsistency(OrchestratorEvent orchestratorEvent)
|
||||
{
|
||||
switch (orchestratorEvent.Payload)
|
||||
{
|
||||
case ReportReadyEventPayload ready:
|
||||
Assert.Equal(ready.ReportId, ready.Report.ReportId);
|
||||
Assert.Equal(ready.ScanId, ready.Report.ReportId);
|
||||
AssertDsseMatchesReport(ready.Dsse, ready.Report);
|
||||
Assert.False(string.IsNullOrWhiteSpace(ready.Links.Ui));
|
||||
Assert.False(string.IsNullOrWhiteSpace(ready.Links.Report));
|
||||
Assert.False(string.IsNullOrWhiteSpace(ready.Links.Attestation));
|
||||
break;
|
||||
case ScanCompletedEventPayload completed:
|
||||
Assert.Equal(completed.ReportId, completed.Report.ReportId);
|
||||
Assert.Equal(completed.ScanId, completed.Report.ReportId);
|
||||
AssertDsseMatchesReport(completed.Dsse, completed.Report);
|
||||
Assert.NotEmpty(completed.Findings);
|
||||
Assert.False(string.IsNullOrWhiteSpace(completed.Links.Ui));
|
||||
Assert.False(string.IsNullOrWhiteSpace(completed.Links.Report));
|
||||
Assert.False(string.IsNullOrWhiteSpace(completed.Links.Attestation));
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unexpected payload type {orchestratorEvent.Payload.GetType().Name}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertDsseMatchesReport(DsseEnvelopeDto? envelope, ReportDocumentDto report)
|
||||
{
|
||||
Assert.NotNull(envelope);
|
||||
var canonicalReportBytes = JsonSerializer.SerializeToUtf8Bytes(report, SerializerOptions);
|
||||
var expectedPayload = Convert.ToBase64String(canonicalReportBytes);
|
||||
Assert.Equal(expectedPayload, envelope.Payload);
|
||||
}
|
||||
|
||||
private static OrchestratorEvent DeserializeOrchestratorEvent(string json, string expectedKind)
|
||||
{
|
||||
var root = JsonNode.Parse(json)?.AsObject() ?? throw new InvalidOperationException("Sample JSON must not be null.");
|
||||
|
||||
var attributes = root["attributes"] is JsonObject attrObj && attrObj.Count > 0
|
||||
? attrObj.ToImmutableDictionary(
|
||||
pair => pair.Key,
|
||||
pair => pair.Value?.GetValue<string>() ?? string.Empty,
|
||||
StringComparer.Ordinal).ToImmutableSortedDictionary(StringComparer.Ordinal)
|
||||
: null;
|
||||
|
||||
OrchestratorEventScope? scope = null;
|
||||
if (root["scope"] is JsonObject scopeObj)
|
||||
{
|
||||
scope = new OrchestratorEventScope
|
||||
{
|
||||
Namespace = scopeObj["namespace"]?.GetValue<string>(),
|
||||
Repo = scopeObj["repo"]?.GetValue<string>() ?? string.Empty,
|
||||
Digest = scopeObj["digest"]?.GetValue<string>() ?? string.Empty,
|
||||
Component = scopeObj["component"]?.GetValue<string>(),
|
||||
Image = scopeObj["image"]?.GetValue<string>()
|
||||
};
|
||||
}
|
||||
|
||||
var payloadNode = root["payload"] ?? throw new InvalidOperationException("Payload node missing.");
|
||||
OrchestratorEventPayload payload = expectedKind switch
|
||||
{
|
||||
OrchestratorEventKinds.ScannerReportReady => payloadNode.Deserialize<ReportReadyEventPayload>(SerializerOptions)
|
||||
?? throw new InvalidOperationException("Unable to deserialize report ready payload."),
|
||||
OrchestratorEventKinds.ScannerScanCompleted => payloadNode.Deserialize<ScanCompletedEventPayload>(SerializerOptions)
|
||||
?? throw new InvalidOperationException("Unable to deserialize scan completed payload."),
|
||||
_ => throw new InvalidOperationException("Unexpected event kind.")
|
||||
};
|
||||
|
||||
return new OrchestratorEvent
|
||||
{
|
||||
EventId = Guid.Parse(root["eventId"]!.GetValue<string>()),
|
||||
Kind = root["kind"]!.GetValue<string>(),
|
||||
Version = root["version"]?.GetValue<int>() ?? 1,
|
||||
Tenant = root["tenant"]!.GetValue<string>(),
|
||||
OccurredAt = DateTimeOffset.Parse(root["occurredAt"]!.GetValue<string>(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
|
||||
RecordedAt = root["recordedAt"] is JsonValue recordedAtNode
|
||||
? DateTimeOffset.Parse(recordedAtNode.GetValue<string>(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)
|
||||
: null,
|
||||
Source = root["source"]?.GetValue<string>() ?? string.Empty,
|
||||
IdempotencyKey = root["idempotencyKey"]?.GetValue<string>() ?? string.Empty,
|
||||
CorrelationId = root["correlationId"]?.GetValue<string>(),
|
||||
TraceId = root["traceId"]?.GetValue<string>(),
|
||||
SpanId = root["spanId"]?.GetValue<string>(),
|
||||
Scope = scope,
|
||||
Attributes = attributes,
|
||||
Payload = payload
|
||||
};
|
||||
}
|
||||
|
||||
private static string LoadSample(string fileName)
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, fileName);
|
||||
Assert.True(File.Exists(path), $"Sample file not found at '{path}'.");
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user