using System.Text.Json;
namespace StellaOps.TestKit.Connectors;
///
/// Utility for updating test fixtures from live sources.
/// Enabled via STELLAOPS_UPDATE_FIXTURES=true environment variable.
///
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";
}
///
/// Returns true if fixture updating is enabled.
///
public bool IsEnabled => _enabled;
///
/// Fetches and saves a fixture from a live URL.
/// Only runs when STELLAOPS_UPDATE_FIXTURES=true.
///
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);
}
///
/// Fetches JSON and saves as pretty-printed fixture.
///
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);
}
///
/// Saves content to a fixture file.
///
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);
}
///
/// Saves a canonical JSON snapshot.
///
public async Task SaveExpectedSnapshotAsync(
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);
}
///
/// Compares current live data with existing fixture and reports drift.
///
public async Task 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");
}
}
}
///
/// Report of schema/content drift between live source and fixture.
///
public sealed record FixtureDriftReport(
string FixtureName,
bool HasDrift,
string Message,
string? LiveContent = null);
///
/// Configuration for fixture update operations.
///
public sealed class FixtureUpdateConfig
{
///
/// Mapping of fixture names to live URLs.
///
public Dictionary FixtureUrls { get; init; } = new();
///
/// Headers to include in live requests.
///
public Dictionary RequestHeaders { get; init; } = new();
///
/// Timeout for live requests.
///
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
}