feat(mgmt): accept + validate ElementDataType on attribute add/update
This commit is contained in:
@@ -8,8 +8,8 @@ public record DeleteTemplateCommand(int TemplateId);
|
||||
public record ValidateTemplateCommand(int TemplateId);
|
||||
|
||||
// Template member operations
|
||||
public record AddTemplateAttributeCommand(int TemplateId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked);
|
||||
public record UpdateTemplateAttributeCommand(int AttributeId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked);
|
||||
public record AddTemplateAttributeCommand(int TemplateId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked, string? ElementDataType = null);
|
||||
public record UpdateTemplateAttributeCommand(int AttributeId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked, string? ElementDataType = null);
|
||||
public record DeleteTemplateAttributeCommand(int AttributeId);
|
||||
public record AddTemplateAlarmCommand(int TemplateId, string Name, string TriggerType, int PriorityLevel, string? Description, string? TriggerConfiguration, bool IsLocked);
|
||||
public record UpdateTemplateAlarmCommand(int AlarmId, string Name, string TriggerType, int PriorityLevel, string? Description, string? TriggerConfiguration, bool IsLocked);
|
||||
|
||||
@@ -1442,9 +1442,13 @@ public class ManagementActor : ReceiveActor
|
||||
private static async Task<object?> HandleAddAttribute(IServiceProvider sp, AddTemplateAttributeCommand cmd, string user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<TemplateService>();
|
||||
var dataType = Enum.Parse<Commons.Types.Enums.DataType>(cmd.DataType, ignoreCase: true);
|
||||
var elementType = ParseElementDataType(cmd.ElementDataType);
|
||||
ValidateAttributeTypes(cmd.Name, dataType, elementType, cmd.Value);
|
||||
var attr = new TemplateAttribute(cmd.Name)
|
||||
{
|
||||
DataType = Enum.Parse<ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.DataType>(cmd.DataType, ignoreCase: true),
|
||||
DataType = dataType,
|
||||
ElementDataType = elementType,
|
||||
Value = cmd.Value,
|
||||
Description = cmd.Description,
|
||||
DataSourceReference = cmd.DataSourceReference,
|
||||
@@ -1457,9 +1461,13 @@ public class ManagementActor : ReceiveActor
|
||||
private static async Task<object?> HandleUpdateAttribute(IServiceProvider sp, UpdateTemplateAttributeCommand cmd, string user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<TemplateService>();
|
||||
var dataType = Enum.Parse<Commons.Types.Enums.DataType>(cmd.DataType, ignoreCase: true);
|
||||
var elementType = ParseElementDataType(cmd.ElementDataType);
|
||||
ValidateAttributeTypes(cmd.Name, dataType, elementType, cmd.Value);
|
||||
var attr = new TemplateAttribute(cmd.Name)
|
||||
{
|
||||
DataType = Enum.Parse<ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.DataType>(cmd.DataType, ignoreCase: true),
|
||||
DataType = dataType,
|
||||
ElementDataType = elementType,
|
||||
Value = cmd.Value,
|
||||
Description = cmd.Description,
|
||||
DataSourceReference = cmd.DataSourceReference,
|
||||
@@ -1469,6 +1477,57 @@ public class ManagementActor : ReceiveActor
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses an optional element data type token. Returns null when the token is
|
||||
/// empty/whitespace; throws <see cref="ManagementCommandException"/> on an
|
||||
/// unrecognised type name.
|
||||
/// </summary>
|
||||
private static Commons.Types.Enums.DataType? ParseElementDataType(string? elementDataType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(elementDataType)) return null;
|
||||
if (!Enum.TryParse<Commons.Types.Enums.DataType>(elementDataType, ignoreCase: true, out var parsed))
|
||||
throw new ManagementCommandException($"Unrecognised element type '{elementDataType}'.");
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the (DataType, ElementDataType, Value) triple shared by the add
|
||||
/// and update attribute handlers. Throws <see cref="ManagementCommandException"/>
|
||||
/// on any violation:
|
||||
/// <list type="bullet">
|
||||
/// <item>List attributes require a valid scalar element type.</item>
|
||||
/// <item>Scalar attributes may not carry an element type.</item>
|
||||
/// <item>A List default value must decode against the declared element type.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
private static void ValidateAttributeTypes(
|
||||
string name, Commons.Types.Enums.DataType dataType, Commons.Types.Enums.DataType? elementType, string? value)
|
||||
{
|
||||
if (dataType == Commons.Types.Enums.DataType.List)
|
||||
{
|
||||
if (elementType is null || !Commons.Types.AttributeValueCodec.IsValidElementType(elementType.Value))
|
||||
throw new ManagementCommandException(
|
||||
$"List attribute '{name}' requires a valid element type (String, Int32, Float, Double, Boolean, DateTime).");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
try
|
||||
{
|
||||
Commons.Types.AttributeValueCodec.Decode(value, Commons.Types.Enums.DataType.List, elementType);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new ManagementCommandException(
|
||||
$"List attribute '{name}' has an invalid list value: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (elementType is not null)
|
||||
{
|
||||
throw new ManagementCommandException("Element type is only valid on List attributes.");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteAttribute(IServiceProvider sp, DeleteTemplateAttributeCommand cmd, string user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<TemplateService>();
|
||||
|
||||
@@ -387,6 +387,140 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
Assert.Contains("Designer", response.Message);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// MV-10: ElementDataType accept + validate on attribute add/update
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void AddListAttribute_WithStringElementType_PersistsBothColumns()
|
||||
{
|
||||
// A template exists with no attributes; AddAttributeAsync will save the
|
||||
// entity built by the handler. Capture it to assert the persisted shape.
|
||||
var template = new Template("T1") { Id = 1 };
|
||||
_templateRepo.GetTemplateByIdAsync(1, Arg.Any<CancellationToken>()).Returns(template);
|
||||
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Template> { template });
|
||||
|
||||
TemplateAttribute? saved = null;
|
||||
_templateRepo
|
||||
.When(r => r.AddTemplateAttributeAsync(Arg.Any<TemplateAttribute>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => saved = ci.Arg<TemplateAttribute>());
|
||||
_templateRepo.SaveChangesAsync(Arg.Any<CancellationToken>()).Returns(1);
|
||||
_services.AddScoped<TemplateService>();
|
||||
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAttributeCommand(1, "Tags", "List", "[\"a\",\"b\"]", null, null, false, "String"),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
|
||||
Assert.NotNull(saved);
|
||||
Assert.Equal(Commons.Types.Enums.DataType.List, saved!.DataType);
|
||||
Assert.Equal(Commons.Types.Enums.DataType.String, saved.ElementDataType);
|
||||
Assert.Equal("[\"a\",\"b\"]", saved.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddListAttribute_WithNoElementType_ReturnsManagementError()
|
||||
{
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAttributeCommand(1, "Tags", "List", null, null, null, false, null),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("requires a valid element type", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddListAttribute_WithBinaryElementType_ReturnsManagementError()
|
||||
{
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAttributeCommand(1, "Tags", "List", null, null, null, false, "Binary"),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("requires a valid element type", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddScalarAttribute_WithElementType_ReturnsManagementError()
|
||||
{
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAttributeCommand(1, "Count", "Int32", null, null, null, false, "String"),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("only valid on List attributes", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddListAttribute_WithMalformedDefault_ReturnsManagementError()
|
||||
{
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAttributeCommand(1, "Tags", "List", "[\"a\"", null, null, false, "String"),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("invalid list value", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddListAttribute_WithTypeMismatchedDefault_ReturnsManagementError()
|
||||
{
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAttributeCommand(1, "Counts", "List", "[\"x\"]", null, null, false, "Int32"),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("invalid list value", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateScalarAttribute_WithElementType_ReturnsManagementError()
|
||||
{
|
||||
// Update path runs the same pre-service validation: a scalar attribute
|
||||
// may not carry an element type, regardless of repository state.
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new UpdateTemplateAttributeCommand(5, "Count", "Int32", null, null, null, false, "String"),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("only valid on List attributes", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateApiKey_WithDesignRole_ReturnsUnauthorized()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user