feat(db): PendingSecuredWrite entity + migration + repository (T14b)

This commit is contained in:
Joseph Doherty
2026-06-18 02:09:31 -04:00
parent a0ce8b6c44
commit c799f41d53
10 changed files with 2477 additions and 0 deletions
@@ -0,0 +1,137 @@
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.SecuredWrites;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
using Xunit;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
/// <summary>
/// Integration tests for <see cref="SecuredWriteRepository"/> (M7 OPC UA / MxGateway
/// UX, Task T14b). Uses the same <see cref="MsSqlMigrationFixture"/> as the migration
/// tests so the EF reads/writes execute against the real <c>PendingSecuredWrites</c>
/// schema produced by the migration. Each test mints a fresh per-test
/// <c>SiteId</c>/<c>Status</c> suffix so tests neither collide nor require teardown.
/// Tests pair <see cref="SkippableFactAttribute"/> with <c>Skip.IfNot(...)</c> so the
/// runner reports them as Skipped (not Passed) when MSSQL is unreachable.
/// </summary>
public class SecuredWriteRepositoryTests : IClassFixture<MsSqlMigrationFixture>
{
private readonly MsSqlMigrationFixture _fixture;
public SecuredWriteRepositoryTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
}
[SkippableFact]
public async Task Lifecycle_AddGetUpdateQuery_RoundTrips()
{
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
var siteId = NewSiteId();
// Add a Pending row.
await using var context = CreateContext();
var repo = new SecuredWriteRepository(context);
var pending = NewRow(siteId, status: "Pending");
var id = await repo.AddAsync(pending);
Assert.True(id > 0, "AddAsync should return the store-generated identity.");
// Get returns it.
await using (var readContext = CreateContext())
{
var loaded = await new SecuredWriteRepository(readContext).GetAsync(id);
Assert.NotNull(loaded);
Assert.Equal(siteId, loaded!.SiteId);
Assert.Equal("Pending", loaded.Status);
Assert.Equal("conn-1", loaded.ConnectionName);
Assert.Equal("Plant.Tank.Setpoint", loaded.TagPath);
Assert.Equal("op.alice", loaded.OperatorUser);
Assert.Null(loaded.VerifierUser);
Assert.Null(loaded.DecidedAtUtc);
}
// Update to Approved.
await using (var updateContext = CreateContext())
{
var updateRepo = new SecuredWriteRepository(updateContext);
var toApprove = await updateRepo.GetAsync(id);
Assert.NotNull(toApprove);
toApprove!.Status = "Approved";
toApprove.VerifierUser = "ver.bob";
toApprove.VerifierComment = "looks good";
toApprove.DecidedAtUtc = DateTime.UtcNow;
await updateRepo.UpdateAsync(toApprove);
}
// Get reflects the update.
await using (var verifyContext = CreateContext())
{
var reloaded = await new SecuredWriteRepository(verifyContext).GetAsync(id);
Assert.NotNull(reloaded);
Assert.Equal("Approved", reloaded!.Status);
Assert.Equal("ver.bob", reloaded.VerifierUser);
Assert.Equal("looks good", reloaded.VerifierComment);
Assert.NotNull(reloaded.DecidedAtUtc);
}
// Query by status returns it.
await using (var queryContext = CreateContext())
{
var queryRepo = new SecuredWriteRepository(queryContext);
var byStatus = await queryRepo.QueryAsync(status: "Approved", siteId: siteId, skip: 0, take: 50);
Assert.Contains(byStatus, p => p.Id == id);
Assert.All(byStatus, p => Assert.Equal("Approved", p.Status));
// A non-matching status filter excludes the row.
var pendingPage = await queryRepo.QueryAsync(status: "Pending", siteId: siteId, skip: 0, take: 50);
Assert.DoesNotContain(pendingPage, p => p.Id == id);
}
}
[SkippableFact]
public async Task QueryAsync_NullFilters_MatchEveryStatusForSite()
{
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
var siteId = NewSiteId();
await using var context = CreateContext();
var repo = new SecuredWriteRepository(context);
var pendingId = await repo.AddAsync(NewRow(siteId, status: "Pending"));
var executedId = await repo.AddAsync(NewRow(siteId, status: "Executed"));
var all = await repo.QueryAsync(status: null, siteId: siteId, skip: 0, take: 50);
Assert.Contains(all, p => p.Id == pendingId);
Assert.Contains(all, p => p.Id == executedId);
}
// --- helpers ------------------------------------------------------------
private ScadaBridgeDbContext CreateContext()
{
var options = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
.UseSqlServer(_fixture.ConnectionString)
.Options;
return new ScadaBridgeDbContext(options);
}
private static string NewSiteId() =>
"site-t14b-" + Guid.NewGuid().ToString("N").Substring(0, 8);
private static PendingSecuredWrite NewRow(string siteId, string status) => new()
{
SiteId = siteId,
ConnectionName = "conn-1",
TagPath = "Plant.Tank.Setpoint",
ValueJson = "42.5",
ValueType = "Double",
Status = status,
OperatorUser = "op.alice",
OperatorComment = "raise setpoint",
SubmittedAtUtc = DateTime.UtcNow,
};
}