171 lines
7.5 KiB
C#
171 lines
7.5 KiB
C#
using Microsoft.Data.SqlClient;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Audit;
|
|
|
|
/// <summary>
|
|
/// Direct-SQL seeding helper for the Configuration Audit Log Playwright E2E tests
|
|
/// (Wave 3). The <c>/audit/configuration</c> page reads the <c>AuditLogEntries</c>
|
|
/// table (entity <see cref="ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit.AuditLogEntry"/>)
|
|
/// via <c>CentralUiRepository</c> — a different table from the canonical
|
|
/// <c>AuditLog</c> table that <see cref="AuditDataSeeder"/> writes — so this seeder
|
|
/// targets <c>AuditLogEntries</c> specifically.
|
|
///
|
|
/// <para>
|
|
/// Mirrors <see cref="AuditDataSeeder"/>'s connection handling and best-effort
|
|
/// cleanup idiom: it opens a <see cref="SqlConnection"/> to
|
|
/// <see cref="PlaywrightDbConnection.ConnectionString"/> (the running Docker
|
|
/// cluster's <c>ScadaBridgeConfig</c> DB), inserts its own rows at setup time, and
|
|
/// best-effort deletes them at teardown.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Every seeded row carries <c>EntityType = marker</c> (a unique per-test prefix
|
|
/// derived from the test name + a GUID), so a UI filter on Entity Type isolates
|
|
/// exactly this run's rows for deterministic pagination at 50/page, and the
|
|
/// teardown <c>DELETE</c> never touches rows the cluster itself produced. Column
|
|
/// names/types are verified against <c>ScadaBridgeDbContextModelSnapshot</c>:
|
|
/// <c>AuditLogEntries</c> with <c>Id</c> (int identity), <c>User</c>/<c>Action</c>/
|
|
/// <c>EntityType</c>/<c>EntityId</c>/<c>EntityName</c> (nvarchar, required),
|
|
/// <c>AfterStateJson</c> (nvarchar(max), nullable), <c>Timestamp</c>
|
|
/// (datetimeoffset), <c>BundleImportId</c> (uniqueidentifier, nullable).
|
|
/// </para>
|
|
/// </summary>
|
|
internal static class ConfigAuditDataSeeder
|
|
{
|
|
/// <summary>
|
|
/// Connection string for the running cluster's configuration DB.
|
|
/// Delegates to <see cref="PlaywrightDbConnection.ConnectionString"/>.
|
|
/// </summary>
|
|
public static string ConnectionString => PlaywrightDbConnection.ConnectionString;
|
|
|
|
private const string InsertSql = @"
|
|
INSERT INTO [AuditLogEntries]
|
|
([User],[Action],[EntityType],[EntityId],[EntityName],[AfterStateJson],[Timestamp],[BundleImportId])
|
|
VALUES (@user, @action, @entityType, @entityId, @entityName, @afterState, @ts, @bundleId);";
|
|
|
|
/// <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. Mirrors
|
|
/// <see cref="AuditDataSeeder.IsAvailableAsync"/>.
|
|
/// </summary>
|
|
public static async Task<bool> IsAvailableAsync()
|
|
{
|
|
try
|
|
{
|
|
await using var connection = new SqlConnection(ConnectionString);
|
|
await connection.OpenAsync();
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seeds rows into <c>AuditLogEntries</c>. Inserts <paramref name="bulkCount"/>
|
|
/// bulk rows (all <c>EntityType = marker</c>, <c>BundleImportId = NULL</c>) plus
|
|
/// <paramref name="bundleRows"/> bundle rows (also <c>EntityType = marker</c>,
|
|
/// but <c>BundleImportId = bundleId</c>). Row index 0 of the bulk set carries a
|
|
/// > 1024-char <c>AfterStateJson</c> to drive the large-state modal on the
|
|
/// page; the rest carry a tiny JSON blob. Timestamps step back one second per
|
|
/// row from "now" so rows are recent and ordered.
|
|
/// </summary>
|
|
/// <param name="marker">Unique per-test prefix; isolates this run's rows.</param>
|
|
/// <param name="bundleId">Bundle import id stamped on the extra bundle rows.</param>
|
|
/// <param name="bulkCount">Number of bulk rows (default 55 — spans two pages at 50/page).</param>
|
|
/// <param name="bundleRows">Number of extra rows carrying the bundle import id (default 2).</param>
|
|
public static async Task SeedAsync(string marker, Guid bundleId, int bulkCount = 55, int bundleRows = 2)
|
|
{
|
|
await using var connection = new SqlConnection(ConnectionString);
|
|
await connection.OpenAsync();
|
|
|
|
// Bulk rows: all EntityType = marker, BundleImportId NULL. Row 0 gets a
|
|
// large AfterStateJson (> 1024 chars) to drive the large-state modal.
|
|
for (var i = 0; i < bulkCount; i++)
|
|
{
|
|
var afterState = i == 0
|
|
? "{\"blob\":\"" + new string('x', 1100) + "\"}"
|
|
: "{\"k\":\"v\"}";
|
|
|
|
await InsertRowAsync(
|
|
connection,
|
|
user: marker + "-user",
|
|
action: "Update",
|
|
entityType: marker,
|
|
entityId: marker + "-eid-" + i,
|
|
entityName: marker + "-" + i,
|
|
afterStateJson: afterState,
|
|
timestamp: DateTimeOffset.UtcNow.AddSeconds(-i),
|
|
bundleImportId: null);
|
|
}
|
|
|
|
// Bundle rows: EntityType = marker (so DeleteByMarkerAsync cleans them too)
|
|
// AND BundleImportId = bundleId, to drive the ?bundleImportId= chip drill-in.
|
|
for (var i = 0; i < bundleRows; i++)
|
|
{
|
|
await InsertRowAsync(
|
|
connection,
|
|
user: marker + "-user",
|
|
action: "Update",
|
|
entityType: marker,
|
|
entityId: marker + "-eid-bundle-" + i,
|
|
entityName: marker + "-bundle-" + i,
|
|
afterStateJson: "{\"k\":\"v\"}",
|
|
timestamp: DateTimeOffset.UtcNow.AddSeconds(-(bulkCount + i)),
|
|
bundleImportId: bundleId);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Best-effort cleanup. Deletes every <c>AuditLogEntries</c> row whose
|
|
/// <c>EntityType</c> equals <paramref name="marker"/>. Swallows all errors — the
|
|
/// marker carries a GUID so the rows are unique to this test run and tests
|
|
/// should not fail teardown. Mirrors
|
|
/// <see cref="AuditDataSeeder.DeleteByTargetPrefixAsync"/>.
|
|
/// </summary>
|
|
public static async Task DeleteByMarkerAsync(string marker)
|
|
{
|
|
try
|
|
{
|
|
await using var connection = new SqlConnection(ConnectionString);
|
|
await connection.OpenAsync();
|
|
await using var cmd = connection.CreateCommand();
|
|
cmd.CommandText = "DELETE FROM [AuditLogEntries] WHERE [EntityType] = @marker";
|
|
cmd.Parameters.AddWithValue("@marker", marker);
|
|
await cmd.ExecuteNonQueryAsync();
|
|
}
|
|
catch
|
|
{
|
|
// Best-effort — the marker carries a GUID so the rows are unique to
|
|
// this test run and won't collide on the next pass.
|
|
}
|
|
}
|
|
|
|
private static async Task InsertRowAsync(
|
|
SqlConnection connection,
|
|
string user,
|
|
string action,
|
|
string entityType,
|
|
string entityId,
|
|
string entityName,
|
|
string afterStateJson,
|
|
DateTimeOffset timestamp,
|
|
Guid? bundleImportId)
|
|
{
|
|
await using var cmd = connection.CreateCommand();
|
|
cmd.CommandText = InsertSql;
|
|
cmd.Parameters.AddWithValue("@user", user);
|
|
cmd.Parameters.AddWithValue("@action", action);
|
|
cmd.Parameters.AddWithValue("@entityType", entityType);
|
|
cmd.Parameters.AddWithValue("@entityId", entityId);
|
|
cmd.Parameters.AddWithValue("@entityName", entityName);
|
|
cmd.Parameters.AddWithValue("@afterState", (object?)afterStateJson ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("@ts", timestamp);
|
|
cmd.Parameters.AddWithValue("@bundleId", (object?)bundleImportId ?? DBNull.Value);
|
|
await cmd.ExecuteNonQueryAsync();
|
|
}
|
|
}
|