156 lines
6.7 KiB
C#
156 lines
6.7 KiB
C#
using Microsoft.Data.SqlClient;
|
|
|
|
namespace ScadaLink.CentralUI.PlaywrightTests.Audit;
|
|
|
|
/// <summary>
|
|
/// Direct-SQL seeding helper for the Audit Log Playwright E2E tests (#23 M7-T16).
|
|
///
|
|
/// <para>
|
|
/// The Playwright suite runs against the live Docker cluster (the same one that
|
|
/// answers <c>http://localhost:9000</c>), which talks to the <c>ScadaLinkConfig</c>
|
|
/// database on <c>localhost:1433</c>. <c>infra/mssql/seed-config.sql</c> is off
|
|
/// limits per the task's strict rules, so each test inserts its own
|
|
/// <c>AuditLog</c> rows at setup time and best-effort deletes them at teardown.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Rows are tagged with a unique <c>Target</c> prefix derived from the test
|
|
/// name + a GUID so the teardown <c>DELETE</c> never touches rows the cluster
|
|
/// itself produced. The <c>OccurredAtUtc</c> is pinned to "now" so the default
|
|
/// <see cref="ScadaLink.CentralUI.Components.Audit.AuditTimeRangePreset.LastHour"/>
|
|
/// time-range filter still sees the row after Apply.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Connection string mirrors the Docker cluster's <c>scadalink_app</c> account
|
|
/// from <c>docker/central-node-a/appsettings.Central.json</c>, with the host
|
|
/// pointed at the host-exposed port (<c>localhost:1433</c>). The
|
|
/// <c>SCADALINK_PLAYWRIGHT_DB</c> env var lets CI override the connection
|
|
/// without recompiling.
|
|
/// </para>
|
|
/// </summary>
|
|
internal static class AuditDataSeeder
|
|
{
|
|
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";
|
|
|
|
/// <summary>
|
|
/// Connection string for the running cluster's configuration DB. Resolved
|
|
/// from <c>SCADALINK_PLAYWRIGHT_DB</c> when set, otherwise the local docker
|
|
/// dev defaults.
|
|
/// </summary>
|
|
public static string ConnectionString
|
|
{
|
|
get
|
|
{
|
|
var fromEnv = Environment.GetEnvironmentVariable(EnvVar);
|
|
return string.IsNullOrWhiteSpace(fromEnv) ? DefaultConnectionString : fromEnv;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts a single audit row into <c>AuditLog</c>. All optional fields are
|
|
/// nullable so individual tests can shape the row to whatever payload they
|
|
/// need for their drawer/grid assertions.
|
|
/// </summary>
|
|
public static async Task InsertAuditEventAsync(
|
|
Guid eventId,
|
|
DateTime occurredAtUtc,
|
|
string channel,
|
|
string kind,
|
|
string status,
|
|
string? sourceSiteId = null,
|
|
string? target = null,
|
|
string? actor = null,
|
|
Guid? correlationId = null,
|
|
Guid? executionId = null,
|
|
int? httpStatus = null,
|
|
int? durationMs = null,
|
|
string? errorMessage = null,
|
|
string? requestSummary = null,
|
|
string? responseSummary = null,
|
|
string? extra = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
const string sql = @"
|
|
INSERT INTO [AuditLog]
|
|
([EventId], [OccurredAtUtc], [IngestedAtUtc], [Channel], [Kind], [CorrelationId],
|
|
[ExecutionId], [SourceSiteId], [SourceInstanceId], [SourceScript], [Actor], [Target],
|
|
[Status], [HttpStatus], [DurationMs], [ErrorMessage], [ErrorDetail], [RequestSummary],
|
|
[ResponseSummary], [PayloadTruncated], [Extra], [ForwardState])
|
|
VALUES
|
|
(@eventId, @occurredAtUtc, SYSUTCDATETIME(), @channel, @kind, @correlationId,
|
|
@executionId, @sourceSiteId, NULL, NULL, @actor, @target,
|
|
@status, @httpStatus, @durationMs, @errorMessage, NULL, @requestSummary,
|
|
@responseSummary, 0, @extra, NULL);";
|
|
|
|
await using var connection = new SqlConnection(ConnectionString);
|
|
await connection.OpenAsync(ct);
|
|
await using var cmd = connection.CreateCommand();
|
|
cmd.CommandText = sql;
|
|
cmd.Parameters.AddWithValue("@eventId", eventId);
|
|
cmd.Parameters.AddWithValue("@occurredAtUtc", occurredAtUtc);
|
|
cmd.Parameters.AddWithValue("@channel", channel);
|
|
cmd.Parameters.AddWithValue("@kind", kind);
|
|
cmd.Parameters.AddWithValue("@correlationId", (object?)correlationId ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("@executionId", (object?)executionId ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("@sourceSiteId", (object?)sourceSiteId ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("@actor", (object?)actor ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("@target", (object?)target ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("@status", status);
|
|
cmd.Parameters.AddWithValue("@httpStatus", (object?)httpStatus ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("@durationMs", (object?)durationMs ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("@errorMessage", (object?)errorMessage ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("@requestSummary", (object?)requestSummary ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("@responseSummary", (object?)responseSummary ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("@extra", (object?)extra ?? DBNull.Value);
|
|
|
|
await cmd.ExecuteNonQueryAsync(ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Best-effort cleanup. Deletes every <c>AuditLog</c> row whose <c>Target</c>
|
|
/// starts with <paramref name="targetPrefix"/>. Swallows all errors — a
|
|
/// stuck row carrying a random GUID suffix does not collide with future
|
|
/// runs and tests should not fail teardown.
|
|
/// </summary>
|
|
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 [AuditLog] 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.
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Probe whether the configuration DB is reachable. Tests gate their
|
|
/// per-test setup on this; when the cluster is down the test fails with a
|
|
/// clear "MSSQL unavailable" message instead of an opaque SqlException.
|
|
/// </summary>
|
|
public static async Task<bool> IsAvailableAsync(CancellationToken ct = default)
|
|
{
|
|
try
|
|
{
|
|
await using var connection = new SqlConnection(ConnectionString);
|
|
await connection.OpenAsync(ct);
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|