fix(management): strict enum parsing for DataType/ElementDataType/AlarmTriggerType (P6-10)
ManagementActor's ParseDataType/ParseElementDataType (bare Enum.TryParse) and
the two AlarmTriggerType parse sites in HandleAddAlarm/HandleUpdateAlarm (raw
Enum.Parse) all admitted ordinals ("2"), undefined values ("99" — a
deploy-time bomb that saves fine and only detonates at deploy), and
comma-joined name lists (bitwise-ORed together regardless of [Flags], e.g.
"String,Int32" silently becoming DateType.DateTime). A typo against the raw
Enum.Parse sites also surfaced as a masked "An internal error occurred"
rather than a curated field-naming error.
Adds ParseEnumStrict<TEnum>, mirroring the existing ParseDeploymentStatus
guard (names only, defined values only, comma-token rejection per the
Medium-wave T8 nuance / AuditQueryParamParsers.ParseEnumListStrict pattern),
and routes all four call sites through it.
Tests: 22 new ManagementActorTests cases per parse site (ordinal, undefined
numeric, comma-joined, and typo all rejected with the curated field-naming
error; every defined name still parses through to the service layer).
Proof of failure recorded in each rejection test's doc comment; verified
live by stashing the ManagementActor.cs fix — exactly the 14 new rejection
tests fail pre-fix (13 acceptance tests pass unchanged), confirming the
defect and the fix.
This commit is contained in:
@@ -765,6 +765,255 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
Assert.DoesNotContain("internal error", response.Error);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// P6-10 (register row-88 class): strict single-value enum parsing in
|
||||
// ParseDataType / ParseElementDataType / the AlarmTriggerType parse sites.
|
||||
// Raw Enum.Parse/TryParse admits ordinals ("2"), undefined values ("99" —
|
||||
// a deploy-time bomb that saves fine and only detonates at deploy), and
|
||||
// (Medium-wave T8 nuance) comma-joined name lists that TryParse/Parse
|
||||
// bitwise-OR together regardless of [Flags]. ParseEnumStrict rejects all
|
||||
// three, and — for AlarmTriggerType specifically — replaces a masked
|
||||
// "An internal error occurred" (raw Enum.Parse's uncaught
|
||||
// ArgumentException) with a curated, field-naming error.
|
||||
// ========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData("2")] // ordinal for a DEFINED member (Float)
|
||||
[InlineData("99")] // undefined ordinal — the deploy-time-bomb case
|
||||
[InlineData("String,Int32")] // comma-joined names — TryParse ORs 4|1 into DateTime (5)
|
||||
public void AddAttribute_WithNonNameDataTypeToken_ReturnsCuratedError(string token)
|
||||
{
|
||||
// PROOF OF FAILURE (pre-fix): the unguarded `Enum.TryParse` in the old
|
||||
// ParseDataType has no numeric guard, no comma guard, and never checks
|
||||
// Enum.IsDefined. Each of these tokens parsed successfully pre-fix (to
|
||||
// Float, an undefined value, and DateTime respectively) and the command
|
||||
// proceeded past the parser into TemplateService, failing later with
|
||||
// "Template with ID 1 not found." instead of this curated message —
|
||||
// verified by stashing this fix and re-running (see task report).
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAttributeCommand(1, "Whatever", token, null, null, null, false, null),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("Unrecognised data type", response.Error);
|
||||
Assert.Contains(token, response.Error);
|
||||
Assert.Contains("Valid values:", response.Error);
|
||||
Assert.DoesNotContain("internal error", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddAttribute_WithTypoDataType_ReturnsCuratedErrorNamingValidValues()
|
||||
{
|
||||
// The pre-fix ParseDataType already curated a plain typo (that part of
|
||||
// MV-10 was correct), but its message named no valid set — this asserts
|
||||
// the new "Valid values:" suffix, which is absent pre-fix.
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAttributeCommand(1, "Whatever", "Sting", null, null, null, false, null),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Unrecognised data type 'Sting'", response.Error);
|
||||
Assert.Contains("Valid values:", response.Error);
|
||||
Assert.Contains("Boolean", response.Error);
|
||||
Assert.Contains("List", response.Error);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Boolean")]
|
||||
[InlineData("Int32")]
|
||||
[InlineData("Float")]
|
||||
[InlineData("Double")]
|
||||
[InlineData("String")]
|
||||
[InlineData("DateTime")]
|
||||
[InlineData("Binary")]
|
||||
public void AddAttribute_WithDefinedDataTypeName_ParsesAndProceedsPastGuard(string name)
|
||||
{
|
||||
// No repository stub: GetTemplateByIdAsync returns null (NSubstitute
|
||||
// default), so a token the guard ACCEPTS reaches AddAttributeAsync's
|
||||
// "Template not found" failure. A token the guard REJECTS would never
|
||||
// reach the service at all (see the tests above), so this proves every
|
||||
// defined name still parses.
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAttributeCommand(1, "Whatever", name, null, null, null, false, null),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Template with ID 1 not found", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddAttribute_WithDefinedListDataTypeNameAndElementType_ParsesAndProceedsPastGuard()
|
||||
{
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAttributeCommand(1, "Whatever", "List", null, null, null, false, "String"),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Template with ID 1 not found", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateAttribute_WithCommaJoinedDataType_ReturnsCuratedError()
|
||||
{
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new UpdateTemplateAttributeCommand(5, "Count", "String,Int32", null, null, null, false, null),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Unrecognised data type", response.Error);
|
||||
Assert.Contains("Valid values:", response.Error);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("2")] // ordinal for a DEFINED, valid element type (Float)
|
||||
[InlineData("99")] // undefined ordinal
|
||||
[InlineData("String,Int32")] // comma-joined — combines to DateTime (also a valid element type)
|
||||
public void AddListAttribute_WithNonNameElementTypeToken_ReturnsCuratedError(string token)
|
||||
{
|
||||
// PROOF OF FAILURE (pre-fix): each of these tokens parsed successfully
|
||||
// (Float / an undefined value that fails a LATER, differently-worded
|
||||
// "requires a valid element type" check / DateTime) rather than being
|
||||
// rejected at the parse boundary with this message.
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAttributeCommand(1, "Tags", "List", null, null, null, false, token),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("Unrecognised element type", response.Error);
|
||||
Assert.Contains(token, response.Error);
|
||||
Assert.Contains("Valid values:", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddListAttribute_WithTypoElementType_ReturnsCuratedErrorNamingValidValues()
|
||||
{
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAttributeCommand(1, "Tags", "List", null, null, null, false, "Sting"),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Unrecognised element type 'Sting'", response.Error);
|
||||
Assert.Contains("Valid values:", response.Error);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0")] // ordinal for a DEFINED member (ValueMatch)
|
||||
[InlineData("99")] // undefined ordinal — the deploy-time-bomb case
|
||||
[InlineData("ValueMatch,RangeViolation")] // comma-joined — Enum.Parse ORs 0|1 into RangeViolation
|
||||
public void AddAlarm_WithNonNameTriggerTypeToken_ReturnsCuratedError(string token)
|
||||
{
|
||||
// PROOF OF FAILURE (pre-fix): AddAlarm/UpdateAlarm built TriggerType via
|
||||
// raw `Enum.Parse`, which has none of ParseDeploymentStatus's guards.
|
||||
// Each of these tokens parsed SILENTLY (to ValueMatch, an undefined
|
||||
// value, and RangeViolation respectively — no exception at all) and the
|
||||
// command proceeded into TemplateService, failing later with
|
||||
// "Template with ID 1 not found." instead of this curated message.
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAlarmCommand(1, "AlarmX", token, 5, null, null, false),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("Unrecognised alarm trigger type", response.Error);
|
||||
Assert.Contains(token, response.Error);
|
||||
Assert.Contains("Valid values:", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddAlarm_WithTypoTriggerType_ReturnsCuratedErrorInsteadOfMaskedInternalError()
|
||||
{
|
||||
// PROOF OF FAILURE (pre-fix): unlike ParseDataType (already curated by
|
||||
// MV-10), the TriggerType sites used raw `Enum.Parse`. A typo's
|
||||
// ArgumentException is uncaught by any handler-local guard and reaches
|
||||
// MapFault as an unanticipated fault — masked to a generic
|
||||
// "An internal error occurred (CorrelationId=...)" message, exactly the
|
||||
// masking this task's defect description calls out. Post-fix,
|
||||
// ParseEnumStrict raises a curated ManagementCommandException instead.
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAlarmCommand(1, "AlarmX", "ValueMach", 5, null, null, false),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("Unrecognised alarm trigger type 'ValueMach'", response.Error);
|
||||
Assert.Contains("Valid values:", response.Error);
|
||||
Assert.DoesNotContain("internal error", response.Error);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("ValueMatch")]
|
||||
[InlineData("RangeViolation")]
|
||||
[InlineData("RateOfChange")]
|
||||
[InlineData("HiLo")]
|
||||
[InlineData("Expression")]
|
||||
public void AddAlarm_WithDefinedTriggerTypeName_ParsesAndProceedsPastGuard(string name)
|
||||
{
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAlarmCommand(1, "AlarmX", name, 5, null, null, false),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Template with ID 1 not found", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateAlarm_WithOrdinalTriggerType_ReturnsCuratedError()
|
||||
{
|
||||
_services.AddScoped<TemplateService>();
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new UpdateTemplateAlarmCommand(5, "AlarmX", "0", 5, null, null, false),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Unrecognised alarm trigger type", response.Error);
|
||||
Assert.Contains("Valid values:", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateAttribute_ValidScalar_PersistsValueAndKeepsFixedFields()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user