Files
git.stella-ops.org/src/__Libraries/StellaOps.TestKit/Connectors/FixtureUpdater.cs
master 491e883653 Add tests for SBOM generation determinism across multiple formats
- 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.
2025-12-24 00:36:14 +02:00

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);
}