142 lines
6.9 KiB
C#
142 lines
6.9 KiB
C#
using Microsoft.Data.SqlClient;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Notifications;
|
|
|
|
/// <summary>
|
|
/// Direct-SQL seeding helper for the Notification Report page Playwright E2E tests
|
|
/// (Notification Outbox #21).
|
|
///
|
|
/// <para>
|
|
/// The Notification Report page reads the central <c>Notifications</c> table through the
|
|
/// <c>NotificationOutboxActor</c> singleton. Its query path
|
|
/// (<c>NotificationOutboxQueryRequest</c> → <c>NotificationOutboxRepository.QueryAsync</c>)
|
|
/// is a pure read-from-table projection with NO default time window — a row INSERTed
|
|
/// directly into <c>Notifications</c> surfaces on the page exactly as a site-ingested row
|
|
/// would. The actor's manual Retry / Discard handlers
|
|
/// (<c>RetryNotificationRequest</c> / <c>DiscardNotificationRequest</c>) likewise act
|
|
/// purely on the central row (load by id, flip <c>Status</c>, persist) — there is no
|
|
/// site relay on this path — so a directly-seeded <c>Parked</c> row is genuinely
|
|
/// retryable/discardable from central. This mirrors <see cref="SiteCalls.SiteCallDataSeeder"/>:
|
|
/// each test inserts its own row at setup and best-effort deletes it at teardown, keeping
|
|
/// the suite self-contained without touching <c>infra/mssql/seed-config.sql</c>.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Rows are tagged with a unique <c>ListName</c> marker derived from the test name + a GUID
|
|
/// so the teardown <c>DELETE</c> never touches rows the cluster itself produced.
|
|
/// <c>CreatedAt</c>/<c>SiteEnqueuedAt</c> are pinned to "now" so the page's default
|
|
/// (unconstrained) query window sees the row.
|
|
/// </para>
|
|
/// </summary>
|
|
internal static class NotificationDataSeeder
|
|
{
|
|
/// <summary>
|
|
/// Connection string for the running cluster's configuration DB.
|
|
/// Delegates to <see cref="PlaywrightDbConnection.ConnectionString"/>.
|
|
/// </summary>
|
|
public static string ConnectionString => PlaywrightDbConnection.ConnectionString;
|
|
|
|
/// <summary>
|
|
/// Inserts a single <c>Parked</c> row into the central <c>Notifications</c> table.
|
|
/// Populates every NOT NULL column (NotificationId, Type, ListName, Subject, Body,
|
|
/// Status, RetryCount, SourceSiteId, SiteEnqueuedAt, CreatedAt) and stamps the row with
|
|
/// the unique <paramref name="listNameMarker"/> so the test can filter to it and the
|
|
/// teardown can delete by it. <c>Status</c> is fixed to <c>Parked</c> — the only status
|
|
/// from which the page exposes Retry/Discard. Timestamps are pinned to "now" so the
|
|
/// page's default unconstrained query window sees the row.
|
|
/// </summary>
|
|
/// <param name="notificationId">GUID primary key (stored as its 36-char string form).</param>
|
|
/// <param name="listNameMarker">Unique per-run marker stored in <c>ListName</c>.</param>
|
|
/// <param name="subject">Subject text (searchable via the page's subject keyword box).</param>
|
|
/// <param name="sourceSite">Originating site identifier (e.g. <c>site-a</c>).</param>
|
|
/// <param name="retryCount">Retry count to display on the row.</param>
|
|
/// <param name="lastError">Optional last-error text shown beneath the subject.</param>
|
|
/// <param name="ct">Cancellation token.</param>
|
|
public static async Task InsertParkedNotificationAsync(
|
|
Guid notificationId,
|
|
string listNameMarker,
|
|
string subject,
|
|
string sourceSite,
|
|
int retryCount = 3,
|
|
string? lastError = "SMTP 451 transient failure (seeded)",
|
|
CancellationToken ct = default)
|
|
{
|
|
// NotificationId is the varchar(64) primary key; Type/Status are stored as
|
|
// varchar(32) (HasConversion<string>()). All NOT NULL columns are supplied;
|
|
// the nullable provenance columns (SourceNode, OriginExecutionId, …) are left
|
|
// to default to NULL, which the page renders as an em-dash.
|
|
const string sql = @"
|
|
INSERT INTO [Notifications]
|
|
([NotificationId], [Type], [ListName], [Subject], [Body], [Status], [RetryCount],
|
|
[LastError], [SourceSiteId], [SiteEnqueuedAt], [CreatedAt])
|
|
VALUES
|
|
(@id, @type, @listName, @subject, @body, @status, @retryCount,
|
|
@lastError, @sourceSite, @siteEnqueuedAt, @createdAt);";
|
|
|
|
var now = DateTimeOffset.UtcNow;
|
|
|
|
await using var connection = new SqlConnection(ConnectionString);
|
|
await connection.OpenAsync(ct);
|
|
await using var cmd = connection.CreateCommand();
|
|
cmd.CommandText = sql;
|
|
cmd.Parameters.AddWithValue("@id", notificationId.ToString());
|
|
cmd.Parameters.AddWithValue("@type", "Email");
|
|
cmd.Parameters.AddWithValue("@listName", listNameMarker);
|
|
cmd.Parameters.AddWithValue("@subject", subject);
|
|
cmd.Parameters.AddWithValue("@body", "Seeded notification body for Playwright E2E.");
|
|
cmd.Parameters.AddWithValue("@status", "Parked");
|
|
cmd.Parameters.AddWithValue("@retryCount", retryCount);
|
|
cmd.Parameters.AddWithValue("@lastError", (object?)lastError ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("@sourceSite", sourceSite);
|
|
cmd.Parameters.AddWithValue("@siteEnqueuedAt", now);
|
|
cmd.Parameters.AddWithValue("@createdAt", now);
|
|
|
|
await cmd.ExecuteNonQueryAsync(ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Best-effort cleanup. Deletes every <c>Notifications</c> row whose <c>ListName</c>
|
|
/// equals <paramref name="listNameMarker"/>. Swallows all errors — the marker carries a
|
|
/// per-run GUID so the rows are unique to this test run. A Retry that flips the row back
|
|
/// to <c>Pending</c> (and a subsequent dispatch sweep) does not change the <c>ListName</c>,
|
|
/// so the marker still matches whatever terminal state the row ends in.
|
|
/// </summary>
|
|
public static async Task DeleteByMarkerAsync(string listNameMarker, 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 [Notifications] WHERE [ListName] = @listName";
|
|
cmd.Parameters.AddWithValue("@listName", listNameMarker);
|
|
await cmd.ExecuteNonQueryAsync(ct);
|
|
}
|
|
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.
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// <see cref="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;
|
|
}
|
|
}
|
|
}
|