feat(mtconnect): typed AdminUI tag-config model (Task 17)
MTConnectTagConfigModel: pure FromJson/ToJson/Validate for the MTConnect tag TagConfig JSON blob, mirroring ModbusTagConfigModel/OpcUaClientTagConfigModel. Fields: fullName (DataItem@id, required), dataType (DriverDataType override, written as an enum name string), mtCategory/mtType/mtSubType/units (probe metadata), mtDevice/mtComponent (author context). Preserves unknown JSON keys across load->save (isHistorized/historianTagname and anything else). Guards against the enum-serialization trap (numeric dataType would fault the driver at deploy) both on write (always TagConfigJson.Set via enum name) and on read (Validate() rejects a dataType that was stored as a bare digit string, since Enum.TryParse silently accepts numeric text). AdminUI.csproj gains a ProjectReference to Driver.MTConnect.Contracts, matching the existing POCO-only driver-Contracts pattern used by the other six typed tag editors. Scope: pure model only (Task 17). The razor editor shell + TagConfigEditorMap/ TagConfigValidator registration is Task 18.
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
|
||||
public sealed class MTConnectTagConfigModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Roundtrip_preserves_unknown_keys_and_writes_enum_as_name()
|
||||
{
|
||||
var json = "{\"fullName\":\"dev1_pos\",\"dataType\":\"Float64\",\"somethingUnknown\":42}";
|
||||
var m = MTConnectTagConfigModel.FromJson(json);
|
||||
var outJson = m.ToJson();
|
||||
outJson.ShouldContain("\"somethingUnknown\":42");
|
||||
outJson.ShouldContain("\"dataType\":\"Float64\""); // name, never a number
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_blocks_blank_fullname()
|
||||
=> MTConnectTagConfigModel.FromJson("{}").Validate().ShouldNotBeNull();
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData("{}")]
|
||||
public void FromJson_returns_defaults_for_empty_input(string? json)
|
||||
{
|
||||
var m = MTConnectTagConfigModel.FromJson(json);
|
||||
|
||||
m.FullName.ShouldBe("");
|
||||
m.DataType.ShouldBe(DriverDataType.String);
|
||||
m.MtCategory.ShouldBeNull();
|
||||
m.MtType.ShouldBeNull();
|
||||
m.MtSubType.ShouldBeNull();
|
||||
m.Units.ShouldBeNull();
|
||||
m.MtDevice.ShouldBeNull();
|
||||
m.MtComponent.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Validate_blocks_missing_and_whitespace_fullname(string? fullName)
|
||||
{
|
||||
var m = new MTConnectTagConfigModel { FullName = fullName! };
|
||||
m.Validate().ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_returns_null_for_a_valid_model()
|
||||
{
|
||||
var m = new MTConnectTagConfigModel { FullName = "dev1_pos", DataType = DriverDataType.Float64 };
|
||||
m.Validate().ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToJson_emits_camelCase_keys_with_enum_name_and_all_fields()
|
||||
{
|
||||
var m = new MTConnectTagConfigModel
|
||||
{
|
||||
FullName = "dev1_partcount",
|
||||
DataType = DriverDataType.Int64,
|
||||
MtCategory = "EVENT",
|
||||
MtType = "PART_COUNT",
|
||||
MtSubType = "ACTUAL",
|
||||
Units = "COUNT",
|
||||
MtDevice = "Mill-01",
|
||||
MtComponent = "Controller",
|
||||
};
|
||||
|
||||
var json = m.ToJson();
|
||||
|
||||
json.ShouldContain("\"fullName\":\"dev1_partcount\"");
|
||||
json.ShouldContain("\"dataType\":\"Int64\"");
|
||||
json.ShouldContain("\"mtCategory\":\"EVENT\"");
|
||||
json.ShouldContain("\"mtType\":\"PART_COUNT\"");
|
||||
json.ShouldContain("\"mtSubType\":\"ACTUAL\"");
|
||||
json.ShouldContain("\"units\":\"COUNT\"");
|
||||
json.ShouldContain("\"mtDevice\":\"Mill-01\"");
|
||||
json.ShouldContain("\"mtComponent\":\"Controller\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToJson_never_emits_a_bare_numeric_dataType()
|
||||
{
|
||||
foreach (DriverDataType value in Enum.GetValues<DriverDataType>())
|
||||
{
|
||||
var json = new MTConnectTagConfigModel { FullName = "x", DataType = value }.ToJson();
|
||||
|
||||
// The enum-serialization trap: "dataType":8 would fault the driver at deploy (string-typed
|
||||
// factory DTOs). Assert the value after the key is a quoted name, not a bare digit.
|
||||
json.ShouldNotContain($"\"dataType\":{(int)value}");
|
||||
json.ShouldContain($"\"dataType\":\"{value}\"");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromJson_then_ToJson_preserves_unknown_keys_including_nested_object_and_isHistorized()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"fullName": "dev1_pos",
|
||||
"dataType": "Float64",
|
||||
"isHistorized": true,
|
||||
"historianTagname": "Mill01.Position",
|
||||
"nested": { "a": 1, "b": [1, 2, 3] }
|
||||
}
|
||||
""";
|
||||
|
||||
var roundTripped = MTConnectTagConfigModel.FromJson(json).ToJson();
|
||||
|
||||
roundTripped.ShouldContain("\"isHistorized\":true");
|
||||
roundTripped.ShouldContain("\"historianTagname\":\"Mill01.Position\"");
|
||||
roundTripped.ShouldContain("\"nested\":{\"a\":1,\"b\":[1,2,3]}");
|
||||
// and the modelled fields still round-trip alongside the preserved keys
|
||||
roundTripped.ShouldContain("\"fullName\":\"dev1_pos\"");
|
||||
roundTripped.ShouldContain("\"dataType\":\"Float64\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromJson_of_malformed_json_falls_back_to_defaults_rather_than_throwing()
|
||||
{
|
||||
var m = MTConnectTagConfigModel.FromJson("{not valid json");
|
||||
|
||||
m.FullName.ShouldBe("");
|
||||
m.DataType.ShouldBe(DriverDataType.String);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Round_trip_is_stable_for_a_fully_populated_model()
|
||||
{
|
||||
var m = new MTConnectTagConfigModel
|
||||
{
|
||||
FullName = "dev1_feedrate",
|
||||
DataType = DriverDataType.Float64,
|
||||
MtCategory = "SAMPLE",
|
||||
MtType = "PATH_FEEDRATE",
|
||||
MtSubType = "PROGRAMMED",
|
||||
Units = "MILLIMETER/SECOND",
|
||||
MtDevice = "Mill-01",
|
||||
MtComponent = "Path",
|
||||
};
|
||||
|
||||
var m2 = MTConnectTagConfigModel.FromJson(m.ToJson());
|
||||
|
||||
m2.FullName.ShouldBe(m.FullName);
|
||||
m2.DataType.ShouldBe(m.DataType);
|
||||
m2.MtCategory.ShouldBe(m.MtCategory);
|
||||
m2.MtType.ShouldBe(m.MtType);
|
||||
m2.MtSubType.ShouldBe(m.MtSubType);
|
||||
m2.Units.ShouldBe(m.Units);
|
||||
m2.MtDevice.ShouldBe(m.MtDevice);
|
||||
m2.MtComponent.ShouldBe(m.MtComponent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Optional_metadata_keys_are_omitted_when_blank_rather_than_persisted_empty()
|
||||
{
|
||||
var json = new MTConnectTagConfigModel { FullName = "dev1_pos", MtCategory = " " }.ToJson();
|
||||
|
||||
json.ShouldNotContain("mtCategory");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_rejects_a_dataType_that_was_stored_as_a_bare_number()
|
||||
{
|
||||
// Enum.TryParse (which TagConfigJson.GetEnum uses under the hood) silently accepts numeric text,
|
||||
// so a config that was ever hand-authored/migrated with a quoted-number dataType would otherwise
|
||||
// load into a plausible-looking enum value with no signal. Guard it explicitly.
|
||||
var m = MTConnectTagConfigModel.FromJson("{\"fullName\":\"dev1_pos\",\"dataType\":\"8\"}");
|
||||
|
||||
m.DataType.ShouldBe(DriverDataType.Float64); // 8 == DriverDataType.Float64 — parsed "successfully"
|
||||
m.Validate().ShouldNotBeNull();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user