feat(db): PendingSecuredWrite entity + migration + repository (T14b)
This commit is contained in:
+137
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user