feat(adminui): CreateAliasTagAsync/UpdateAliasTagAsync + Galaxy-gateway guard
This commit is contained in:
@@ -379,6 +379,33 @@ public interface IUnsTreeService
|
|||||||
/// <returns>Success, a missing-row failure, a guard failure, or a concurrency failure.</returns>
|
/// <returns>Success, a missing-row failure, a guard failure, or a concurrency failure.</returns>
|
||||||
Task<UnsMutationResult> UpdateTagAsync(string tagId, TagInput input, byte[] rowVersion, CancellationToken ct = default);
|
Task<UnsMutationResult> UpdateTagAsync(string tagId, TagInput input, byte[] rowVersion, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new Galaxy alias tag on an equipment: an ordinary equipment-bound <c>Tag</c> bound to a
|
||||||
|
/// <c>GalaxyMxGateway</c> driver, with <c>FolderPath</c> null and a <c>{"FullName":…}</c> TagConfig
|
||||||
|
/// carrying the picked Galaxy reference. Fails on a duplicate <c>TagId</c>, an empty Galaxy reference,
|
||||||
|
/// an unknown equipment, a driver that is not a Galaxy gateway in the equipment's cluster, or a name
|
||||||
|
/// already used on the equipment.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="equipmentId">The owning equipment.</param>
|
||||||
|
/// <param name="input">The operator-editable alias fields (id, name, gateway, type, access, reference).</param>
|
||||||
|
/// <param name="ct">A token to cancel the operation.</param>
|
||||||
|
/// <returns>Success, or one of the guard failures.</returns>
|
||||||
|
Task<UnsMutationResult> CreateAliasTagAsync(string equipmentId, AliasTagInput input, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates a Galaxy alias tag's gateway binding, name, data type, access level, and Galaxy reference
|
||||||
|
/// (the <c>{"FullName":…}</c> TagConfig). The owning <c>EquipmentId</c> and the null <c>FolderPath</c>
|
||||||
|
/// are preserved. Re-runs the empty-reference and Galaxy-gateway-in-cluster guards against the alias's
|
||||||
|
/// existing equipment, and enforces name uniqueness on that equipment excluding this tag. Uses
|
||||||
|
/// last-write-wins optimistic concurrency on <see cref="Configuration.Entities.Tag.RowVersion"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tagId">The alias tag to update.</param>
|
||||||
|
/// <param name="input">The new operator-editable alias fields.</param>
|
||||||
|
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
||||||
|
/// <param name="ct">A token to cancel the operation.</param>
|
||||||
|
/// <returns>Success, a missing-row failure, a guard failure, or a concurrency failure.</returns>
|
||||||
|
Task<UnsMutationResult> UpdateAliasTagAsync(string tagId, AliasTagInput input, byte[] rowVersion, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deletes a tag. A missing row is treated as success (already gone). Uses last-write-wins
|
/// Deletes a tag. A missing row is treated as success (already gone). Uses last-write-wins
|
||||||
/// optimistic concurrency on <see cref="Configuration.Entities.Tag.RowVersion"/>.
|
/// optimistic concurrency on <see cref="Configuration.Entities.Tag.RowVersion"/>.
|
||||||
|
|||||||
@@ -893,6 +893,101 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<UnsMutationResult> CreateAliasTagAsync(
|
||||||
|
string equipmentId,
|
||||||
|
AliasTagInput input,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||||
|
|
||||||
|
if (await db.Tags.AnyAsync(t => t.TagId == input.TagId, ct))
|
||||||
|
{
|
||||||
|
return new UnsMutationResult(false, $"Tag '{input.TagId}' already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(input.FullName))
|
||||||
|
{
|
||||||
|
return new UnsMutationResult(false, "Alias is missing a Galaxy reference.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await db.Equipment.AnyAsync(e => e.EquipmentId == equipmentId, ct))
|
||||||
|
{
|
||||||
|
return new UnsMutationResult(false, $"Equipment '{equipmentId}' not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var equipmentCluster = await ResolveEquipmentClusterAsync(db, equipmentId, ct);
|
||||||
|
var guard = await CheckAliasDriverGuardAsync(db, input.DriverInstanceId, equipmentCluster, ct);
|
||||||
|
if (guard is not null)
|
||||||
|
{
|
||||||
|
return guard.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await db.Tags.AnyAsync(t => t.EquipmentId == equipmentId && t.Name == input.Name, ct))
|
||||||
|
{
|
||||||
|
return new UnsMutationResult(false, $"A tag named '{input.Name}' already exists on this equipment.");
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Tags.Add(BuildAliasTag(equipmentId, input));
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
return new UnsMutationResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<UnsMutationResult> UpdateAliasTagAsync(
|
||||||
|
string tagId,
|
||||||
|
AliasTagInput input,
|
||||||
|
byte[] rowVersion,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||||
|
|
||||||
|
var entity = await db.Tags.FirstOrDefaultAsync(t => t.TagId == tagId, ct);
|
||||||
|
if (entity is null)
|
||||||
|
{
|
||||||
|
return new UnsMutationResult(false, "Row no longer exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(input.FullName))
|
||||||
|
{
|
||||||
|
return new UnsMutationResult(false, "Alias is missing a Galaxy reference.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var equipmentCluster = await ResolveEquipmentClusterAsync(db, entity.EquipmentId, ct);
|
||||||
|
var guard = await CheckAliasDriverGuardAsync(db, input.DriverInstanceId, equipmentCluster, ct);
|
||||||
|
if (guard is not null)
|
||||||
|
{
|
||||||
|
return guard.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await db.Tags.AnyAsync(
|
||||||
|
t => t.EquipmentId == entity.EquipmentId && t.Name == input.Name && t.TagId != tagId,
|
||||||
|
ct))
|
||||||
|
{
|
||||||
|
return new UnsMutationResult(false, $"A tag named '{input.Name}' already exists on this equipment.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// EquipmentId and FolderPath (null) are preserved — alias tags are always equipment-bound.
|
||||||
|
entity.DriverInstanceId = input.DriverInstanceId;
|
||||||
|
entity.Name = input.Name;
|
||||||
|
entity.DataType = input.DataType;
|
||||||
|
entity.AccessLevel = input.AccessLevel;
|
||||||
|
entity.FolderPath = null;
|
||||||
|
entity.TagConfig = BuildAliasTagConfig(input.FullName);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
return new UnsMutationResult(true, null);
|
||||||
|
}
|
||||||
|
catch (DbUpdateConcurrencyException)
|
||||||
|
{
|
||||||
|
return new UnsMutationResult(false, "Another user changed this tag while you were editing.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<UnsMutationResult> DeleteTagAsync(
|
public async Task<UnsMutationResult> DeleteTagAsync(
|
||||||
string tagId,
|
string tagId,
|
||||||
@@ -1290,6 +1385,65 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Galaxy-aware driver guard for aliases: the driver must be a Galaxy gateway
|
||||||
|
/// (<c>DriverType == "GalaxyMxGateway"</c>) in the equipment's cluster. Distinct from
|
||||||
|
/// <see cref="CheckTagDriverGuardAsync"/>, which requires an Equipment-kind namespace and would reject
|
||||||
|
/// the gateway. Returns <c>null</c> when the binding is allowed, or a populated failure otherwise.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<UnsMutationResult?> CheckAliasDriverGuardAsync(
|
||||||
|
OtOpcUaConfigDbContext db,
|
||||||
|
string driverInstanceId,
|
||||||
|
string? equipmentCluster,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var driver = await db.DriverInstances.FirstOrDefaultAsync(d => d.DriverInstanceId == driverInstanceId, ct);
|
||||||
|
if (driver is null)
|
||||||
|
{
|
||||||
|
return new UnsMutationResult(false, $"Driver '{driverInstanceId}' not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (driver.DriverType != "GalaxyMxGateway")
|
||||||
|
{
|
||||||
|
return new UnsMutationResult(false, $"Driver '{driverInstanceId}' is not a Galaxy gateway.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (driver.ClusterId != equipmentCluster)
|
||||||
|
{
|
||||||
|
return new UnsMutationResult(
|
||||||
|
false,
|
||||||
|
$"Galaxy gateway '{driverInstanceId}' is in cluster '{driver.ClusterId}' but the equipment is in cluster '{equipmentCluster}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serialises a Galaxy reference into the alias TagConfig envelope <c>{"FullName":"<ref>"}</c>.
|
||||||
|
/// </summary>
|
||||||
|
private static string BuildAliasTagConfig(string fullName) =>
|
||||||
|
System.Text.Json.JsonSerializer.Serialize(new Dictionary<string, string> { ["FullName"] = fullName });
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the alias <see cref="Tag"/> entity for an equipment: a Galaxy-gateway-bound, equipment-scoped
|
||||||
|
/// tag with a null <c>FolderPath</c>, no poll group, non-idempotent writes, and the
|
||||||
|
/// <c>{"FullName":…}</c> TagConfig. A pure builder — reused by the relay→alias converter so create and
|
||||||
|
/// conversion produce byte-identical rows.
|
||||||
|
/// </summary>
|
||||||
|
private static Tag BuildAliasTag(string equipmentId, AliasTagInput input) => new()
|
||||||
|
{
|
||||||
|
TagId = input.TagId,
|
||||||
|
DriverInstanceId = input.DriverInstanceId,
|
||||||
|
EquipmentId = equipmentId,
|
||||||
|
Name = input.Name,
|
||||||
|
FolderPath = null,
|
||||||
|
DataType = input.DataType,
|
||||||
|
AccessLevel = input.AccessLevel,
|
||||||
|
WriteIdempotent = false,
|
||||||
|
PollGroupId = null,
|
||||||
|
TagConfig = BuildAliasTagConfig(input.FullName),
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decision #122: an equipment may only bind to a driver in the same cluster as its line.
|
/// Decision #122: an equipment may only bind to a driver in the same cluster as its line.
|
||||||
/// Policy:
|
/// Policy:
|
||||||
|
|||||||
@@ -207,6 +207,211 @@ public sealed class UnsTreeServiceAliasTagTests
|
|||||||
alias.IsAlias.ShouldBeTrue();
|
alias.IsAlias.ShouldBeTrue();
|
||||||
alias.Source.ShouldBeNull();
|
alias.Source.ShouldBeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Write-side: CreateAliasTagAsync / UpdateAliasTagAsync (T5) ----
|
||||||
|
|
||||||
|
private const string SecondClusterId = "ALT";
|
||||||
|
private const string SecondGatewayDriverId = "DRV-GALAXY-ALT";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seeds a second cluster (no UNS hierarchy needed) carrying its own GalaxyMxGateway driver, so a
|
||||||
|
/// cross-cluster guard test can reference a gateway that is NOT in the equipment's cluster.
|
||||||
|
/// </summary>
|
||||||
|
private static void SeedSecondClusterGateway(string dbName)
|
||||||
|
{
|
||||||
|
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
||||||
|
|
||||||
|
db.ServerClusters.Add(new ServerCluster
|
||||||
|
{
|
||||||
|
ClusterId = SecondClusterId,
|
||||||
|
Name = "Alt",
|
||||||
|
Enterprise = "zb",
|
||||||
|
Site = "krakow",
|
||||||
|
RedundancyMode = RedundancyMode.None,
|
||||||
|
CreatedBy = "test",
|
||||||
|
});
|
||||||
|
db.Namespaces.Add(new Namespace
|
||||||
|
{
|
||||||
|
NamespaceId = "NS-SP-ALT",
|
||||||
|
ClusterId = SecondClusterId,
|
||||||
|
Kind = NamespaceKind.SystemPlatform,
|
||||||
|
NamespaceUri = "urn:zb:sp:alt",
|
||||||
|
});
|
||||||
|
db.DriverInstances.Add(new DriverInstance
|
||||||
|
{
|
||||||
|
DriverInstanceId = SecondGatewayDriverId,
|
||||||
|
ClusterId = SecondClusterId,
|
||||||
|
NamespaceId = "NS-SP-ALT",
|
||||||
|
Name = "alt galaxy gateway",
|
||||||
|
DriverType = "GalaxyMxGateway",
|
||||||
|
DriverConfig = "{}",
|
||||||
|
});
|
||||||
|
|
||||||
|
db.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AliasTagInput Input(
|
||||||
|
string tagId = "TAG-NEW-ALIAS",
|
||||||
|
string name = "speed-alias",
|
||||||
|
string driverInstanceId = GatewayDriverId,
|
||||||
|
string dataType = "Float",
|
||||||
|
TagAccessLevel accessLevel = TagAccessLevel.Read,
|
||||||
|
string fullName = "TestMachine_020.Speed") =>
|
||||||
|
new(tagId, name, driverInstanceId, dataType, accessLevel, fullName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A valid create persists a Galaxy-gateway-bound, equipment-scoped alias Tag: <c>FolderPath</c> is
|
||||||
|
/// null, the supplied AccessLevel is honoured, and the TagConfig is the <c>{"FullName":…}</c> envelope.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAliasTag_persists_galaxy_alias_row()
|
||||||
|
{
|
||||||
|
var dbName = SeedCluster();
|
||||||
|
var service = new UnsTreeService(UnsTreeTestDb.Factory(dbName));
|
||||||
|
|
||||||
|
var result = await service.CreateAliasTagAsync(
|
||||||
|
EquipmentId, Input(accessLevel: TagAccessLevel.ReadWrite, fullName: "TestMachine_020.Speed"));
|
||||||
|
|
||||||
|
result.Ok.ShouldBeTrue();
|
||||||
|
result.Error.ShouldBeNull();
|
||||||
|
|
||||||
|
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
||||||
|
var tag = db.Tags.Single(t => t.TagId == "TAG-NEW-ALIAS");
|
||||||
|
tag.DriverInstanceId.ShouldBe(GatewayDriverId);
|
||||||
|
tag.EquipmentId.ShouldBe(EquipmentId);
|
||||||
|
tag.FolderPath.ShouldBeNull();
|
||||||
|
tag.AccessLevel.ShouldBe(TagAccessLevel.ReadWrite);
|
||||||
|
tag.Name.ShouldBe("speed-alias");
|
||||||
|
tag.DataType.ShouldBe("Float");
|
||||||
|
|
||||||
|
using var doc = System.Text.Json.JsonDocument.Parse(tag.TagConfig);
|
||||||
|
doc.RootElement.GetProperty("FullName").GetString().ShouldBe("TestMachine_020.Speed");
|
||||||
|
tag.TagConfig.ShouldContain("FullName");
|
||||||
|
tag.TagConfig.ShouldContain("TestMachine_020.Speed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>An empty/whitespace Galaxy reference is rejected before anything is written.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAliasTag_rejects_empty_reference()
|
||||||
|
{
|
||||||
|
var dbName = SeedCluster();
|
||||||
|
var service = new UnsTreeService(UnsTreeTestDb.Factory(dbName));
|
||||||
|
|
||||||
|
var result = await service.CreateAliasTagAsync(EquipmentId, Input(fullName: " "));
|
||||||
|
|
||||||
|
result.Ok.ShouldBeFalse();
|
||||||
|
result.Error!.ShouldContain("reference");
|
||||||
|
|
||||||
|
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
||||||
|
db.Tags.Any(t => t.TagId == "TAG-NEW-ALIAS").ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Binding the alias to a non-Galaxy driver (the cluster's Modbus driver) is rejected.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAliasTag_rejects_non_galaxy_driver()
|
||||||
|
{
|
||||||
|
var dbName = SeedCluster();
|
||||||
|
var service = new UnsTreeService(UnsTreeTestDb.Factory(dbName));
|
||||||
|
|
||||||
|
var result = await service.CreateAliasTagAsync(
|
||||||
|
EquipmentId, Input(driverInstanceId: ModbusDriverId));
|
||||||
|
|
||||||
|
result.Ok.ShouldBeFalse();
|
||||||
|
result.Error!.ShouldContain("Galaxy gateway");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A Galaxy gateway in a different cluster than the equipment is rejected (cluster mismatch).</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAliasTag_rejects_cross_cluster_gateway()
|
||||||
|
{
|
||||||
|
var dbName = SeedCluster();
|
||||||
|
SeedSecondClusterGateway(dbName);
|
||||||
|
var service = new UnsTreeService(UnsTreeTestDb.Factory(dbName));
|
||||||
|
|
||||||
|
var result = await service.CreateAliasTagAsync(
|
||||||
|
EquipmentId, Input(driverInstanceId: SecondGatewayDriverId));
|
||||||
|
|
||||||
|
result.Ok.ShouldBeFalse();
|
||||||
|
result.Error!.ShouldContain("cluster");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A name already used by another tag on the equipment is rejected.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAliasTag_rejects_duplicate_name()
|
||||||
|
{
|
||||||
|
var dbName = SeedCluster();
|
||||||
|
|
||||||
|
using (var db = UnsTreeTestDb.CreateNamed(dbName))
|
||||||
|
{
|
||||||
|
db.Tags.Add(new Tag
|
||||||
|
{
|
||||||
|
TagId = "TAG-EXISTING",
|
||||||
|
DriverInstanceId = ModbusDriverId,
|
||||||
|
EquipmentId = EquipmentId,
|
||||||
|
Name = "speed-alias",
|
||||||
|
DataType = "Int32",
|
||||||
|
AccessLevel = TagAccessLevel.Read,
|
||||||
|
TagConfig = "{}",
|
||||||
|
});
|
||||||
|
db.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
var service = new UnsTreeService(UnsTreeTestDb.Factory(dbName));
|
||||||
|
|
||||||
|
var result = await service.CreateAliasTagAsync(EquipmentId, Input(name: "speed-alias"));
|
||||||
|
|
||||||
|
result.Ok.ShouldBeFalse();
|
||||||
|
result.Error!.ShouldContain("speed-alias");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update changes the alias's name, data type, access level, and Galaxy reference, returning Ok and
|
||||||
|
/// persisting the new values and the refreshed <c>{"FullName":…}</c> TagConfig.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAliasTag_changes_fields_and_reference()
|
||||||
|
{
|
||||||
|
var dbName = SeedCluster();
|
||||||
|
byte[] rowVersion;
|
||||||
|
|
||||||
|
using (var db = UnsTreeTestDb.CreateNamed(dbName))
|
||||||
|
{
|
||||||
|
db.Tags.Add(new Tag
|
||||||
|
{
|
||||||
|
TagId = "TAG-UPD-ALIAS",
|
||||||
|
DriverInstanceId = GatewayDriverId,
|
||||||
|
EquipmentId = EquipmentId,
|
||||||
|
Name = "old-name",
|
||||||
|
DataType = "Float",
|
||||||
|
AccessLevel = TagAccessLevel.Read,
|
||||||
|
TagConfig = "{\"FullName\":\"TestMachine_020.Speed\"}",
|
||||||
|
});
|
||||||
|
db.SaveChanges();
|
||||||
|
rowVersion = db.Tags.Single(t => t.TagId == "TAG-UPD-ALIAS").RowVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
var service = new UnsTreeService(UnsTreeTestDb.Factory(dbName));
|
||||||
|
|
||||||
|
var result = await service.UpdateAliasTagAsync(
|
||||||
|
"TAG-UPD-ALIAS",
|
||||||
|
Input(tagId: "TAG-UPD-ALIAS", name: "new-name", dataType: "Double",
|
||||||
|
accessLevel: TagAccessLevel.ReadWrite, fullName: "TestMachine_020.Setpoint"),
|
||||||
|
rowVersion);
|
||||||
|
|
||||||
|
result.Ok.ShouldBeTrue();
|
||||||
|
result.Error.ShouldBeNull();
|
||||||
|
|
||||||
|
using var verify = UnsTreeTestDb.CreateNamed(dbName);
|
||||||
|
var tag = verify.Tags.Single(t => t.TagId == "TAG-UPD-ALIAS");
|
||||||
|
tag.Name.ShouldBe("new-name");
|
||||||
|
tag.DataType.ShouldBe("Double");
|
||||||
|
tag.AccessLevel.ShouldBe(TagAccessLevel.ReadWrite);
|
||||||
|
tag.EquipmentId.ShouldBe(EquipmentId);
|
||||||
|
tag.FolderPath.ShouldBeNull();
|
||||||
|
|
||||||
|
using var doc = System.Text.Json.JsonDocument.Parse(tag.TagConfig);
|
||||||
|
doc.RootElement.GetProperty("FullName").GetString().ShouldBe("TestMachine_020.Setpoint");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Small Shouldly-style helper for "exactly one match" assertions used by these tests.</summary>
|
/// <summary>Small Shouldly-style helper for "exactly one match" assertions used by these tests.</summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user