using Microsoft.Data.SqlClient; namespace ScadaLink.CentralUI.PlaywrightTests.SiteCalls; /// /// Direct-SQL seeding helper for the Site Calls page Playwright E2E tests /// (Site Call Audit #22, follow-ups Task 6). /// /// /// The Site Calls page reads the central SiteCalls table through the /// SiteCallAuditActor, which is a pure read-from-table mirror — so a row /// INSERTed directly into SiteCalls surfaces on the page exactly as a /// telemetry-ingested row would. Mirrors : /// each test inserts its own rows at setup and best-effort deletes them at /// teardown, keeping the suite self-contained without touching /// infra/mssql/seed-config.sql. /// /// /// /// Rows are tagged with a unique Target prefix derived from the test name /// + a GUID so the teardown DELETE never touches rows the cluster itself /// produced. CreatedAtUtc/UpdatedAtUtc are pinned to "now" so the /// page's default (unconstrained) query window sees the row. /// /// internal static class SiteCallDataSeeder { private const string DefaultConnectionString = "Server=localhost,1433;Database=ScadaLinkConfig;User Id=scadalink_app;Password=ScadaLink_Dev1#;TrustServerCertificate=true;Encrypt=false;Connect Timeout=5"; private const string EnvVar = "SCADALINK_PLAYWRIGHT_DB"; /// /// Connection string for the running cluster's configuration DB. Resolved /// from SCADALINK_PLAYWRIGHT_DB when set, otherwise the local docker /// dev defaults. /// public static string ConnectionString { get { var fromEnv = Environment.GetEnvironmentVariable(EnvVar); return string.IsNullOrWhiteSpace(fromEnv) ? DefaultConnectionString : fromEnv; } } /// /// Inserts a single row into the central SiteCalls table. Optional /// fields are nullable so a test can shape the row to the status/channel it /// needs for its grid assertions. TrackedOperationId is stored as the /// 36-character GUID string the entity mapping expects. /// public static async Task InsertSiteCallAsync( Guid trackedOperationId, string channel, string target, string sourceSite, string status, int retryCount, DateTime createdAtUtc, DateTime updatedAtUtc, string? lastError = null, int? httpStatus = null, DateTime? terminalAtUtc = null, CancellationToken ct = default) { const string sql = @" INSERT INTO [SiteCalls] ([TrackedOperationId], [Channel], [Target], [SourceSite], [Status], [RetryCount], [LastError], [HttpStatus], [CreatedAtUtc], [UpdatedAtUtc], [TerminalAtUtc], [IngestedAtUtc]) VALUES (@id, @channel, @target, @sourceSite, @status, @retryCount, @lastError, @httpStatus, @createdAtUtc, @updatedAtUtc, @terminalAtUtc, SYSUTCDATETIME());"; await using var connection = new SqlConnection(ConnectionString); await connection.OpenAsync(ct); await using var cmd = connection.CreateCommand(); cmd.CommandText = sql; cmd.Parameters.AddWithValue("@id", trackedOperationId.ToString()); cmd.Parameters.AddWithValue("@channel", channel); cmd.Parameters.AddWithValue("@target", target); cmd.Parameters.AddWithValue("@sourceSite", sourceSite); cmd.Parameters.AddWithValue("@status", status); cmd.Parameters.AddWithValue("@retryCount", retryCount); cmd.Parameters.AddWithValue("@lastError", (object?)lastError ?? DBNull.Value); cmd.Parameters.AddWithValue("@httpStatus", (object?)httpStatus ?? DBNull.Value); cmd.Parameters.AddWithValue("@createdAtUtc", createdAtUtc); cmd.Parameters.AddWithValue("@updatedAtUtc", updatedAtUtc); cmd.Parameters.AddWithValue("@terminalAtUtc", (object?)terminalAtUtc ?? DBNull.Value); await cmd.ExecuteNonQueryAsync(ct); } /// /// Best-effort cleanup. Deletes every SiteCalls row whose Target /// starts with . Swallows all errors — the /// prefix carries a per-run GUID so the rows are unique to this test run. /// public static async Task DeleteByTargetPrefixAsync(string targetPrefix, CancellationToken ct = default) { try { await using var connection = new SqlConnection(ConnectionString); await connection.OpenAsync(ct); await using var cmd = connection.CreateCommand(); cmd.CommandText = "DELETE FROM [SiteCalls] WHERE [Target] LIKE @prefix"; cmd.Parameters.AddWithValue("@prefix", targetPrefix + "%"); await cmd.ExecuteNonQueryAsync(ct); } catch { // Best-effort — the prefix carries a GUID so the rows are unique to // this test run and won't collide on the next pass. } } /// /// Probe whether the configuration DB is reachable. Tests gate their per-test /// setup on this so a downed cluster surfaces a clear message rather than an /// opaque . /// public static async Task IsAvailableAsync(CancellationToken ct = default) { try { await using var connection = new SqlConnection(ConnectionString); await connection.OpenAsync(ct); return true; } catch { return false; } } }