test(adminui): cover divergent-prefix {{equip}} rejection; use EquipToken constant in message

This commit is contained in:
Joseph Doherty
2026-06-10 08:08:13 -04:00
parent c7041a24e7
commit a7c1d7f7cb
3 changed files with 41 additions and 6 deletions
@@ -375,7 +375,8 @@ public interface IUnsTreeService
/// <param name="equipmentId">The owning equipment.</param>
/// <param name="input">The operator-editable virtual-tag fields.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>Success, or one of the guard failures.</returns>
/// <returns>Success, or one of the guard failures; or if the chosen script uses the <c>{{equip}}</c>
/// token but the equipment has no derivable single tag base.</returns>
Task<UnsMutationResult> CreateVirtualTagAsync(string equipmentId, VirtualTagInput input, CancellationToken ct = default);
/// <summary>
@@ -389,7 +390,8 @@ public interface IUnsTreeService
/// <param name="input">The new operator-editable virtual-tag 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>
/// <returns>Success, a missing-row failure, a guard failure, or a concurrency failure; or if the
/// chosen script uses the <c>{{equip}}</c> token but the equipment has no derivable single tag base.</returns>
Task<UnsMutationResult> UpdateVirtualTagAsync(string virtualTagId, VirtualTagInput input, byte[] rowVersion, CancellationToken ct = default);
/// <summary>
@@ -908,12 +908,10 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
var fullNames = configs.Select(TagConfigFullName.Extract);
if (EquipmentScriptPaths.DeriveEquipmentBase(fullNames) is null)
{
// NOTE: literal {{equip}} must survive — in a C# interpolated string `{{`/`}}`
// collapse to single braces, so keep the token text in a NON-interpolated segment.
return new UnsMutationResult(false,
$"Equipment '{equipmentId}' has no single tag base, so the "
+ "{{equip}} token can't be resolved. Add at least one driver tag under this "
+ "equipment (all sharing one object prefix), or remove {{equip}} from the script.");
+ $"{EquipmentScriptPaths.EquipToken} token can't be resolved. Add at least one driver tag under this "
+ $"equipment (all sharing one object prefix), or remove {EquipmentScriptPaths.EquipToken} from the script.");
}
return null;
}
@@ -138,6 +138,41 @@ public sealed class VirtualTagEquipTokenValidationTests
result.Error.ShouldBeNull();
}
/// <summary>
/// {{equip}} script + TWO driver tags whose FullNames have DIFFERENT object prefixes
/// (no single base can be derived) → create rejected, error names equipment + token.
/// </summary>
[Fact]
public async Task Create_equip_token_with_divergent_prefixes_rejected()
{
var (service, dbName) = Fresh();
Seed(dbName, EquipBaseScript, tagFullName: "TestMachine_001.X");
using (var db = UnsTreeTestDb.CreateNamed(dbName))
{
db.Tags.Add(new Tag
{
TagId = "TAG-2",
DriverInstanceId = "DRV-1",
EquipmentId = "EQ-1",
Name = "y",
DataType = "Float",
AccessLevel = TagAccessLevel.Read,
TagConfig = "{\"FullName\":\"DelmiaReceiver_001.Y\"}",
});
db.SaveChanges();
}
var result = await service.CreateVirtualTagAsync("EQ-1", Input());
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
result.Error.ShouldContain("EQ-1");
result.Error.ShouldContain("{{equip}}");
using var verifyDb = UnsTreeTestDb.CreateNamed(dbName);
verifyDb.VirtualTags.Any(v => v.VirtualTagId == "VTAG-1").ShouldBeFalse();
}
// ----- Update -----
/// <summary>{{equip}} script + a derivable base → update succeeds.</summary>