using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening; using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation; namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Validation; /// /// M9-T32b deploy-time validation: a dangling {"$ref":"lib:Name"} in any /// validated script parameter/return schema must BLOCK deployment (a /// error naming the missing ref). /// A valid ref resolved through the supplied seam passes. The check is inert for /// schemas with no $ref (existing behaviour unchanged) and when no resolver /// is supplied and there are no refs. /// public class SchemaRefValidationTests { private readonly ValidationService _sut = new(); [Fact] public void Validate_DanglingRefInParameterDefinitions_BlocksDeploy() { var config = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Scripts = [ new ResolvedScript { CanonicalName = "CallMe", Code = "var x = 1;", ParameterDefinitions = """{"$ref":"lib:MissingLib"}""", } ] }; // Resolver knows nothing → the ref dangles. var result = _sut.Validate(config, resolveSchemaRef: _ => null); Assert.False(result.IsValid); var error = Assert.Single(result.Errors, e => e.Category == ValidationCategory.SchemaReference); Assert.Contains("MissingLib", error.Message, StringComparison.Ordinal); } [Fact] public void Validate_DanglingRefInReturnDefinition_BlocksDeploy() { var config = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Scripts = [ new ResolvedScript { CanonicalName = "CallMe", Code = "var x = 1;", ReturnDefinition = """{"$ref":"lib:GoneReturn"}""", } ] }; var result = _sut.Validate(config, resolveSchemaRef: _ => null); Assert.False(result.IsValid); Assert.Contains(result.Errors, e => e.Category == ValidationCategory.SchemaReference && e.Message.Contains("GoneReturn", StringComparison.Ordinal)); } [Fact] public void Validate_ValidRef_Passes() { const string libSchema = """{"type":"object","properties":{"qty":{"type":"integer"}}}"""; var config = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Scripts = [ new ResolvedScript { CanonicalName = "CallMe", Code = "var x = 1;", TriggerType = "Call", ParameterDefinitions = """{"$ref":"lib:OrderRequest"}""", } ] }; var result = _sut.Validate( config, resolveSchemaRef: name => name == "OrderRequest" ? libSchema : null); Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.SchemaReference); } [Fact] public void Validate_NoRefSchema_NoSchemaReferenceError() { var config = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Scripts = [ new ResolvedScript { CanonicalName = "CallMe", Code = "var x = 1;", ParameterDefinitions = """{"type":"object","properties":{"a":{"type":"integer"}}}""", } ] }; // Even with a resolver present, a schema with no $ref yields no ref errors. var result = _sut.Validate(config, resolveSchemaRef: _ => null); Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.SchemaReference); } [Fact] public void Validate_NoResolverSupplied_NoRegression() { // The default deploy/design path with no resolver and no $ref schemas must // behave exactly as before (no SchemaReference errors). var config = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Attributes = [new ResolvedAttribute { CanonicalName = "Temp", Value = "25", DataType = "Double" }], Scripts = [ new ResolvedScript { CanonicalName = "Monitor", Code = "var x = 1;", ParameterDefinitions = """{"type":"object","properties":{"a":{"type":"integer"}}}""", } ] }; var result = _sut.Validate(config); Assert.True(result.IsValid); Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.SchemaReference); } [Fact] public void Validate_CyclicRef_BlocksDeployWithoutOverflow() { var schemas = new Dictionary { ["A"] = """{"$ref":"lib:B"}""", ["B"] = """{"$ref":"lib:A"}""", }; var config = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Scripts = [ new ResolvedScript { CanonicalName = "CallMe", Code = "var x = 1;", ParameterDefinitions = """{"$ref":"lib:A"}""", } ] }; var result = _sut.Validate( config, resolveSchemaRef: name => schemas.GetValueOrDefault(name)); Assert.False(result.IsValid); Assert.Contains(result.Errors, e => e.Category == ValidationCategory.SchemaReference); } }