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.
115 lines
5.1 KiB
C#
115 lines
5.1 KiB
C#
using Microsoft.Data.Sqlite;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests;
|
|
|
|
/// <summary>
|
|
/// Pins <see cref="StoreAndForwardSchema"/> as the single owner of the
|
|
/// <c>sf_messages</c> DDL. The Host applies this to a LocalDb-managed connection
|
|
/// before <c>RegisterReplicated</c> installs the capture triggers, so the shape it
|
|
/// produces is the shape that replicates.
|
|
/// </summary>
|
|
public class StoreAndForwardSchemaTests
|
|
{
|
|
/// <summary>Verifies that applying the schema is idempotent and creates every column.</summary>
|
|
[Fact]
|
|
public void Apply_IsIdempotent_AndCreatesEveryColumn()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), $"sf-schema-{Guid.NewGuid():N}.db");
|
|
try
|
|
{
|
|
using var connection = new SqliteConnection($"Data Source={path}");
|
|
connection.Open();
|
|
|
|
StoreAndForwardSchema.Apply(connection);
|
|
StoreAndForwardSchema.Apply(connection); // second run must not throw
|
|
|
|
using var cmd = connection.CreateCommand();
|
|
cmd.CommandText = "SELECT name FROM pragma_table_info('sf_messages')";
|
|
var columns = new List<string>();
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read()) columns.Add(reader.GetString(0));
|
|
|
|
// 12 original + 4 additive
|
|
Assert.Equal(16, columns.Count);
|
|
Assert.Contains("last_attempt_at_ms", columns);
|
|
Assert.Contains("parent_execution_id", columns);
|
|
}
|
|
finally
|
|
{
|
|
SqliteConnection.ClearAllPools();
|
|
File.Delete(path);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The additive columns exist to upgrade a database created by an older build in
|
|
/// place. Asserting only against a freshly-created table would pass even if every
|
|
/// <c>ALTER</c> were deleted, because <c>CREATE TABLE</c> already lists them — so
|
|
/// this starts from the pre-upgrade shape and proves the ALTER path runs.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Apply_UpgradesALegacyTable_WithoutLosingRows()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), $"sf-schema-legacy-{Guid.NewGuid():N}.db");
|
|
try
|
|
{
|
|
using var connection = new SqliteConnection($"Data Source={path}");
|
|
connection.Open();
|
|
|
|
// The 12-column shape as it existed before execution_id / source_script /
|
|
// parent_execution_id / last_attempt_at_ms were added.
|
|
using (var legacy = connection.CreateCommand())
|
|
{
|
|
legacy.CommandText = """
|
|
CREATE TABLE sf_messages (
|
|
id TEXT PRIMARY KEY,
|
|
category INTEGER NOT NULL,
|
|
target TEXT NOT NULL,
|
|
payload_json TEXT NOT NULL,
|
|
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
max_retries INTEGER NOT NULL DEFAULT 50,
|
|
retry_interval_ms INTEGER NOT NULL DEFAULT 30000,
|
|
created_at TEXT NOT NULL,
|
|
last_attempt_at TEXT,
|
|
status INTEGER NOT NULL DEFAULT 0,
|
|
last_error TEXT,
|
|
origin_instance TEXT
|
|
);
|
|
INSERT INTO sf_messages (id, category, target, payload_json, created_at, last_attempt_at)
|
|
VALUES ('legacy-1', 0, 'target-a', '{}', '2026-01-01T00:00:00Z', '2026-01-02T03:04:05Z');
|
|
""";
|
|
legacy.ExecuteNonQuery();
|
|
}
|
|
|
|
StoreAndForwardSchema.Apply(connection);
|
|
|
|
using var check = connection.CreateCommand();
|
|
check.CommandText =
|
|
"SELECT COUNT(*) FROM pragma_table_info('sf_messages') WHERE name = 'last_attempt_at_ms'";
|
|
Assert.Equal(1, Convert.ToInt32(check.ExecuteScalar()));
|
|
|
|
// The pre-existing row survives and its epoch-ms sibling is backfilled from
|
|
// the authoritative text timestamp.
|
|
using var backfilled = connection.CreateCommand();
|
|
backfilled.CommandText =
|
|
"SELECT last_attempt_at_ms FROM sf_messages WHERE id = 'legacy-1'";
|
|
var ms = Convert.ToInt64(backfilled.ExecuteScalar());
|
|
var expected = DateTimeOffset.Parse("2026-01-02T03:04:05Z").ToUnixTimeMilliseconds();
|
|
|
|
// Tolerance of 1 ms, not sloppiness: the backfill derives epoch-ms from
|
|
// julianday(), whose double-precision day-fraction cannot represent every
|
|
// millisecond exactly, so it lands 1 ms low here. That rounding is
|
|
// pre-existing production behaviour carried over verbatim from
|
|
// StoreAndForwardStorage — the column is a sweep-ordering hint, with the
|
|
// ISO-8601 text column remaining authoritative for reads. Asserting exact
|
|
// equality would pin a precision the implementation never had.
|
|
Assert.InRange(ms, expected - 1, expected + 1);
|
|
}
|
|
finally
|
|
{
|
|
SqliteConnection.ClearAllPools();
|
|
File.Delete(path);
|
|
}
|
|
}
|
|
}
|