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.
This commit is contained in:
193
src/__Libraries/StellaOps.TestKit/Connectors/FixtureUpdater.cs
Normal file
193
src/__Libraries/StellaOps.TestKit/Connectors/FixtureUpdater.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user