Task #155 — TagService + TagsTab CRUD UI for Modbus tags
Closes the remaining loop on user-visible Modbus tag editing. Pre-#155 tags arrived only via SQL seeding or runtime ITagDiscovery; the Admin UI had no interactive surface for creating / editing / deleting tag rows. Changes: - TagService.cs (Admin/Services/) — CRUD wrapper around OtOpcUaConfigDbContext.Tags. ListAsync supports optional driver / equipment filters; CreateAsync auto-derives TagId; UpdateAsync persists editable fields; DeleteAsync removes the row. Mirrors the EquipmentService shape. - TagsTab.razor (Components/Pages/Clusters/) — list + filter + add/edit/remove form. The address/config editor is conditional: when the selected DriverInstance is Modbus, ModbusAddressEditor (#145) renders with live-parse preview; otherwise a generic JSON textarea (matches the DriversTab pattern from #147). Save-side serializes the address-string into TagConfig as `{"addressString":"..."}` JSON. - ClusterDetail.razor — new "Tags" tab in the cluster-detail nav strip + the routing switch. - Program.cs — TagService registered as a scoped DI service. Drive-by fix: ModbusDriverFactoryExtensions.CreateInstance promoted from internal to public — Admin.Tests was using it via reflection-friendly internal access that broke under the #153 logger overload addition. Public is the right access modifier anyway since the Server-side bootstrapper calls it from a different assembly. Drive-by fix #2: ModbusDriverConfigDto was missing MaxReadGap (#143) — surfaced by the #147 round-trip test that flips MaxReadGap=12 in the view model and asserts it lands on the resolved options. Added the field + binding line. Confirms #143's DriverConfig JSON binding was incomplete since the original commit; no production deployment configured this knob through JSON until now so the gap stayed hidden. Tests (4 new TagServiceTests): - Create_And_List_Surfaces_The_Tag — CreateAsync auto-assigns TagId; list returns the row. - List_Filters_By_DriverInstance — driver-scoped filter works. - Update_Persists_Editable_Fields — Name / DataType / AccessLevel / TagConfig all persist through Update. - Delete_Removes_The_Row — basic delete verification. 113 + 4 (TagService) + 2 (DriversTab round-trip restored after compile fix) = 119 Admin tests green. Solution build clean. Caveat: bUnit-style render tests for TagsTab still aren't included — Admin.Tests doesn't have bUnit set up. The TagService logic is fully covered; the razor component's parser/save glue is exercised by hand at runtime for now.
This commit is contained in:
96
tests/ZB.MOM.WW.OtOpcUa.Admin.Tests/TagServiceTests.cs
Normal file
96
tests/ZB.MOM.WW.OtOpcUa.Admin.Tests/TagServiceTests.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Admin.Services;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Admin.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// #155 — TagService CRUD round-trip coverage. Mirrors the EquipmentService test shape;
|
||||
/// uses EF Core InMemory so no SQL Server is required.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class TagServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Create_And_List_Surfaces_The_Tag()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
var svc = new TagService(ctx);
|
||||
|
||||
var created = await svc.CreateAsync(draftId: 1, NewTag("Temp"), TestContext.Current.CancellationToken);
|
||||
created.TagId.ShouldNotBeNullOrEmpty();
|
||||
created.GenerationId.ShouldBe(1);
|
||||
|
||||
var list = await svc.ListAsync(1, ct: TestContext.Current.CancellationToken);
|
||||
list.Count.ShouldBe(1);
|
||||
list[0].Name.ShouldBe("Temp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_Filters_By_DriverInstance()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
var svc = new TagService(ctx);
|
||||
await svc.CreateAsync(1, NewTag("a", driver: "drv-1"), TestContext.Current.CancellationToken);
|
||||
await svc.CreateAsync(1, NewTag("b", driver: "drv-2"), TestContext.Current.CancellationToken);
|
||||
await svc.CreateAsync(1, NewTag("c", driver: "drv-1"), TestContext.Current.CancellationToken);
|
||||
|
||||
var d1 = await svc.ListAsync(1, driverInstanceId: "drv-1", ct: TestContext.Current.CancellationToken);
|
||||
d1.Count.ShouldBe(2);
|
||||
d1.Select(t => t.Name).ShouldBe(new[] { "a", "c" }, ignoreOrder: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_Persists_Editable_Fields()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
var svc = new TagService(ctx);
|
||||
var t = await svc.CreateAsync(1, NewTag("Original"), TestContext.Current.CancellationToken);
|
||||
|
||||
t.Name = "Renamed";
|
||||
t.DataType = "Float";
|
||||
t.AccessLevel = TagAccessLevel.ReadWrite;
|
||||
t.TagConfig = "{\"addressString\":\"40001:F\"}";
|
||||
await svc.UpdateAsync(t, TestContext.Current.CancellationToken);
|
||||
|
||||
var fresh = (await svc.ListAsync(1, ct: TestContext.Current.CancellationToken))[0];
|
||||
fresh.Name.ShouldBe("Renamed");
|
||||
fresh.DataType.ShouldBe("Float");
|
||||
fresh.AccessLevel.ShouldBe(TagAccessLevel.ReadWrite);
|
||||
fresh.TagConfig.ShouldContain("40001:F");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_Removes_The_Row()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
var svc = new TagService(ctx);
|
||||
var t = await svc.CreateAsync(1, NewTag("Doomed"), TestContext.Current.CancellationToken);
|
||||
|
||||
await svc.DeleteAsync(t.TagRowId, TestContext.Current.CancellationToken);
|
||||
|
||||
(await svc.ListAsync(1, ct: TestContext.Current.CancellationToken)).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
private static Tag NewTag(string name, string driver = "drv-1") => new()
|
||||
{
|
||||
TagId = string.Empty, // CreateAsync auto-assigns
|
||||
DriverInstanceId = driver,
|
||||
Name = name,
|
||||
DataType = "Int32",
|
||||
AccessLevel = TagAccessLevel.Read,
|
||||
TagConfig = "{}",
|
||||
};
|
||||
|
||||
private static OtOpcUaConfigDbContext NewContext()
|
||||
{
|
||||
var opts = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new OtOpcUaConfigDbContext(opts);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user