using System.Text.Json; using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi; namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests; /// /// M9-T32b runtime regression: the InboundAPI RUNTIME validators /// ( / ) must /// resolve {"$ref":"lib:Name"} library references through the same seam the /// deploy-time path uses. Before this fix the runtime validators called the /// single-arg InboundApiSchema.Parse(json) with NO resolver, so a schema /// containing a $ref threw and every runtime /// invocation of the method returned a 400 ("Invalid ... definitions ...") — a /// deploy-passes/runtime-breaks defect. /// /// /// These tests assert: a VALID ref resolves and validation proceeds against the /// referenced shape; a DANGLING ref yields a CLEAR Invalid result naming the /// missing reference (NOT an opaque "Invalid ... definitions" from a swallowed /// ); and schemas WITHOUT a $ref are unaffected /// when no resolver is supplied. /// /// public class SchemaRefRuntimeValidationTests { private static JsonElement Body(string json) { using var doc = JsonDocument.Parse(json); return doc.RootElement.Clone(); } // ── ParameterValidator: $ref in parameter definitions ───────────────────── [Fact] public void ParameterValidator_ValidLibRef_ResolvesAndValidates() { // The parameter schema is a single {"$ref":"lib:OrderRequest"} that the // resolver maps to an object schema with one required integer field. const string orderRequest = """{"type":"object","properties":{"qty":{"type":"integer"}},"required":["qty"]}"""; const string paramDef = """{"$ref":"lib:OrderRequest"}"""; var ok = ParameterValidator.Validate( Body("""{"qty":5}"""), paramDef, resolveRef: name => name == "OrderRequest" ? orderRequest : null); Assert.True(ok.IsValid, ok.ErrorMessage); Assert.Equal(5L, ok.Parameters["qty"]); // And a body that violates the resolved shape is rejected with a real // type error (NOT a configuration/parse error) — proving the ref shape // is actually being enforced. var bad = ParameterValidator.Validate( Body("""{"qty":"not-a-number"}"""), paramDef, resolveRef: name => name == "OrderRequest" ? orderRequest : null); Assert.False(bad.IsValid); Assert.DoesNotContain("definitions in method configuration", bad.ErrorMessage); } [Fact] public void ParameterValidator_DanglingLibRef_ReturnsClearInvalidNamingTheRef() { // The resolver cannot find "GoneLib" — at runtime this must surface as a // CLEAR Invalid result that names the missing reference, NOT an opaque // "Invalid parameter definitions" and NOT a thrown JsonException. const string paramDef = """{"$ref":"lib:GoneLib"}"""; var result = ParameterValidator.Validate( Body("""{"anything":1}"""), paramDef, resolveRef: _ => null); Assert.False(result.IsValid); Assert.NotNull(result.ErrorMessage); Assert.Contains("GoneLib", result.ErrorMessage!, StringComparison.Ordinal); } [Fact] public void ParameterValidator_NoRef_UnchangedWithoutResolver() { // A schema with no $ref behaves exactly as before when no resolver is // supplied (the default-null resolver path). const string def = """{"type":"object","properties":{"count":{"type":"integer"}},"required":["count"]}"""; var ok = ParameterValidator.Validate(Body("""{"count":3}"""), def); Assert.True(ok.IsValid, ok.ErrorMessage); Assert.Equal(3L, ok.Parameters["count"]); var bad = ParameterValidator.Validate(Body("""{"count":"x"}"""), def); Assert.False(bad.IsValid); } // ── ReturnValueValidator: $ref in return definition ─────────────────────── [Fact] public void ReturnValueValidator_ValidLibRef_ResolvesAndValidates() { const string report = """{"type":"object","properties":{"total":{"type":"integer"}},"required":["total"]}"""; const string returnDef = """{"$ref":"lib:Report"}"""; var ok = ReturnValueValidator.Validate( """{"total":42}""", returnDef, resolveRef: name => name == "Report" ? report : null); Assert.True(ok.IsValid, ok.ErrorMessage); // A return value violating the resolved shape is rejected as a real // validation failure, not a configuration error. var bad = ReturnValueValidator.Validate( """{"total":"nope"}""", returnDef, resolveRef: name => name == "Report" ? report : null); Assert.False(bad.IsValid); Assert.DoesNotContain("definition in method configuration", bad.ErrorMessage); } [Fact] public void ReturnValueValidator_DanglingLibRef_ReturnsClearInvalidNamingTheRef() { const string returnDef = """{"$ref":"lib:GoneReturn"}"""; var result = ReturnValueValidator.Validate( """{"total":1}""", returnDef, resolveRef: _ => null); Assert.False(result.IsValid); Assert.Contains("GoneReturn", result.ErrorMessage, StringComparison.Ordinal); } [Fact] public void ReturnValueValidator_NoRef_UnchangedWithoutResolver() { const string def = """{"type":"object","properties":{"total":{"type":"integer"}},"required":["total"]}"""; var ok = ReturnValueValidator.Validate("""{"total":7}""", def); Assert.True(ok.IsValid, ok.ErrorMessage); var bad = ReturnValueValidator.Validate("""{"total":"x"}""", def); Assert.False(bad.IsValid); } }