namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests; /// /// InboundAPI-014 / InboundAPI-M2.6: tests for return-value validation against a /// method's ReturnDefinition. Mirrors /// (shared recursive engine) — RECURSIVE nested Object / List-element type /// validation with path-qualified errors. /// /// /// Definitions are expressed as JSON Schema (the canonical persisted format); /// the legacy flat-array form is still accepted (final region). /// /// public class ReturnValueValidatorTests { // ── No definition → no validation (backward compatible) ─────────────────── [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void NoReturnDefinition_AnythingIsValid(string? returnDefinition) { var result = ReturnValueValidator.Validate("{\"anything\":1}", returnDefinition); Assert.True(result.IsValid); } [Fact] public void NoReturnDefinition_NullResult_IsValid() { var result = ReturnValueValidator.Validate(null, null); Assert.True(result.IsValid); } // ── Happy path: result matches the declared object shape ────────────────── [Fact] public void ResultMatchingDefinition_IsValid() { const string def = """ {"type":"object","properties":{ "siteName":{"type":"string"}, "totalUnits":{"type":"integer"} },"required":["siteName","totalUnits"]} """; const string json = """{"siteName":"Site Alpha","totalUnits":14250}"""; var result = ReturnValueValidator.Validate(json, def); Assert.True(result.IsValid); } [Fact] public void ResultWithListOfScalars_TypeChecked_IsValid() { const string def = """ {"type":"object","properties":{ "codes":{"type":"array","items":{"type":"integer"}} }} """; const string json = """{"codes":[1,2,3]}"""; var result = ReturnValueValidator.Validate(json, def); Assert.True(result.IsValid); } // ── Scalar / shape mismatches must be reported ──────────────────────────── [Fact] public void ResultMissingDeclaredField_IsInvalid() { const string def = """ {"type":"object","properties":{ "siteName":{"type":"string"}, "totalUnits":{"type":"integer"} },"required":["siteName","totalUnits"]} """; const string json = """{"siteName":"Site Alpha"}"""; var result = ReturnValueValidator.Validate(json, def); Assert.False(result.IsValid); Assert.Contains("totalUnits", result.ErrorMessage); } [Fact] public void ResultFieldWrongType_IsInvalid() { const string def = """{"type":"object","properties":{"totalUnits":{"type":"integer"}},"required":["totalUnits"]}"""; const string json = """{"totalUnits":"not-a-number"}"""; var result = ReturnValueValidator.Validate(json, def); Assert.False(result.IsValid); Assert.Contains("totalUnits", result.ErrorMessage); } [Fact] public void NullResultWhenStructureRequired_IsInvalid() { const string def = """{"type":"object","properties":{"siteName":{"type":"string"}},"required":["siteName"]}"""; var result = ReturnValueValidator.Validate(null, def); Assert.False(result.IsValid); } [Fact] public void NonObjectResultWhenStructureRequired_IsInvalid() { const string def = """{"type":"object","properties":{"siteName":{"type":"string"}},"required":["siteName"]}"""; var result = ReturnValueValidator.Validate("42", def); Assert.False(result.IsValid); } [Fact] public void ListFieldGivenNonArray_IsInvalid() { const string def = """{"type":"object","properties":{"lines":{"type":"array","items":{"type":"object"}}}}"""; const string json = """{"lines":"not-a-list"}"""; var result = ReturnValueValidator.Validate(json, def); Assert.False(result.IsValid); Assert.Contains("lines", result.ErrorMessage); } [Fact] public void MalformedReturnDefinition_IsInvalid() { var result = ReturnValueValidator.Validate("{\"x\":1}", "%%% not json %%%"); Assert.False(result.IsValid); } // ── Nested validation: the M2.6 core (production-report shape) ───────────── private const string ReportDef = """ { "type":"object", "properties":{ "siteName":{"type":"string"}, "totalUnits":{"type":"integer"}, "lines":{ "type":"array", "items":{ "type":"object", "properties":{ "lineName":{"type":"string"}, "units":{"type":"integer"}, "efficiency":{"type":"number"} }, "required":["lineName","units"] } } }, "required":["siteName","totalUnits","lines"] } """; [Fact] public void ValidNestedReturn_Passes() { const string json = """ { "siteName":"Site Alpha", "totalUnits":14250, "lines":[ {"lineName":"Line-1","units":8200,"efficiency":92.5}, {"lineName":"Line-2","units":6050,"efficiency":88.1} ] } """; var result = ReturnValueValidator.Validate(json, ReportDef); Assert.True(result.IsValid); } [Fact] public void WrongScalarInsideListElement_IsInvalid_WithElementIndexInPath() { // lines[1].units declared integer, given a string. const string json = """ { "siteName":"Site Alpha", "totalUnits":14250, "lines":[ {"lineName":"Line-1","units":8200}, {"lineName":"Line-2","units":"lots"} ] } """; var result = ReturnValueValidator.Validate(json, ReportDef); Assert.False(result.IsValid); Assert.Contains("'lines[1].units'", result.ErrorMessage); Assert.Contains("Integer", result.ErrorMessage); } [Fact] public void WrongListElementType_IsInvalid_WithElementIndexInPath() { // lines[0] declared object, given a scalar. const string json = """ { "siteName":"Site Alpha", "totalUnits":14250, "lines":[ 7 ] } """; var result = ReturnValueValidator.Validate(json, ReportDef); Assert.False(result.IsValid); Assert.Contains("'lines[0]'", result.ErrorMessage); Assert.Contains("Object", result.ErrorMessage); } [Fact] public void MissingRequiredNestedField_IsInvalid_PathQualified() { // lines[0].units is required but absent. const string json = """ { "siteName":"Site Alpha", "totalUnits":14250, "lines":[ {"lineName":"Line-1"} ] } """; var result = ReturnValueValidator.Validate(json, ReportDef); Assert.False(result.IsValid); Assert.Contains("missing required field", result.ErrorMessage); Assert.Contains("'lines[0].units'", result.ErrorMessage); } [Fact] public void UndeclaredNestedField_IsInvalid_PathQualified() { // lines[0].bogus is not declared on the line-item schema. const string json = """ { "siteName":"Site Alpha", "totalUnits":14250, "lines":[ {"lineName":"Line-1","units":1,"bogus":true} ] } """; var result = ReturnValueValidator.Validate(json, ReportDef); Assert.False(result.IsValid); Assert.Contains("'lines[0].bogus'", result.ErrorMessage); Assert.Contains("not a declared field", result.ErrorMessage); } // ── Empty / null edge cases ──────────────────────────────────────────────── [Fact] public void EmptyListAgainstTypedElement_Passes() { const string json = """{"siteName":"S","totalUnits":0,"lines":[]}"""; var result = ReturnValueValidator.Validate(json, ReportDef); Assert.True(result.IsValid); } [Fact] public void EmptyObjectSchema_AnythingIsValid() { const string def = """{"type":"object","properties":{}}"""; var result = ReturnValueValidator.Validate("""{"whatever":1}""", def); Assert.True(result.IsValid); } [Fact] public void NullOptionalNestedScalar_Passes() { // lines[0].efficiency is optional; explicit null is accepted. const string json = """ { "siteName":"S", "totalUnits":1, "lines":[ {"lineName":"L1","units":1,"efficiency":null} ] } """; var result = ReturnValueValidator.Validate(json, ReportDef); Assert.True(result.IsValid); } // ── Legacy flat-array backward-compat ────────────────────────────────────── [Fact] public void LegacyFlatArrayDefinition_StillAccepted() { const string def = """[{"name":"siteName","type":"String"},{"name":"totalUnits","type":"Integer"}]"""; var ok = ReturnValueValidator.Validate("""{"siteName":"A","totalUnits":1}""", def); Assert.True(ok.IsValid); var bad = ReturnValueValidator.Validate("""{"siteName":"A","totalUnits":"x"}""", def); Assert.False(bad.IsValid); Assert.Contains("totalUnits", bad.ErrorMessage); } }