5ca4f8041d
#549/#551 made every typed editor judge its driver's enum KEYS against the runtime's own reader and stopped there. Every other reason a reader refuses a tag met no gate until deploy, or until the driver silently skipped the tag: an absent Modbus `region`, an `"address":"notanumber"`, an S7 `stringLength` of 300, a TwinCAT `dataType: "Structure"` (a VALID enum name the reader refuses), a Sql blob the parser cannot recognise as Sql at all, an MQTT `"qos":null`. Rather than hand-mirroring N more rules into N models — the drift #551 closed on the deploy side — each `<Driver>TagConfigModel.Validate()` now asks its driver's own parse whether it would read the AS-LOADED blob, via the new `TagConfigParseGate`. One authority for the verdict AND the wording. Two cautions are load-bearing and are pinned by tests: - The gate is the PARSE VERDICT, never "the inspector said something". FOCAS, MQTT and Calculation inspectors emit advisories for blobs their reader ACCEPTS, so a non-empty-inspection gate would make the modal stricter than the runtime and leave rows nobody can save (#520/#535). Inspect is consulted only to word a refusal that already happened. - It judges the blob the model was LOADED FROM, not the typed fields — the lenient load ("address":"notanumber" ⇒ 0) is what hides the defect. Same capture-the-source shape as S7's `_dataTypeMissingInSource`. Deliberately-stricter editor rules were surveyed and KEPT, not aligned: AbLegacy's arrayLength > 256 (reader clamps, but the un-clamped value still sets ArrayDimensions), FOCAS's unparseable address (#540), MQTT wildcard topics + Sparkplug segment chars, and Calculation's trigger/50 ms floors. They are now recorded fleet-wide in a table the tests assert is still accurate. Behaviour change: Modbus `{}` is now refused. Modbus was the one mapped editor with no required field, and `{}` is exactly what the modal persists for a config nobody touched — the driver skips it and it reads BadNodeIdUnknown forever with the deploy green. Its pinned test asserted the old, weaker behaviour and is rewritten with the rationale. Falsification: neutering only the Modbus consult turns 14 tests red, all Modbus-scoped; every #549 enum test stays green.
454 lines
18 KiB
C#
454 lines
18 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
|
|
|
/// <summary>
|
|
/// Covers the B2 Wave-B service prelude added to <see cref="RawTreeService"/> — the tag CRUD/import path
|
|
/// (WP4 manual entry + WP5 CSV import) and the driver/device config + merged-probe projections (WP3
|
|
/// modals). Reuses the shared <see cref="RawTreeTestDb"/> InMemory fixture.
|
|
/// <para>
|
|
/// As with the WP1 suite, the EF InMemory provider does not enforce <c>RowVersion</c> tokens, so the
|
|
/// optimistic-concurrency failure is exercised via the "row vanished under a stale handle" path.
|
|
/// </para>
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class RawTreeServiceWaveBTests
|
|
{
|
|
private static (RawTreeService Service, string DbName) Seeded()
|
|
{
|
|
var name = RawTreeTestDb.SeedFresh();
|
|
return (new RawTreeService(RawTreeTestDb.Factory(name)), name);
|
|
}
|
|
|
|
private static byte[] RowVersionOf(string dbName, Func<OtOpcUaConfigDbContext, byte[]> selector)
|
|
{
|
|
using var db = RawTreeTestDb.CreateNamed(dbName);
|
|
return selector(db);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A <c>TagConfig</c> the <b>Modbus</b> driver would actually read — <c>FolderedDeviceId</c> hangs off
|
|
/// the seed's Modbus driver, and the service runs the driver-typed <c>TagConfigValidator</c> on every
|
|
/// tag it writes. Gitea #554 made the Modbus editor ask the driver's own reader, which requires a
|
|
/// <c>region</c> and an in-range <c>address</c>, so the bare <c>{}</c> this helper used to default to
|
|
/// is a config the driver skips (<c>BadNodeIdUnknown</c>) and the service now refuses. None of the
|
|
/// tests below is about the config blob — they are tree-structure and concurrency tests — so they
|
|
/// take the minimal readable one.
|
|
/// </summary>
|
|
private const string ModbusConfig = """{"region":"HoldingRegisters","address":1}""";
|
|
|
|
private static RawTagInput Tag(string name, string dataType = "Float", string tagConfig = ModbusConfig) =>
|
|
new(name, dataType, TagAccessLevel.ReadWrite, WriteIdempotent: false, PollGroupId: null, tagConfig);
|
|
|
|
/// <summary>
|
|
/// The same, for the <b>S7</b> device — <c>RootDeviceId</c> hangs off the seed's S7 driver, which
|
|
/// requires BOTH an <c>address</c> and a present <c>dataType</c> key (Gitea #551).
|
|
/// </summary>
|
|
/// <param name="name">The tag name.</param>
|
|
/// <returns>The import-ready tag input.</returns>
|
|
private static RawTagInput S7Tag(string name) =>
|
|
Tag(name, tagConfig: """{"address":"DB1.DBW0","dataType":"Int16"}""");
|
|
|
|
// -------------------------------------------------------------------------------- CreateTag (WP4)
|
|
|
|
[Fact]
|
|
public async Task CreateTag_under_group_succeeds_and_returns_new_id()
|
|
{
|
|
var (service, dbName) = Seeded();
|
|
|
|
var result = await service.CreateTagAsync(
|
|
RawTreeTestDb.FolderedDeviceId, RawTreeTestDb.RootGroupId, Tag("torque"));
|
|
|
|
result.Ok.ShouldBeTrue();
|
|
result.CreatedId.ShouldNotBeNull();
|
|
|
|
using var db = RawTreeTestDb.CreateNamed(dbName);
|
|
var tag = db.Tags.Single(t => t.TagId == result.CreatedId);
|
|
tag.Name.ShouldBe("torque");
|
|
tag.DeviceId.ShouldBe(RawTreeTestDb.FolderedDeviceId);
|
|
tag.TagGroupId.ShouldBe(RawTreeTestDb.RootGroupId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateTag_collapses_whitespace_pollgroup_to_null()
|
|
{
|
|
var (service, dbName) = Seeded();
|
|
|
|
var result = await service.CreateTagAsync(
|
|
RawTreeTestDb.FolderedDeviceId, null,
|
|
new RawTagInput("flow", "Float", TagAccessLevel.Read, false, " ", ModbusConfig));
|
|
|
|
result.Ok.ShouldBeTrue();
|
|
using var db = RawTreeTestDb.CreateNamed(dbName);
|
|
db.Tags.Single(t => t.TagId == result.CreatedId).PollGroupId.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateTag_rejects_sibling_name_collision()
|
|
{
|
|
var (service, _) = Seeded();
|
|
|
|
// "speed" already exists under RootGroup.
|
|
var dup = await service.CreateTagAsync(RawTreeTestDb.FolderedDeviceId, RawTreeTestDb.RootGroupId, Tag("speed"));
|
|
|
|
dup.Ok.ShouldBeFalse();
|
|
dup.Error!.ShouldContain("already exists");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateTag_rejects_malformed_tagconfig_json()
|
|
{
|
|
var (service, _) = Seeded();
|
|
|
|
var bad = await service.CreateTagAsync(
|
|
RawTreeTestDb.FolderedDeviceId, null, Tag("pressure", tagConfig: "{not json"));
|
|
|
|
bad.Ok.ShouldBeFalse();
|
|
bad.Error!.ShouldContain("JSON");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateTag_rejects_invalid_name_segment()
|
|
{
|
|
var (service, _) = Seeded();
|
|
|
|
var bad = await service.CreateTagAsync(RawTreeTestDb.FolderedDeviceId, null, Tag("a/b"));
|
|
|
|
bad.Ok.ShouldBeFalse();
|
|
bad.Error!.ShouldContain("/");
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------- UpdateTag (WP4)
|
|
|
|
[Fact]
|
|
public async Task UpdateTag_edits_fields_last_write_wins()
|
|
{
|
|
var (service, dbName) = Seeded();
|
|
var rv = RowVersionOf(dbName, db => db.Tags.Single(t => t.TagId == RawTreeTestDb.PlainTagId).RowVersion);
|
|
|
|
var result = await service.UpdateTagAsync(
|
|
RawTreeTestDb.PlainTagId,
|
|
// The unknown "scale" key rides a readable Modbus config: this test is about field edits and
|
|
// unknown-key survival, and since Gitea #554 the service's driver-typed gate refuses a blob the
|
|
// Modbus reader would skip.
|
|
new RawTagInput(
|
|
"running", "Boolean", TagAccessLevel.ReadWrite, true, "pg1",
|
|
"""{"region":"HoldingRegisters","address":1,"scale":2}"""),
|
|
rv);
|
|
|
|
result.Ok.ShouldBeTrue();
|
|
using var db = RawTreeTestDb.CreateNamed(dbName);
|
|
var tag = db.Tags.Single(t => t.TagId == RawTreeTestDb.PlainTagId);
|
|
tag.Name.ShouldBe("running");
|
|
tag.AccessLevel.ShouldBe(TagAccessLevel.ReadWrite);
|
|
tag.WriteIdempotent.ShouldBeTrue();
|
|
tag.PollGroupId.ShouldBe("pg1");
|
|
tag.TagConfig.ShouldContain("scale");
|
|
// Identity location preserved.
|
|
tag.DeviceId.ShouldBe(RawTreeTestDb.FolderedDeviceId);
|
|
tag.TagGroupId.ShouldBe(RawTreeTestDb.RootGroupId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateTag_rejects_rename_collision_excluding_self()
|
|
{
|
|
var (service, dbName) = Seeded();
|
|
// Rename "state" (PlainTag) to "speed" — a sibling under the same group ⇒ collision.
|
|
var rv = RowVersionOf(dbName, db => db.Tags.Single(t => t.TagId == RawTreeTestDb.PlainTagId).RowVersion);
|
|
|
|
var result = await service.UpdateTagAsync(RawTreeTestDb.PlainTagId, Tag("speed"), rv);
|
|
|
|
result.Ok.ShouldBeFalse();
|
|
result.Error!.ShouldContain("already exists");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateTag_allows_renaming_to_same_name_excluding_self()
|
|
{
|
|
var (service, dbName) = Seeded();
|
|
var rv = RowVersionOf(dbName, db => db.Tags.Single(t => t.TagId == RawTreeTestDb.PlainTagId).RowVersion);
|
|
|
|
// Keeping its own name "state" must not trip the excl-self uniqueness check.
|
|
var result = await service.UpdateTagAsync(RawTreeTestDb.PlainTagId, Tag("state", "Boolean"), rv);
|
|
|
|
result.Ok.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateTag_fails_when_row_vanished_concurrently()
|
|
{
|
|
var (service, dbName) = Seeded();
|
|
var rv = RowVersionOf(dbName, db => db.Tags.Single(t => t.TagId == RawTreeTestDb.PlainTagId).RowVersion);
|
|
|
|
using (var db = RawTreeTestDb.CreateNamed(dbName))
|
|
{
|
|
db.Tags.Remove(db.Tags.Single(t => t.TagId == RawTreeTestDb.PlainTagId));
|
|
db.SaveChanges();
|
|
}
|
|
|
|
var result = await service.UpdateTagAsync(RawTreeTestDb.PlainTagId, Tag("whatever", "Boolean"), rv);
|
|
|
|
result.Ok.ShouldBeFalse();
|
|
result.Error.ShouldNotBeNull();
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------ ImportTags (WP5)
|
|
|
|
[Fact]
|
|
public async Task ImportTags_autocreates_nested_groups_and_reuses_them()
|
|
{
|
|
var (service, dbName) = Seeded();
|
|
|
|
var rows = new[]
|
|
{
|
|
new RawTagImportRow("Line/A", S7Tag("t1")),
|
|
new RawTagImportRow("Line/A", S7Tag("t2")), // reuses Line + Line/A
|
|
new RawTagImportRow("Line/B", S7Tag("t3")), // reuses Line, new B
|
|
};
|
|
|
|
var result = await service.ImportTagsAsync(RawTreeTestDb.RootDeviceId, rows);
|
|
|
|
result.Errors.ShouldBeEmpty();
|
|
result.Inserted.ShouldBe(3);
|
|
|
|
using var db = RawTreeTestDb.CreateNamed(dbName);
|
|
var line = db.TagGroups.Single(g => g.DeviceId == RawTreeTestDb.RootDeviceId && g.ParentTagGroupId == null && g.Name == "Line");
|
|
var groupA = db.TagGroups.Single(g => g.ParentTagGroupId == line.TagGroupId && g.Name == "A");
|
|
var groupB = db.TagGroups.Single(g => g.ParentTagGroupId == line.TagGroupId && g.Name == "B");
|
|
db.Tags.Count(t => t.TagGroupId == groupA.TagGroupId).ShouldBe(2); // Line/A reused, not duplicated
|
|
db.Tags.Count(t => t.TagGroupId == groupB.TagGroupId).ShouldBe(1);
|
|
// Exactly one "Line" group created despite three references.
|
|
db.TagGroups.Count(g => g.DeviceId == RawTreeTestDb.RootDeviceId && g.Name == "Line").ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ImportTags_places_tag_at_device_root_when_path_blank()
|
|
{
|
|
var (service, dbName) = Seeded();
|
|
|
|
var result = await service.ImportTagsAsync(
|
|
RawTreeTestDb.RootDeviceId, new[] { new RawTagImportRow(null, S7Tag("rootTag")) });
|
|
|
|
result.Inserted.ShouldBe(1);
|
|
using var db = RawTreeTestDb.CreateNamed(dbName);
|
|
db.Tags.Single(t => t.Name == "rootTag").TagGroupId.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ImportTags_all_or_nothing_on_a_bad_row_inserts_nothing()
|
|
{
|
|
var (service, dbName) = Seeded();
|
|
|
|
var rows = new[]
|
|
{
|
|
new RawTagImportRow("Grp", S7Tag("good1")),
|
|
new RawTagImportRow("Grp", Tag("bad", tagConfig: "{oops")), // malformed JSON ⇒ error
|
|
new RawTagImportRow("Grp", S7Tag("good2")),
|
|
};
|
|
|
|
var result = await service.ImportTagsAsync(RawTreeTestDb.RootDeviceId, rows);
|
|
|
|
result.Inserted.ShouldBe(0);
|
|
result.Errors.ShouldNotBeEmpty();
|
|
result.Errors[0].ShouldContain("Row 2");
|
|
|
|
// Nothing committed — not the two good tags, not the auto-created group.
|
|
using var db = RawTreeTestDb.CreateNamed(dbName);
|
|
db.Tags.Any(t => t.Name == "good1" || t.Name == "good2").ShouldBeFalse();
|
|
db.TagGroups.Any(g => g.DeviceId == RawTreeTestDb.RootDeviceId && g.Name == "Grp").ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ImportTags_detects_intra_batch_duplicate()
|
|
{
|
|
var (service, _) = Seeded();
|
|
|
|
var rows = new[]
|
|
{
|
|
new RawTagImportRow("Grp", S7Tag("dup")),
|
|
new RawTagImportRow("Grp", S7Tag("dup")), // same path + name within the batch
|
|
};
|
|
|
|
var result = await service.ImportTagsAsync(RawTreeTestDb.RootDeviceId, rows);
|
|
|
|
result.Inserted.ShouldBe(0);
|
|
result.Errors.ShouldContain(e => e.Contains("Row 2") && e.Contains("already exists"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ImportTags_detects_collision_with_existing_tag()
|
|
{
|
|
var (service, _) = Seeded();
|
|
|
|
// "temperature" already sits at the device root of RootDevice.
|
|
var result = await service.ImportTagsAsync(
|
|
RawTreeTestDb.RootDeviceId, new[] { new RawTagImportRow(null, S7Tag("temperature")) });
|
|
|
|
result.Inserted.ShouldBe(0);
|
|
result.Errors.ShouldContain(e => e.Contains("already exists"));
|
|
}
|
|
|
|
// ----------------------------------------------------------------------- Driver config (WP3)
|
|
|
|
[Fact]
|
|
public async Task LoadDriverForEdit_projects_fields()
|
|
{
|
|
var (service, _) = Seeded();
|
|
|
|
var dto = await service.LoadDriverForEditAsync(RawTreeTestDb.FolderedDriverId);
|
|
|
|
dto.ShouldNotBeNull();
|
|
dto!.Name.ShouldBe("modbus-1");
|
|
dto.DriverType.ShouldBe("Modbus");
|
|
dto.DriverConfig.ShouldBe("{}");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LoadDriverForEdit_returns_null_when_missing()
|
|
{
|
|
var (service, _) = Seeded();
|
|
(await service.LoadDriverForEditAsync("DRV-nope")).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateDriver_edits_name_and_config_preserving_type()
|
|
{
|
|
var (service, dbName) = Seeded();
|
|
var rv = RowVersionOf(dbName, db => db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.FolderedDriverId).RowVersion);
|
|
|
|
var result = await service.UpdateDriverAsync(
|
|
RawTreeTestDb.FolderedDriverId, "modbus-main", "{\"Host\":\"10.0.0.1\"}", "{\"x\":1}", rv);
|
|
|
|
result.Ok.ShouldBeTrue();
|
|
using var db = RawTreeTestDb.CreateNamed(dbName);
|
|
var drv = db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.FolderedDriverId);
|
|
drv.Name.ShouldBe("modbus-main");
|
|
drv.DriverConfig.ShouldContain("Host");
|
|
drv.ResilienceConfig.ShouldBe("{\"x\":1}");
|
|
drv.DriverType.ShouldBe("Modbus"); // immutable
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateDriver_rejects_sibling_name_collision()
|
|
{
|
|
var (service, dbName) = Seeded();
|
|
// Two cluster-root vs foldered drivers differ in scope; create a sibling in the same folder to collide.
|
|
await service.CreateDriverAsync(RawTreeTestDb.ClusterId, RawTreeTestDb.RootFolderId, "modbus-2", "Modbus", "{}");
|
|
var rv = RowVersionOf(dbName, db => db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.FolderedDriverId).RowVersion);
|
|
|
|
var result = await service.UpdateDriverAsync(RawTreeTestDb.FolderedDriverId, "modbus-2", "{}", null, rv);
|
|
|
|
result.Ok.ShouldBeFalse();
|
|
result.Error!.ShouldContain("already exists");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateDriver_fails_when_row_vanished_concurrently()
|
|
{
|
|
var (service, dbName) = Seeded();
|
|
var rv = RowVersionOf(dbName, db => db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.FolderedDriverId).RowVersion);
|
|
|
|
using (var db = RawTreeTestDb.CreateNamed(dbName))
|
|
{
|
|
db.Devices.RemoveRange(db.Devices.Where(d => d.DriverInstanceId == RawTreeTestDb.FolderedDriverId));
|
|
db.Tags.RemoveRange(db.Tags.Where(t => t.DeviceId == RawTreeTestDb.FolderedDeviceId));
|
|
db.TagGroups.RemoveRange(db.TagGroups.Where(g => g.DeviceId == RawTreeTestDb.FolderedDeviceId));
|
|
db.DriverInstances.Remove(db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.FolderedDriverId));
|
|
db.SaveChanges();
|
|
}
|
|
|
|
var result = await service.UpdateDriverAsync(RawTreeTestDb.FolderedDriverId, "x", "{}", null, rv);
|
|
|
|
result.Ok.ShouldBeFalse();
|
|
result.Error.ShouldNotBeNull();
|
|
}
|
|
|
|
// ----------------------------------------------------------------------- Device config (WP3)
|
|
|
|
[Fact]
|
|
public async Task LoadDeviceForEdit_projects_fields_with_driver_type_join()
|
|
{
|
|
var (service, _) = Seeded();
|
|
|
|
var dto = await service.LoadDeviceForEditAsync(RawTreeTestDb.FolderedDeviceId);
|
|
|
|
dto.ShouldNotBeNull();
|
|
dto!.Name.ShouldBe("Device1");
|
|
dto.DriverInstanceId.ShouldBe(RawTreeTestDb.FolderedDriverId);
|
|
dto.Enabled.ShouldBeTrue();
|
|
dto.DriverType.ShouldBe("Modbus"); // joined from the parent driver
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LoadDeviceForEdit_returns_null_when_missing()
|
|
{
|
|
var (service, _) = Seeded();
|
|
(await service.LoadDeviceForEditAsync("DEV-nope")).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateDevice_edits_name_config_and_enabled()
|
|
{
|
|
var (service, dbName) = Seeded();
|
|
var rv = RowVersionOf(dbName, db => db.Devices.Single(d => d.DeviceId == RawTreeTestDb.FolderedDeviceId).RowVersion);
|
|
|
|
var result = await service.UpdateDeviceAsync(
|
|
RawTreeTestDb.FolderedDeviceId, "PLC-A", "{\"Host\":\"10.0.0.9\",\"Port\":502}", enabled: false, rv);
|
|
|
|
result.Ok.ShouldBeTrue();
|
|
using var db = RawTreeTestDb.CreateNamed(dbName);
|
|
var dev = db.Devices.Single(d => d.DeviceId == RawTreeTestDb.FolderedDeviceId);
|
|
dev.Name.ShouldBe("PLC-A");
|
|
dev.DeviceConfig.ShouldContain("Host");
|
|
dev.Enabled.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateDevice_rejects_sibling_name_collision()
|
|
{
|
|
var (service, dbName) = Seeded();
|
|
await service.CreateDeviceAsync(RawTreeTestDb.FolderedDriverId, "Device2", "{}");
|
|
var rv = RowVersionOf(dbName, db => db.Devices.Single(d => d.DeviceId == RawTreeTestDb.FolderedDeviceId).RowVersion);
|
|
|
|
var result = await service.UpdateDeviceAsync(RawTreeTestDb.FolderedDeviceId, "Device2", "{}", true, rv);
|
|
|
|
result.Ok.ShouldBeFalse();
|
|
result.Error!.ShouldContain("already exists");
|
|
}
|
|
|
|
// ------------------------------------------------------------- Merged probe config (Test connect)
|
|
|
|
[Fact]
|
|
public async Task LoadMergedProbeConfig_merges_deviceconfig_endpoint_up()
|
|
{
|
|
var (service, dbName) = Seeded();
|
|
// Put an endpoint on the device config (endpoint lives in DeviceConfig in v3).
|
|
using (var db = RawTreeTestDb.CreateNamed(dbName))
|
|
{
|
|
var dev = db.Devices.Single(d => d.DeviceId == RawTreeTestDb.FolderedDeviceId);
|
|
dev.DeviceConfig = "{\"Host\":\"10.1.2.3\",\"Port\":5020}";
|
|
db.SaveChanges();
|
|
}
|
|
|
|
var probe = await service.LoadMergedProbeConfigAsync(RawTreeTestDb.FolderedDeviceId);
|
|
|
|
probe.ShouldNotBeNull();
|
|
probe!.Value.DriverType.ShouldBe("Modbus");
|
|
// Endpoint key from DeviceConfig appears merged up at the top level of the probe config.
|
|
probe.Value.MergedConfigJson.ShouldContain("Host");
|
|
probe.Value.MergedConfigJson.ShouldContain("10.1.2.3");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LoadMergedProbeConfig_returns_null_for_unknown_device()
|
|
{
|
|
var (service, _) = Seeded();
|
|
(await service.LoadMergedProbeConfigAsync("DEV-nope")).ShouldBeNull();
|
|
}
|
|
}
|