feat(template): stamp ElementDataType on instance attribute overrides

Set existingOverride.ElementDataType and newOverride.ElementDataType from
templateAttr.ElementDataType in both the update and create branches of
SetAttributeOverrideAsync, so the persisted InstanceAttributeOverride row
always carries the element type for later central normalizer use (#93/M3).
This commit is contained in:
Joseph Doherty
2026-06-16 17:33:15 -04:00
parent 180d55482b
commit abe8832e9e
2 changed files with 103 additions and 1 deletions
@@ -269,6 +269,106 @@ public class InstanceServiceTests
_repoMock.Verify(r => r.AddInstanceConnectionBindingAsync(It.IsAny<InstanceConnectionBinding>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
}
// --- NJ-2: ElementDataType stamping on SetAttributeOverrideAsync ---
[Fact]
public async Task SetAttributeOverride_CreatePath_ListAttribute_StampsElementDataType()
{
var instance = new Instance("Inst1") { Id = 1, TemplateId = 1 };
_repoMock.Setup(r => r.GetInstanceByIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(instance);
_repoMock.Setup(r => r.GetAttributesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<TemplateAttribute>
{
new("Tags")
{
IsLocked = false,
DataType = DataType.List,
ElementDataType = DataType.String
}
});
_repoMock.Setup(r => r.GetOverridesByInstanceIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<InstanceAttributeOverride>()); // no existing override → create path
_repoMock.Setup(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(1);
InstanceAttributeOverride? captured = null;
_repoMock.Setup(r => r.AddInstanceAttributeOverrideAsync(It.IsAny<InstanceAttributeOverride>(), It.IsAny<CancellationToken>()))
.Callback<InstanceAttributeOverride, CancellationToken>((o, _) => captured = o);
var result = await _sut.SetAttributeOverrideAsync(1, "Tags", "[\"a\"]", "admin");
Assert.True(result.IsSuccess);
Assert.NotNull(captured);
Assert.Equal(DataType.String, captured.ElementDataType);
}
[Fact]
public async Task SetAttributeOverride_UpdatePath_ListAttribute_StampsElementDataType()
{
var instance = new Instance("Inst1") { Id = 1, TemplateId = 1 };
_repoMock.Setup(r => r.GetInstanceByIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(instance);
_repoMock.Setup(r => r.GetAttributesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<TemplateAttribute>
{
new("Tags")
{
IsLocked = false,
DataType = DataType.List,
ElementDataType = DataType.String
}
});
var existingOverride = new InstanceAttributeOverride("Tags") { Id = 42, InstanceId = 1, OverrideValue = "[\"old\"]" };
_repoMock.Setup(r => r.GetOverridesByInstanceIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<InstanceAttributeOverride> { existingOverride }); // pre-existing → update path
_repoMock.Setup(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(1);
InstanceAttributeOverride? captured = null;
_repoMock.Setup(r => r.UpdateInstanceAttributeOverrideAsync(It.IsAny<InstanceAttributeOverride>(), It.IsAny<CancellationToken>()))
.Callback<InstanceAttributeOverride, CancellationToken>((o, _) => captured = o);
var result = await _sut.SetAttributeOverrideAsync(1, "Tags", "[\"new\"]", "admin");
Assert.True(result.IsSuccess);
Assert.NotNull(captured);
Assert.Equal(DataType.String, captured.ElementDataType);
}
[Fact]
public async Task SetAttributeOverride_CreatePath_ScalarAttribute_ElementDataTypeIsNull()
{
var instance = new Instance("Inst1") { Id = 1, TemplateId = 1 };
_repoMock.Setup(r => r.GetInstanceByIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(instance);
_repoMock.Setup(r => r.GetAttributesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<TemplateAttribute>
{
new("Threshold")
{
IsLocked = false,
DataType = DataType.Float,
ElementDataType = null
}
});
_repoMock.Setup(r => r.GetOverridesByInstanceIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<InstanceAttributeOverride>());
_repoMock.Setup(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(1);
InstanceAttributeOverride? captured = null;
_repoMock.Setup(r => r.AddInstanceAttributeOverrideAsync(It.IsAny<InstanceAttributeOverride>(), It.IsAny<CancellationToken>()))
.Callback<InstanceAttributeOverride, CancellationToken>((o, _) => captured = o);
var result = await _sut.SetAttributeOverrideAsync(1, "Threshold", "3.14", "admin");
Assert.True(result.IsSuccess);
Assert.NotNull(captured);
Assert.Null(captured.ElementDataType);
}
[Fact]
public async Task AssignToArea_AreaInDifferentSite_ReturnsFailure()
{