- Created `StellaOps.TestKit.Tests` project for unit tests related to determinism. - Implemented `DeterminismManifestTests` to validate deterministic output for canonical bytes and strings, file read/write operations, and error handling for invalid schema versions. - Added `SbomDeterminismTests` to ensure identical inputs produce consistent SBOMs across SPDX 3.0.1 and CycloneDX 1.6/1.7 formats, including parallel execution tests. - Updated project references in `StellaOps.Integration.Determinism` to include the new determinism testing library.
194 lines
5.8 KiB
C#
194 lines
5.8 KiB
C#
using System.Text.Json;
|
|
|
|
namespace StellaOps.TestKit.Connectors;
|
|
|
|
/// <summary>
|
|
/// Utility for updating test fixtures from live sources.
|
|
/// Enabled via STELLAOPS_UPDATE_FIXTURES=true environment variable.
|
|
/// </summary>
|
|
public sealed class FixtureUpdater
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private readonly string _fixturesDirectory;
|
|
private readonly bool _enabled;
|
|
|
|
public FixtureUpdater(string fixturesDirectory, HttpClient? httpClient = null)
|
|
{
|
|
_fixturesDirectory = fixturesDirectory;
|
|
_httpClient = httpClient ?? new HttpClient();
|
|
_enabled = Environment.GetEnvironmentVariable("STELLAOPS_UPDATE_FIXTURES") == "true";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if fixture updating is enabled.
|
|
/// </summary>
|
|
public bool IsEnabled => _enabled;
|
|
|
|
/// <summary>
|
|
/// Fetches and saves a fixture from a live URL.
|
|
/// Only runs when STELLAOPS_UPDATE_FIXTURES=true.
|
|
/// </summary>
|
|
public async Task UpdateFixtureFromUrlAsync(
|
|
string url,
|
|
string fixtureName,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (!_enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var response = await _httpClient.GetAsync(url, ct);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var content = await response.Content.ReadAsStringAsync(ct);
|
|
await SaveFixtureAsync(fixtureName, content, ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches JSON and saves as pretty-printed fixture.
|
|
/// </summary>
|
|
public async Task UpdateJsonFixtureFromUrlAsync(
|
|
string url,
|
|
string fixtureName,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (!_enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var response = await _httpClient.GetAsync(url, ct);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var json = await response.Content.ReadAsStringAsync(ct);
|
|
|
|
// Pretty-print for readability
|
|
var doc = JsonDocument.Parse(json);
|
|
var prettyJson = JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true });
|
|
|
|
await SaveFixtureAsync(fixtureName, prettyJson, ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves content to a fixture file.
|
|
/// </summary>
|
|
public async Task SaveFixtureAsync(
|
|
string fixtureName,
|
|
string content,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (!_enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var path = Path.Combine(_fixturesDirectory, fixtureName);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
|
await File.WriteAllTextAsync(path, content, ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves a canonical JSON snapshot.
|
|
/// </summary>
|
|
public async Task SaveExpectedSnapshotAsync<T>(
|
|
T model,
|
|
string snapshotName,
|
|
string? expectedDirectory = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (!_enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var canonical = StellaOps.Canonical.Json.CanonJson.Serialize(model);
|
|
var directory = expectedDirectory ?? Path.Combine(_fixturesDirectory, "..", "Expected");
|
|
var path = Path.Combine(directory, snapshotName);
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
|
await File.WriteAllTextAsync(path, canonical, ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compares current live data with existing fixture and reports drift.
|
|
/// </summary>
|
|
public async Task<FixtureDriftReport> CheckDriftAsync(
|
|
string url,
|
|
string fixtureName,
|
|
CancellationToken ct = default)
|
|
{
|
|
var fixturePath = Path.Combine(_fixturesDirectory, fixtureName);
|
|
if (!File.Exists(fixturePath))
|
|
{
|
|
return new FixtureDriftReport(fixtureName, true, "Fixture file does not exist");
|
|
}
|
|
|
|
var response = await _httpClient.GetAsync(url, ct);
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
return new FixtureDriftReport(fixtureName, false, $"Failed to fetch: {response.StatusCode}");
|
|
}
|
|
|
|
var liveContent = await response.Content.ReadAsStringAsync(ct);
|
|
var fixtureContent = await File.ReadAllTextAsync(fixturePath, ct);
|
|
|
|
// Try to normalize JSON for comparison
|
|
try
|
|
{
|
|
var liveDoc = JsonDocument.Parse(liveContent);
|
|
var fixtureDoc = JsonDocument.Parse(fixtureContent);
|
|
|
|
var liveNormalized = JsonSerializer.Serialize(liveDoc);
|
|
var fixtureNormalized = JsonSerializer.Serialize(fixtureDoc);
|
|
|
|
if (liveNormalized != fixtureNormalized)
|
|
{
|
|
return new FixtureDriftReport(fixtureName, true, "JSON content differs", liveContent);
|
|
}
|
|
|
|
return new FixtureDriftReport(fixtureName, false, "No drift detected");
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// Non-JSON content, compare raw
|
|
if (liveContent != fixtureContent)
|
|
{
|
|
return new FixtureDriftReport(fixtureName, true, "Content differs", liveContent);
|
|
}
|
|
|
|
return new FixtureDriftReport(fixtureName, false, "No drift detected");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Report of schema/content drift between live source and fixture.
|
|
/// </summary>
|
|
public sealed record FixtureDriftReport(
|
|
string FixtureName,
|
|
bool HasDrift,
|
|
string Message,
|
|
string? LiveContent = null);
|
|
|
|
/// <summary>
|
|
/// Configuration for fixture update operations.
|
|
/// </summary>
|
|
public sealed class FixtureUpdateConfig
|
|
{
|
|
/// <summary>
|
|
/// Mapping of fixture names to live URLs.
|
|
/// </summary>
|
|
public Dictionary<string, string> FixtureUrls { get; init; } = new();
|
|
|
|
/// <summary>
|
|
/// Headers to include in live requests.
|
|
/// </summary>
|
|
public Dictionary<string, string> RequestHeaders { get; init; } = new();
|
|
|
|
/// <summary>
|
|
/// Timeout for live requests.
|
|
/// </summary>
|
|
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
|
|
}
|