513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
154 lines
5.6 KiB
C#
154 lines
5.6 KiB
C#
using Microsoft.Data.Sqlite;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Tracking;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Tracking;
|
|
|
|
/// <summary>
|
|
/// LocalDb Phase 1 — the <c>OperationTracking</c> DDL is extracted out of
|
|
/// <see cref="OperationTrackingStore"/> so it can also be applied from the Host's
|
|
/// <c>AddZbLocalDb</c> onReady callback, which owns table creation for the consolidated
|
|
/// site database. These tests pin the extracted schema against the shape the store has
|
|
/// always created: the move must be behaviour-preserving.
|
|
/// </summary>
|
|
public class OperationTrackingSchemaTests
|
|
{
|
|
private static SqliteConnection OpenConnection()
|
|
{
|
|
var conn = new SqliteConnection("Data Source=:memory:");
|
|
conn.Open();
|
|
return conn;
|
|
}
|
|
|
|
private static List<(string Name, string Type, bool NotNull, bool Pk)> TableInfo(
|
|
SqliteConnection conn, string table)
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = $"PRAGMA table_info('{table}')";
|
|
using var reader = cmd.ExecuteReader();
|
|
var cols = new List<(string, string, bool, bool)>();
|
|
while (reader.Read())
|
|
{
|
|
cols.Add((reader.GetString(1), reader.GetString(2), reader.GetInt32(3) == 1,
|
|
reader.GetInt32(5) > 0));
|
|
}
|
|
|
|
return cols;
|
|
}
|
|
|
|
/// <summary>Verifies that applying the schema creates the table with the expected columns.</summary>
|
|
[Fact]
|
|
public void Apply_CreatesTableWithExpectedColumns()
|
|
{
|
|
using var conn = OpenConnection();
|
|
|
|
OperationTrackingSchema.Apply(conn);
|
|
|
|
var cols = TableInfo(conn, "OperationTracking");
|
|
Assert.Equal(
|
|
[
|
|
"TrackedOperationId", "Kind", "TargetSummary", "Status", "RetryCount",
|
|
"LastError", "HttpStatus", "CreatedAtUtc", "UpdatedAtUtc", "TerminalAtUtc",
|
|
"SourceInstanceId", "SourceScript", "SourceNode"
|
|
],
|
|
cols.Select(c => c.Name));
|
|
}
|
|
|
|
/// <summary>Verifies that the schema uses a text primary key.</summary>
|
|
[Fact]
|
|
public void Apply_UsesTextPrimaryKey()
|
|
{
|
|
using var conn = OpenConnection();
|
|
|
|
OperationTrackingSchema.Apply(conn);
|
|
|
|
// RegisterReplicated requires an explicit primary key and rejects BLOB
|
|
// columns; a TEXT PK is what makes this table replicate as-is.
|
|
var pk = Assert.Single(TableInfo(conn, "OperationTracking"), c => c.Pk);
|
|
Assert.Equal("TrackedOperationId", pk.Name);
|
|
Assert.Equal("TEXT", pk.Type);
|
|
}
|
|
|
|
/// <summary>Verifies that the schema has no blob columns.</summary>
|
|
[Fact]
|
|
public void Apply_HasNoBlobColumns()
|
|
{
|
|
using var conn = OpenConnection();
|
|
|
|
OperationTrackingSchema.Apply(conn);
|
|
|
|
// ILocalDb.RegisterReplicated throws on any column whose declared type
|
|
// contains BLOB (json_object cannot capture them).
|
|
Assert.DoesNotContain(
|
|
TableInfo(conn, "OperationTracking"),
|
|
c => c.Type.Contains("BLOB", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
/// <summary>Verifies that applying the schema creates the expected indexes.</summary>
|
|
[Fact]
|
|
public void Apply_CreatesExpectedIndexes()
|
|
{
|
|
using var conn = OpenConnection();
|
|
|
|
OperationTrackingSchema.Apply(conn);
|
|
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText =
|
|
"SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'OperationTracking' " +
|
|
"AND name NOT LIKE 'sqlite_%' ORDER BY name";
|
|
using var reader = cmd.ExecuteReader();
|
|
var indexes = new List<string>();
|
|
while (reader.Read()) indexes.Add(reader.GetString(0));
|
|
|
|
Assert.Equal(
|
|
["IX_OperationTracking_Status_Updated", "IX_OperationTracking_UpdatedAt"],
|
|
indexes);
|
|
}
|
|
|
|
/// <summary>Verifies that applying the schema repeatedly is idempotent.</summary>
|
|
[Fact]
|
|
public void Apply_IsIdempotent()
|
|
{
|
|
using var conn = OpenConnection();
|
|
|
|
OperationTrackingSchema.Apply(conn);
|
|
OperationTrackingSchema.Apply(conn);
|
|
|
|
Assert.Equal(13, TableInfo(conn, "OperationTracking").Count);
|
|
}
|
|
|
|
/// <summary>Verifies that applying the schema adds the SourceNode column to a legacy table that lacks it.</summary>
|
|
[Fact]
|
|
public void Apply_AddsSourceNodeToLegacyTableMissingIt()
|
|
{
|
|
using var conn = OpenConnection();
|
|
|
|
// A tracking DB created by a pre-SourceNode build. CREATE TABLE IF NOT
|
|
// EXISTS will not add the column, so Apply must ALTER it in — the
|
|
// additive-migration behaviour the store has today must survive extraction.
|
|
using (var legacy = conn.CreateCommand())
|
|
{
|
|
legacy.CommandText = """
|
|
CREATE TABLE OperationTracking (
|
|
TrackedOperationId TEXT NOT NULL PRIMARY KEY,
|
|
Kind TEXT NOT NULL,
|
|
TargetSummary TEXT NULL,
|
|
Status TEXT NOT NULL,
|
|
RetryCount INTEGER NOT NULL DEFAULT 0,
|
|
LastError TEXT NULL,
|
|
HttpStatus INTEGER NULL,
|
|
CreatedAtUtc TEXT NOT NULL,
|
|
UpdatedAtUtc TEXT NOT NULL,
|
|
TerminalAtUtc TEXT NULL,
|
|
SourceInstanceId TEXT NULL,
|
|
SourceScript TEXT NULL
|
|
);
|
|
""";
|
|
legacy.ExecuteNonQuery();
|
|
}
|
|
|
|
OperationTrackingSchema.Apply(conn);
|
|
|
|
Assert.Contains(TableInfo(conn, "OperationTracking"), c => c.Name == "SourceNode");
|
|
}
|
|
}
|