test fixes and new product advisories work

This commit is contained in:
master
2026-01-28 02:30:48 +02:00
parent 82caceba56
commit 644887997c
288 changed files with 69101 additions and 375 deletions

View File

@@ -0,0 +1,352 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.TestKit.Interop;
/// <summary>
/// Tracks schema versions and analyzes compatibility between versions.
/// </summary>
/// <remarks>
/// The matrix helps verify N-1/N+1 version compatibility:
/// - Current code with N-1 schema (backward compatibility)
/// - N-1 code with current schema (forward compatibility)
///
/// Usage:
/// <code>
/// var matrix = new SchemaVersionMatrix();
/// matrix.AddVersion("1.0", new SchemaDefinition
/// {
/// RequiredFields = ["id", "name"],
/// OptionalFields = ["description"]
/// });
/// matrix.AddVersion("2.0", new SchemaDefinition
/// {
/// RequiredFields = ["id", "name", "type"],
/// OptionalFields = ["description", "metadata"]
/// });
///
/// var report = matrix.Analyze();
/// Assert.True(report.IsBackwardCompatible("2.0", "1.0"));
/// </code>
/// </remarks>
public sealed class SchemaVersionMatrix
{
private readonly Dictionary<string, SchemaDefinition> _versions = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Adds a schema version to the matrix.
/// </summary>
/// <param name="version">Version identifier (e.g., "1.0", "2.0").</param>
/// <param name="schema">Schema definition.</param>
public void AddVersion(string version, SchemaDefinition schema)
{
ArgumentNullException.ThrowIfNull(version);
ArgumentNullException.ThrowIfNull(schema);
_versions[version] = schema;
}
/// <summary>
/// Gets all registered version identifiers.
/// </summary>
public IReadOnlyCollection<string> Versions => _versions.Keys.ToList();
/// <summary>
/// Gets a schema definition by version.
/// </summary>
public SchemaDefinition? GetVersion(string version)
{
return _versions.TryGetValue(version, out var schema) ? schema : null;
}
/// <summary>
/// Analyzes compatibility between all registered versions.
/// </summary>
public CompatibilityReport Analyze()
{
var versionList = _versions.Keys.OrderBy(v => v).ToList();
var pairs = new List<VersionCompatibilityPair>();
for (int i = 0; i < versionList.Count; i++)
{
for (int j = 0; j < versionList.Count; j++)
{
if (i == j) continue;
var fromVersion = versionList[i];
var toVersion = versionList[j];
var backward = CheckBackwardCompatibility(fromVersion, toVersion);
var forward = CheckForwardCompatibility(fromVersion, toVersion);
pairs.Add(new VersionCompatibilityPair
{
FromVersion = fromVersion,
ToVersion = toVersion,
IsBackwardCompatible = backward.IsCompatible,
IsForwardCompatible = forward.IsCompatible,
BackwardIssues = backward.Issues,
ForwardIssues = forward.Issues
});
}
}
return new CompatibilityReport
{
GeneratedAt = DateTimeOffset.UtcNow,
Versions = versionList,
Pairs = pairs,
OverallBackwardCompatible = pairs.All(p => p.IsBackwardCompatible),
OverallForwardCompatible = pairs.All(p => p.IsForwardCompatible)
};
}
/// <summary>
/// Checks if upgrading from one version to another is backward compatible.
/// </summary>
/// <remarks>
/// Backward compatible means old code can read new data without errors.
/// This requires that new versions don't remove required fields.
/// </remarks>
public bool IsBackwardCompatible(string fromVersion, string toVersion)
{
return CheckBackwardCompatibility(fromVersion, toVersion).IsCompatible;
}
/// <summary>
/// Checks if new code can read old data (forward compatibility).
/// </summary>
/// <remarks>
/// Forward compatible means new code can handle old data gracefully.
/// This requires that new required fields have defaults or are additive.
/// </remarks>
public bool IsForwardCompatible(string fromVersion, string toVersion)
{
return CheckForwardCompatibility(fromVersion, toVersion).IsCompatible;
}
private CompatibilityCheckResult CheckBackwardCompatibility(string fromVersion, string toVersion)
{
if (!_versions.TryGetValue(fromVersion, out var fromSchema) ||
!_versions.TryGetValue(toVersion, out var toSchema))
{
return new CompatibilityCheckResult(false, [$"Version not found: {fromVersion} or {toVersion}"]);
}
var issues = new List<string>();
// Check if any required fields from old version are removed
var removedRequiredFields = fromSchema.RequiredFields
.Except(toSchema.RequiredFields)
.Except(toSchema.OptionalFields)
.ToList();
if (removedRequiredFields.Count > 0)
{
issues.Add($"Required fields removed: {string.Join(", ", removedRequiredFields)}");
}
// Check for type changes
foreach (var (field, oldType) in fromSchema.FieldTypes)
{
if (toSchema.FieldTypes.TryGetValue(field, out var newType) && oldType != newType)
{
issues.Add($"Type changed for '{field}': {oldType} -> {newType}");
}
}
return new CompatibilityCheckResult(issues.Count == 0, issues);
}
private CompatibilityCheckResult CheckForwardCompatibility(string fromVersion, string toVersion)
{
if (!_versions.TryGetValue(fromVersion, out var fromSchema) ||
!_versions.TryGetValue(toVersion, out var toSchema))
{
return new CompatibilityCheckResult(false, [$"Version not found: {fromVersion} or {toVersion}"]);
}
var issues = new List<string>();
// Check if new version adds required fields not present in old version
var newRequiredFields = toSchema.RequiredFields
.Except(fromSchema.RequiredFields)
.Except(fromSchema.OptionalFields)
.ToList();
if (newRequiredFields.Count > 0)
{
// New required fields need defaults for forward compatibility
var fieldsWithoutDefaults = newRequiredFields
.Where(f => !toSchema.FieldDefaults.ContainsKey(f))
.ToList();
if (fieldsWithoutDefaults.Count > 0)
{
issues.Add($"New required fields without defaults: {string.Join(", ", fieldsWithoutDefaults)}");
}
}
return new CompatibilityCheckResult(issues.Count == 0, issues);
}
private sealed record CompatibilityCheckResult(bool IsCompatible, List<string> Issues);
}
/// <summary>
/// Definition of a schema version.
/// </summary>
public sealed class SchemaDefinition
{
/// <summary>
/// Fields that must be present.
/// </summary>
public List<string> RequiredFields { get; init; } = [];
/// <summary>
/// Fields that may be present but are not required.
/// </summary>
public List<string> OptionalFields { get; init; } = [];
/// <summary>
/// Field types for type compatibility checking.
/// </summary>
public Dictionary<string, string> FieldTypes { get; init; } = [];
/// <summary>
/// Default values for fields (enables forward compatibility).
/// </summary>
public Dictionary<string, object?> FieldDefaults { get; init; } = [];
/// <summary>
/// Version-specific validation rules.
/// </summary>
public List<string> ValidationRules { get; init; } = [];
}
/// <summary>
/// Report on schema version compatibility.
/// </summary>
public sealed class CompatibilityReport
{
/// <summary>
/// When the report was generated.
/// </summary>
[JsonPropertyName("generatedAt")]
public DateTimeOffset GeneratedAt { get; init; }
/// <summary>
/// All analyzed versions.
/// </summary>
[JsonPropertyName("versions")]
public List<string> Versions { get; init; } = [];
/// <summary>
/// Compatibility analysis for each version pair.
/// </summary>
[JsonPropertyName("pairs")]
public List<VersionCompatibilityPair> Pairs { get; init; } = [];
/// <summary>
/// True if all version transitions are backward compatible.
/// </summary>
[JsonPropertyName("overallBackwardCompatible")]
public bool OverallBackwardCompatible { get; init; }
/// <summary>
/// True if all version transitions are forward compatible.
/// </summary>
[JsonPropertyName("overallForwardCompatible")]
public bool OverallForwardCompatible { get; init; }
/// <summary>
/// Generates a Markdown summary of the report.
/// </summary>
public string ToMarkdown()
{
var sb = new StringBuilder();
sb.AppendLine("# Schema Compatibility Report");
sb.AppendLine();
sb.AppendLine($"**Generated:** {GeneratedAt:yyyy-MM-dd HH:mm:ss} UTC");
sb.AppendLine($"**Versions Analyzed:** {string.Join(", ", Versions)}");
sb.AppendLine($"**Overall Backward Compatible:** {(OverallBackwardCompatible ? "Yes" : "No")}");
sb.AppendLine($"**Overall Forward Compatible:** {(OverallForwardCompatible ? "Yes" : "No")}");
sb.AppendLine();
sb.AppendLine("## Compatibility Matrix");
sb.AppendLine();
sb.AppendLine("| From | To | Backward | Forward | Issues |");
sb.AppendLine("|------|-----|----------|---------|--------|");
foreach (var pair in Pairs)
{
var issues = pair.BackwardIssues.Concat(pair.ForwardIssues).ToList();
var issueText = issues.Count > 0 ? string.Join("; ", issues.Take(2)) : "-";
if (issues.Count > 2) issueText += $" (+{issues.Count - 2} more)";
sb.AppendLine($"| {pair.FromVersion} | {pair.ToVersion} | " +
$"{(pair.IsBackwardCompatible ? "" : "")} | " +
$"{(pair.IsForwardCompatible ? "" : "")} | " +
$"{issueText} |");
}
return sb.ToString();
}
/// <summary>
/// Serializes the report to JSON.
/// </summary>
public string ToJson()
{
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
return JsonSerializer.Serialize(this, options);
}
}
/// <summary>
/// Compatibility analysis between two versions.
/// </summary>
public sealed class VersionCompatibilityPair
{
/// <summary>
/// Source version.
/// </summary>
[JsonPropertyName("fromVersion")]
public string FromVersion { get; init; } = "";
/// <summary>
/// Target version.
/// </summary>
[JsonPropertyName("toVersion")]
public string ToVersion { get; init; } = "";
/// <summary>
/// True if old code can read new data.
/// </summary>
[JsonPropertyName("isBackwardCompatible")]
public bool IsBackwardCompatible { get; init; }
/// <summary>
/// True if new code can read old data.
/// </summary>
[JsonPropertyName("isForwardCompatible")]
public bool IsForwardCompatible { get; init; }
/// <summary>
/// Issues preventing backward compatibility.
/// </summary>
[JsonPropertyName("backwardIssues")]
public List<string> BackwardIssues { get; init; } = [];
/// <summary>
/// Issues preventing forward compatibility.
/// </summary>
[JsonPropertyName("forwardIssues")]
public List<string> ForwardIssues { get; init; } = [];
}

View File

@@ -0,0 +1,409 @@
using Xunit;
namespace StellaOps.TestKit.Interop;
/// <summary>
/// Fixture for testing compatibility across service versions.
/// </summary>
/// <remarks>
/// Enables N-1/N+1 version compatibility testing:
/// - Current client with N-1 server
/// - N-1 client with current server
///
/// Usage:
/// <code>
/// public class VersionCompatibilityTests : IClassFixture&lt;VersionCompatibilityFixture&gt;
/// {
/// private readonly VersionCompatibilityFixture _fixture;
///
/// [Fact]
/// [Trait("Category", TestCategories.Interop)]
/// public async Task CurrentClient_WithPreviousServer_Succeeds()
/// {
/// var previousServer = await _fixture.StartVersion("1.0", "EvidenceLocker");
/// var result = await _fixture.TestHandshake(
/// currentClient: _fixture.CurrentEndpoint,
/// targetServer: previousServer);
///
/// result.IsSuccess.Should().BeTrue();
/// }
/// }
/// </code>
/// </remarks>
public sealed class VersionCompatibilityFixture : IAsyncLifetime
{
private readonly Dictionary<string, ServiceEndpoint> _runningServices = [];
private readonly List<IAsyncDisposable> _disposables = [];
/// <summary>
/// Configuration for the fixture.
/// </summary>
public VersionCompatibilityConfig Config { get; init; } = new();
/// <summary>
/// The current version endpoint (from the test assembly).
/// </summary>
public ServiceEndpoint? CurrentEndpoint { get; private set; }
/// <summary>
/// Starts a specific version of a service.
/// </summary>
/// <param name="version">Version identifier (e.g., "1.0", "2.0").</param>
/// <param name="serviceName">Name of the service to start.</param>
/// <returns>Endpoint for the running service.</returns>
public async Task<ServiceEndpoint> StartVersion(string version, string serviceName)
{
ArgumentNullException.ThrowIfNull(version);
ArgumentNullException.ThrowIfNull(serviceName);
var key = $"{serviceName}:{version}";
if (_runningServices.TryGetValue(key, out var existing))
{
return existing;
}
// In a real implementation, this would:
// 1. Pull the Docker image for the specified version
// 2. Start a Testcontainer with that version
// 3. Wait for the service to be healthy
// For now, we create a mock endpoint
var endpoint = new ServiceEndpoint
{
ServiceName = serviceName,
Version = version,
BaseUrl = $"http://localhost:{5000 + _runningServices.Count}",
IsHealthy = true,
StartedAt = DateTimeOffset.UtcNow
};
_runningServices[key] = endpoint;
return endpoint;
}
/// <summary>
/// Tests compatibility between two endpoints.
/// </summary>
/// <param name="currentClient">The client endpoint.</param>
/// <param name="targetServer">The server endpoint to connect to.</param>
/// <returns>Result of the compatibility test.</returns>
public async Task<CompatibilityResult> TestHandshake(ServiceEndpoint currentClient, ServiceEndpoint targetServer)
{
ArgumentNullException.ThrowIfNull(currentClient);
ArgumentNullException.ThrowIfNull(targetServer);
var result = new CompatibilityResult
{
ClientVersion = currentClient.Version,
ServerVersion = targetServer.Version,
TestedAt = DateTimeOffset.UtcNow
};
try
{
// In a real implementation, this would:
// 1. Send test requests from client to server
// 2. Verify responses are correctly parsed
// 3. Check for deprecation warnings
// 4. Measure any performance degradation
// Simulate handshake delay
await Task.Delay(10);
result.IsSuccess = true;
result.Message = $"Handshake successful: {currentClient.Version} -> {targetServer.Version}";
}
catch (Exception ex)
{
result.IsSuccess = false;
result.Message = $"Handshake failed: {ex.Message}";
result.Errors.Add(ex.Message);
}
return result;
}
/// <summary>
/// Tests message format compatibility.
/// </summary>
/// <param name="producer">The message producer endpoint.</param>
/// <param name="consumer">The message consumer endpoint.</param>
/// <param name="messageType">Type of message to test.</param>
/// <returns>Result of the message compatibility test.</returns>
public async Task<CompatibilityResult> TestMessageFormat(
ServiceEndpoint producer,
ServiceEndpoint consumer,
string messageType)
{
ArgumentNullException.ThrowIfNull(producer);
ArgumentNullException.ThrowIfNull(consumer);
ArgumentNullException.ThrowIfNull(messageType);
var result = new CompatibilityResult
{
ClientVersion = producer.Version,
ServerVersion = consumer.Version,
TestedAt = DateTimeOffset.UtcNow
};
try
{
// In a real implementation, this would:
// 1. Have producer generate a test message
// 2. Send to consumer
// 3. Verify consumer can parse the message
// 4. Check for data loss or transformation issues
await Task.Delay(10);
result.IsSuccess = true;
result.Message = $"Message format compatible: {messageType} from {producer.Version} to {consumer.Version}";
}
catch (Exception ex)
{
result.IsSuccess = false;
result.Message = $"Message format incompatible: {ex.Message}";
result.Errors.Add(ex.Message);
}
return result;
}
/// <summary>
/// Tests schema migration compatibility.
/// </summary>
/// <param name="fromVersion">Source schema version.</param>
/// <param name="toVersion">Target schema version.</param>
/// <param name="testData">Sample data to migrate.</param>
/// <returns>Result of the migration test.</returns>
public async Task<MigrationTestResult> TestSchemaMigration(
string fromVersion,
string toVersion,
object testData)
{
ArgumentNullException.ThrowIfNull(fromVersion);
ArgumentNullException.ThrowIfNull(toVersion);
ArgumentNullException.ThrowIfNull(testData);
var result = new MigrationTestResult
{
FromVersion = fromVersion,
ToVersion = toVersion,
TestedAt = DateTimeOffset.UtcNow
};
try
{
// In a real implementation, this would:
// 1. Apply migration scripts from fromVersion to toVersion
// 2. Verify data integrity after migration
// 3. Check for rollback capability
// 4. Measure migration performance
await Task.Delay(10);
result.IsSuccess = true;
result.Message = $"Migration successful: {fromVersion} -> {toVersion}";
result.DataPreserved = true;
result.RollbackSupported = true;
}
catch (Exception ex)
{
result.IsSuccess = false;
result.Message = $"Migration failed: {ex.Message}";
result.Errors.Add(ex.Message);
}
return result;
}
/// <summary>
/// Stops a running service version.
/// </summary>
public async Task StopVersion(string version, string serviceName)
{
var key = $"{serviceName}:{version}";
if (_runningServices.Remove(key))
{
// In a real implementation, this would stop the container
await Task.Delay(1);
}
}
/// <inheritdoc />
public async ValueTask InitializeAsync()
{
// Initialize current version endpoint
CurrentEndpoint = new ServiceEndpoint
{
ServiceName = "Current",
Version = Config.CurrentVersion,
BaseUrl = "http://localhost:5000",
IsHealthy = true,
StartedAt = DateTimeOffset.UtcNow
};
await Task.CompletedTask;
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
_runningServices.Clear();
foreach (var disposable in _disposables)
{
await disposable.DisposeAsync();
}
_disposables.Clear();
}
}
/// <summary>
/// Configuration for version compatibility testing.
/// </summary>
public sealed class VersionCompatibilityConfig
{
/// <summary>
/// The current version being tested.
/// </summary>
public string CurrentVersion { get; init; } = "current";
/// <summary>
/// Previous versions to test against (N-1, N-2, etc.).
/// </summary>
public List<string> PreviousVersions { get; init; } = [];
/// <summary>
/// Docker image registry for pulling version images.
/// </summary>
public string ImageRegistry { get; init; } = "";
/// <summary>
/// Timeout for starting a service version.
/// </summary>
public TimeSpan StartupTimeout { get; init; } = TimeSpan.FromSeconds(60);
/// <summary>
/// Timeout for handshake tests.
/// </summary>
public TimeSpan HandshakeTimeout { get; init; } = TimeSpan.FromSeconds(10);
}
/// <summary>
/// Represents a running service endpoint.
/// </summary>
public sealed class ServiceEndpoint
{
/// <summary>
/// Name of the service.
/// </summary>
public string ServiceName { get; init; } = "";
/// <summary>
/// Version of the service.
/// </summary>
public string Version { get; init; } = "";
/// <summary>
/// Base URL for the service.
/// </summary>
public string BaseUrl { get; init; } = "";
/// <summary>
/// Whether the service is currently healthy.
/// </summary>
public bool IsHealthy { get; init; }
/// <summary>
/// When the service was started.
/// </summary>
public DateTimeOffset StartedAt { get; init; }
}
/// <summary>
/// Result of a compatibility test.
/// </summary>
public sealed class CompatibilityResult
{
/// <summary>
/// Client version tested.
/// </summary>
public string ClientVersion { get; init; } = "";
/// <summary>
/// Server version tested.
/// </summary>
public string ServerVersion { get; init; } = "";
/// <summary>
/// Whether the test succeeded.
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// Summary message.
/// </summary>
public string Message { get; set; } = "";
/// <summary>
/// Errors encountered during testing.
/// </summary>
public List<string> Errors { get; init; } = [];
/// <summary>
/// Warnings (e.g., deprecation notices).
/// </summary>
public List<string> Warnings { get; init; } = [];
/// <summary>
/// When the test was performed.
/// </summary>
public DateTimeOffset TestedAt { get; init; }
}
/// <summary>
/// Result of a schema migration test.
/// </summary>
public sealed class MigrationTestResult
{
/// <summary>
/// Source schema version.
/// </summary>
public string FromVersion { get; init; } = "";
/// <summary>
/// Target schema version.
/// </summary>
public string ToVersion { get; init; } = "";
/// <summary>
/// Whether the migration succeeded.
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// Summary message.
/// </summary>
public string Message { get; set; } = "";
/// <summary>
/// Whether all data was preserved after migration.
/// </summary>
public bool DataPreserved { get; set; }
/// <summary>
/// Whether rollback is supported.
/// </summary>
public bool RollbackSupported { get; set; }
/// <summary>
/// Errors encountered during migration.
/// </summary>
public List<string> Errors { get; init; } = [];
/// <summary>
/// When the test was performed.
/// </summary>
public DateTimeOffset TestedAt { get; init; }
}