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; [Trait("Category", "Unit")] public sealed class FocasDriverDetailServiceTests { [Fact] public async Task GetAsync_returns_null_for_unknown_instance() { using var ctx = NewContext(); var svc = new FocasDriverDetailService(ctx); (await svc.GetAsync("missing", CancellationToken.None)).ShouldBeNull(); } [Fact] public async Task GetAsync_returns_null_for_non_focas_driver_type() { using var ctx = NewContext(); ctx.DriverInstances.Add(NewInstance("drv-modbus", "ModbusTcp", "{}")); await ctx.SaveChangesAsync(); var svc = new FocasDriverDetailService(ctx); (await svc.GetAsync("drv-modbus", CancellationToken.None)).ShouldBeNull(); } [Fact] public async Task GetAsync_parses_devices_tags_and_alarm_projection() { using var ctx = NewContext(); ctx.DriverInstances.Add(NewInstance("drv-focas", "Focas", """ { "Devices": [ { "HostAddress": "focas://10.20.30.40:8193", "Series": "ThirtyOne_i" } ], "Tags": [ { "Name": "Mode", "DeviceHostAddress": "focas://10.20.30.40:8193", "Address": "PARAM:3402", "DataType": "Int32", "Writable": false } ], "AlarmProjection": { "Enabled": true, "PollInterval": "00:00:05" }, "HandleRecycle": { "Enabled": true, "Interval": "01:00:00" } } """)); await ctx.SaveChangesAsync(); var svc = new FocasDriverDetailService(ctx); var detail = await svc.GetAsync("drv-focas", CancellationToken.None); detail.ShouldNotBeNull(); detail.ParseError.ShouldBeNull(); detail.Config.ShouldNotBeNull(); detail.Config.Devices!.Single().HostAddress.ShouldBe("focas://10.20.30.40:8193"); detail.Config.Devices!.Single().Series.ShouldBe("ThirtyOne_i"); detail.Config.Tags!.Single().Name.ShouldBe("Mode"); detail.Config.AlarmProjection!.Enabled.ShouldBeTrue(); detail.Config.HandleRecycle!.Enabled.ShouldBeTrue(); } [Fact] public async Task GetAsync_surfaces_parse_error_for_malformed_json() { using var ctx = NewContext(); ctx.DriverInstances.Add(NewInstance("drv-bad", "Focas", "{ not-valid-json")); await ctx.SaveChangesAsync(); var svc = new FocasDriverDetailService(ctx); var detail = await svc.GetAsync("drv-bad", CancellationToken.None); detail.ShouldNotBeNull(); detail.ParseError.ShouldNotBeNull(); detail.Config.ShouldBeNull(); } [Fact] public async Task GetAsync_joins_host_status_rows_for_the_instance() { using var ctx = NewContext(); ctx.DriverInstances.Add(NewInstance("drv-focas", "Focas", "{}")); ctx.DriverHostStatuses.Add(new DriverHostStatus { NodeId = "node-A", DriverInstanceId = "drv-focas", HostName = "focas://10.0.0.1:8193", State = DriverHostState.Running, StateChangedUtc = DateTime.UtcNow.AddMinutes(-5), LastSeenUtc = DateTime.UtcNow.AddSeconds(-3), }); await ctx.SaveChangesAsync(); var svc = new FocasDriverDetailService(ctx); var detail = await svc.GetAsync("drv-focas", CancellationToken.None); detail.ShouldNotBeNull(); detail.HostStatuses.Count.ShouldBe(1); detail.HostStatuses[0].HostName.ShouldBe("focas://10.0.0.1:8193"); detail.HostStatuses[0].State.ShouldBe("Running"); } [Fact] public async Task GetAsync_picks_latest_generation_when_multiple_rows_exist() { using var ctx = NewContext(); ctx.DriverInstances.Add(NewInstance("drv-focas", "Focas", "{\"Tags\":[]}", generationId: 1)); ctx.DriverInstances.Add(NewInstance("drv-focas", "Focas", """{"Tags":[{"Name":"later"}]}""", generationId: 2)); await ctx.SaveChangesAsync(); var svc = new FocasDriverDetailService(ctx); var detail = await svc.GetAsync("drv-focas", CancellationToken.None); detail.ShouldNotBeNull(); detail.Config!.Tags!.Single().Name.ShouldBe("later"); } private static DriverInstance NewInstance( string driverInstanceId, string driverType, string driverConfigJson, long generationId = 1) => new() { GenerationId = generationId, DriverInstanceId = driverInstanceId, ClusterId = "cluster-1", NamespaceId = "ns-1", Name = driverInstanceId, DriverType = driverType, DriverConfig = driverConfigJson, }; private static OtOpcUaConfigDbContext NewContext() { var opts = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; return new OtOpcUaConfigDbContext(opts); } }