fix(inbound-api): M2.6 review nits — legacy required default, recursion depth guard, return-validator comment (#13)

- legacy flat-array "required":"false" (string) now treated as optional (matches migration)
- depth ceiling (32) on InboundApiSchema Parse/Validate recursion — guards against
  stack-overflow from a deeply-nested stored schema (Parse throws->400, Validate adds error)
- DocOptions.MaxDepth=128 so the application-level structural guard fires before the
  System.Text.Json reader ceiling (each schema level = ~3 JSON reader levels)
- comment the intentional ParameterValidator/ReturnValueValidator early-return asymmetry
- note intentional datetime->string legacy collapse in NormalizeType
- tests: legacy string-false optional, parse/validate depth ceiling, scalar return schema
This commit is contained in:
Joseph Doherty
2026-06-15 15:18:44 -04:00
parent 4b6187c853
commit 411d0c043b
5 changed files with 236 additions and 20 deletions
@@ -1,3 +1,5 @@
using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi;
namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests;
/// <summary>
@@ -304,4 +306,92 @@ public class ReturnValueValidatorTests
Assert.False(bad.IsValid);
Assert.Contains("totalUnits", bad.ErrorMessage);
}
// FIX 3: scalar return schema validates scalar return values ──────────────
// (Guards the intentional ParameterValidator/ReturnValueValidator asymmetry:
// ReturnValueValidator must NOT short-circuit on non-object schema types.)
[Fact]
public void ScalarStringReturnSchema_ValidatesScalarStringReturn()
{
// A {"type":"string"} return schema must accept a bare JSON string.
var result = ReturnValueValidator.Validate("\"hello\"", """{"type":"string"}""");
Assert.True(result.IsValid);
}
[Fact]
public void ScalarIntegerReturnSchema_ValidatesScalarIntegerReturn()
{
var result = ReturnValueValidator.Validate("42", """{"type":"integer"}""");
Assert.True(result.IsValid);
}
[Fact]
public void ScalarStringReturnSchema_RejectsIntegerReturn()
{
var result = ReturnValueValidator.Validate("42", """{"type":"string"}""");
Assert.False(result.IsValid);
Assert.Contains("String", result.ErrorMessage);
}
[Fact]
public void ScalarBooleanReturnSchema_ValidatesBooleanReturn()
{
var result = ReturnValueValidator.Validate("true", """{"type":"boolean"}""");
Assert.True(result.IsValid);
}
// FIX 2: recursion depth guard on Validate ─────────────────────────────────
[Fact]
public void ValidateExceedingDepthCeiling_AddsDepthError_DoesNotThrow()
{
// Build a schema programmatically (bypassing Parse) with 34 levels of
// nesting to exceed the ceiling of 32. Validate must add an error and
// return, NOT stack overflow.
//
// Parse prevents creating a >32-level schema from stored JSON, but
// InboundApiSchema is a public type constructable in code, so Validate
// must guard independently.
var deepSchema = BuildProgrammaticSchema(34);
var json = BuildDeeplyNestedValue(34);
using var doc = System.Text.Json.JsonDocument.Parse(json);
var errors = new System.Collections.Generic.List<string>();
// Must not throw — adds a depth error to the list instead.
deepSchema.Validate(doc.RootElement, string.Empty, errors);
Assert.NotEmpty(errors);
Assert.Contains("nesting too deep", errors[0], StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Constructs an <see cref="InboundApiSchema"/> with <paramref name="depth"/>
/// levels of object-nesting programmatically (bypassing <c>Parse</c>) to
/// exercise the Validate depth ceiling independently of the Parse ceiling.
/// </summary>
private static InboundApiSchema BuildProgrammaticSchema(int depth)
{
InboundApiSchema inner = new() { Type = "string" };
for (var i = 0; i < depth; i++)
{
inner = new InboundApiSchema
{
Type = "object",
Fields = [new InboundApiSchemaField("a", Required: false, inner)],
};
}
return inner;
}
private static string BuildDeeplyNestedValue(int depth)
{
var value = "\"leaf\"";
for (var i = 0; i < depth; i++)
{
value = "{\"a\":" + value + "}";
}
return value;
}
}