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