/// <summary>
        /// Performs validation of a method (request/response) with a given service
        /// target and zero or more test scenarios
        /// </summary>
        /// <param name="method"></param>
        /// <param name="account"></param>
        /// <param name="credentials"></param>
        /// <returns></returns>
        public static async Task<ValidationResults> ValidateServiceResponseAsync(
            this MethodDefinition method,
            ScenarioDefinition[] scenarios,
            IServiceAccount account,            
            ValidationOptions options = null)
        {
            if (null == method)
                throw new ArgumentNullException("method");
            if (null == account)
                throw new ArgumentNullException("account");
            
            ValidationResults results = new ValidationResults();

            if (scenarios.Length == 0)
            {
                // If no descenarios are defined for this method, add a new default scenario
                scenarios = new ScenarioDefinition[] {
                    new ScenarioDefinition {
                        Description = "verbatim",
                        Enabled = true,
                        MethodName = method.Identifier,
                        RequiredScopes = method.RequiredScopes
                    }
                };
//                results.AddResult("init", new ValidationMessage(null, "No scenarios were defined for method {0}. Will request verbatim from docs.", method.Identifier), ValidationOutcome.None);
            }

            if (scenarios.Any() && !scenarios.Any(x => x.Enabled))
            {
                results.AddResult("init", 
                    new ValidationWarning(ValidationErrorCode.AllScenariosDisabled, null, "All scenarios for method {0} were disabled.", method.Identifier),
                    ValidationOutcome.Skipped);
                
                return results;
            }

            foreach (var scenario in scenarios.Where(x => x.Enabled))
            {
                try
                {
                    await ValidateMethodWithScenarioAsync(method, scenario, account, results, options);
                }
                catch (Exception ex)
                {
                    results.AddResult(
                        "validation",
                        new ValidationError(
                            ValidationErrorCode.ExceptionWhileValidatingMethod,
                            method.SourceFile.DisplayName,
                            ex.Message));
                }
            }

            return results;
        }
        public ValidationOptions(ValidationOptions options)
        {
            if (null == options)
                return;

            this.AllowTruncatedResponses = options.AllowTruncatedResponses;
            this.RequiredPropertyNames = options.RequiredPropertyNames;
            this.CollectionPropertyName = options.CollectionPropertyName;
            this.ExpectedJsonSchema = options.ExpectedJsonSchema;
            this.NullablePropertyNames = options.NullablePropertyNames;
            this.RelaxedStringValidation = options.RelaxedStringValidation;
        }
        /// <summary>
        /// Examines input json string to ensure that it compiles with the JsonSchema definition. Any errors in the
        /// validation of the schema are returned via the errors out parameter.
        /// </summary>
        /// <param name="schema">Schemas definition used as a reference.</param>
        /// <param name="inputJson">Input json example to be validated</param>
        /// <param name="errors">Out parameter that provides any errors, warnings, or messages that were generated</param>
        /// <param name="expectedJson"></param>
        /// <returns></returns>
        public bool ValidateJsonCompilesWithSchema(JsonSchema schema, JsonExample inputJson, out ValidationError[] errors, JsonExample expectedJson = null, ValidationOptions options = null)
        {
            if (null == schema)
                throw new ArgumentNullException("schema");
            if (null == inputJson)
                throw new ArgumentNullException("inputJson");

            string collectionPropertyName = "value";
            if (null != inputJson.Annotation && null != inputJson.Annotation.CollectionPropertyName)
            {
                collectionPropertyName = inputJson.Annotation.CollectionPropertyName;
            }

            // If we didn't get an options, create a new one with some defaults provided by the annotation
            options = options ?? new ValidationOptions();
            options.AllowTruncatedResponses = (inputJson.Annotation ?? new CodeBlockAnnotation()).TruncatedResult;
            options.CollectionPropertyName = collectionPropertyName;

            return schema.ValidateJson(inputJson, out errors, this.registeredSchema, options, expectedJson);
        }
        /// <summary>
        /// Validate the input json against the defined scehma when the instance was created.
        /// </summary>
        /// <param name="jsonInput">Input json to validate against schema</param>
        /// <param name="errors">Array of errors if the validation fails</param>
        /// <param name="otherSchemas"></param>
        /// <param name="options"></param>
        /// <param name="expectedJson"></param>
        /// <returns>True if validation was successful, otherwise false.</returns>
        public bool ValidateJson(JsonExample jsonInput, out ValidationError[] errors, Dictionary <string, JsonSchema> otherSchemas, ValidationOptions options, JsonExample expectedJson = null)
        {
            JContainer obj;

            try
            {
                var settings = new JsonSerializerSettings {
                    DateParseHandling = DateParseHandling.None, NullValueHandling = NullValueHandling.Include, DefaultValueHandling = DefaultValueHandling.Include
                };
                obj = (JContainer)JsonConvert.DeserializeObject(jsonInput.JsonData, settings);
            }
            catch (Exception ex)
            {
                errors = new ValidationError[] { new ValidationError(ValidationErrorCode.JsonParserException, null, "Failed to parse json string: {0}. Json: {1}", ex.Message, jsonInput.JsonData) };
                return(false);
            }

            var annotation = jsonInput.Annotation ?? new CodeBlockAnnotation();

            List <ValidationError> detectedErrors = new List <ValidationError>();

            bool expectErrorObject = (jsonInput.Annotation != null) && jsonInput.Annotation.ExpectError;

            // Check for an error response
            dynamic errorObject = obj["error"];

            if (null != errorObject && !expectErrorObject)
            {
                string code    = errorObject.code;
                string message = errorObject.message;

                detectedErrors.Clear();
                detectedErrors.Add(new ValidationError(ValidationErrorCode.JsonErrorObject, null, "Error response received. Code: {0}, Message: {1}", code, message));
                errors = detectedErrors.ToArray();
                return(false);
            }
            else if (expectErrorObject && null == errorObject)
            {
                detectedErrors.Clear();
                detectedErrors.Add(new ValidationError(ValidationErrorCode.JsonErrorObjectExpected, null, "Expected an error object response, but didn't receive one."));
                errors = detectedErrors.ToArray();
                return(false);
            }

            // Check to see if this is a "collection" instance
            if (null != annotation && annotation.IsCollection)
            {
                this.ValidateCollectionObject(obj, annotation, otherSchemas, options.CollectionPropertyName, detectedErrors, options);
            }
            // otherwise verify the object matches this schema
            else
            {
                options = options ?? new ValidationOptions(annotation);
                if (null != expectedJson)
                {
                    var expectedJsonSchema = new JsonSchema(expectedJson.JsonData, expectedJson.Annotation);
                    options.ExpectedJsonSchema    = expectedJsonSchema;
                    options.RequiredPropertyNames = expectedJsonSchema.ExpectedProperties.Keys.ToArray();
                }
                this.ValidateContainerObject(obj, options, otherSchemas, detectedErrors);
            }

            errors = detectedErrors.ToArray();
            return(detectedErrors.Count == 0);
        }
        private void ValidateCollectionObject(JContainer obj, CodeBlockAnnotation annotation, Dictionary <string, JsonSchema> otherSchemas, string collectionPropertyName, List <ValidationError> detectedErrors, ValidationOptions options)
        {
            // TODO: also validate additional properties on the collection, like nextDataLink
            var collection = obj[collectionPropertyName];

            if (null == collection)
            {
                detectedErrors.Add(new ValidationError(ValidationErrorCode.MissingCollectionProperty, null, "Failed to locate collection property '{0}' in response.", collectionPropertyName));
            }
            else
            {
                var collectionMembers = obj[collectionPropertyName];
                if (!collectionMembers.Any())
                {
                    if (!annotation.IsEmpty)
                    {
                        detectedErrors.Add(
                            new ValidationWarning(
                                ValidationErrorCode.CollectionArrayEmpty,
                                null,
                                "Property contained an empty array that was not validated: {0}",
                                collectionPropertyName));
                    }
                }
                else if (annotation.IsEmpty)
                {
                    detectedErrors.Add(
                        new ValidationWarning(
                            ValidationErrorCode.CollectionArrayNotEmpty,
                            null,
                            "Property contained a non-empty array that was expected to be empty: {0}",
                            collectionPropertyName));
                }

                foreach (var jToken in collectionMembers)
                {
                    var container = jToken as JContainer;
                    if (null != container)
                    {
                        List <ValidationError> containerErrors = new List <ValidationError>();

                        var deeperOptions = new ValidationOptions(options)
                        {
                            AllowTruncatedResponses = annotation.TruncatedResult
                        };

                        this.ValidateContainerObject(
                            container,
                            deeperOptions,
                            otherSchemas,
                            containerErrors);

                        detectedErrors.AddUniqueErrors(containerErrors);
                    }
                }
            }
        }
Beispiel #6
0
        /// <summary>
        /// Verify that a property from the json-to-validate matches something in our schema
        /// </summary>
        /// <param name="inputProperty"></param>
        /// <param name="schemas"></param>
        /// <param name="detectedErrors"></param>
        /// <param name="options"></param>
        /// <returns></returns>
        private PropertyValidationOutcome ValidateProperty(JsonProperty inputProperty, Dictionary<string, JsonSchema> schemas, List<ValidationError> detectedErrors, ValidationOptions options)
        {
            if (this.ExpectedProperties.ContainsKey(inputProperty.Name))
            {
                // The property was expected to be found in this schema! Yay.
                var schemaPropertyDef = this.ExpectedProperties[inputProperty.Name];

                // Check for simple value types first
                if (this.SimpleValueTypes(schemaPropertyDef.Type, inputProperty.Type) && this.AllFalse(schemaPropertyDef.IsArray, inputProperty.IsArray))
                {
                    if (schemaPropertyDef.Type == inputProperty.Type && inputProperty.Type != JsonDataType.String)
                    {
                        return PropertyValidationOutcome.Ok;
                    }
                    else if (schemaPropertyDef.Type == inputProperty.Type && inputProperty.Type == JsonDataType.String)
                    {
                        // Perform extra validation to see if the string is the right format (iso date, enum value, url, or just a string)
                        if (null == options || options.RelaxedStringValidation)
                            return PropertyValidationOutcome.Ok;

                        return ValidateStringFormat(schemaPropertyDef, inputProperty, detectedErrors);
                    }
                    else
                    {
                        // Type of the inputProperty is mismatched from the expected value.
                        detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedTypeDifferent, null, "Expected type {0} but was instead {1}: {2}", schemaPropertyDef.Type, inputProperty.Type, inputProperty.Name));
                        return PropertyValidationOutcome.InvalidType;
                    }
                }
                else if (null == inputProperty.OriginalValue)
                {
                    if (null != this.NullableProperties && !this.NullableProperties.Contains(schemaPropertyDef.Name))
                    {
                        detectedErrors.Add(new ValidationWarning(ValidationErrorCode.NullPropertyValue, null, "Non-nullable property {0} had a null value in the response. Expected {1}.", schemaPropertyDef.Name, schemaPropertyDef.Type));
                    }
                    return PropertyValidationOutcome.Ok;
                }
                else if (schemaPropertyDef.IsArray || inputProperty.IsArray)
                {
                    // Check for an array
                    if (schemaPropertyDef.IsArray && !inputProperty.IsArray)
                    {
                        // Expected an array, but didn't get one
                        detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedArrayValue, null, "Expected an array but property was not an array: {0}", inputProperty.Name));
                        return PropertyValidationOutcome.InvalidType;
                    }
                    else if (!schemaPropertyDef.IsArray && inputProperty.IsArray)
                    {
                        detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedNonArrayValue, null, "Expected a value of type {0} but property was an array: {1}", schemaPropertyDef.Type, inputProperty.Name));
                        return PropertyValidationOutcome.InvalidType;
                    }

                    return this.ValidateArrayProperty(inputProperty, schemas, detectedErrors, options);
                }
                else if (schemaPropertyDef.Type == JsonDataType.ODataType && (inputProperty.Type == JsonDataType.Object || inputProperty.Type == JsonDataType.ODataType))
                {
                    // Compare the ODataType schema to the custom schema
                    if (!schemas.ContainsKey(schemaPropertyDef.ODataTypeName))
                    {
                        detectedErrors.Add(new ValidationError(ValidationErrorCode.ResourceTypeNotFound, null, "Missing resource: resource {0} was not found (property name '{1}').", schemaPropertyDef.ODataTypeName, inputProperty.Name));
                        return PropertyValidationOutcome.MissingResourceType;
                    }
                    else if (inputProperty.Type == JsonDataType.Object)
                    {
                        var odataSchema = schemas[schemaPropertyDef.ODataTypeName];
                        ValidationError[] odataErrors;
                        if (null != inputProperty.CustomMembers && !odataSchema.ValidateCustomObject(inputProperty.CustomMembers.Values.ToArray(), out odataErrors, schemas, options))
                        {
                            var propertyError = ValidationError.NewConsolidatedError(ValidationErrorCode.ConsolidatedError, odataErrors, "Schema validation failed on property '{0}' ['{1}']", inputProperty.Name, odataSchema.ResourceName);
                            detectedErrors.Add(propertyError);

                            return PropertyValidationOutcome.InvalidType;
                        }
                        else if (null == inputProperty.CustomMembers)
                        {
                            detectedErrors.Add(new ValidationError(ValidationErrorCode.NoCustomMembersFound, null, "Property '{0}' is of type Custom but has no custom members.", inputProperty.Name));
                        }
                        return PropertyValidationOutcome.Ok;
                    }
                    else
                    {
                        var odataSchema = schemas[schemaPropertyDef.ODataTypeName];
                        if (inputProperty.CustomMembers == null)
                        {
                            detectedErrors.Add(new ValidationError(ValidationErrorCode.MissingCustomMembers, null, "Property {0} is missing custom members and cannot be validated.", inputProperty.Name));
                            return PropertyValidationOutcome.InvalidType;
                        }
                        else
                        {
                            odataSchema.ValidateObjectProperties(inputProperty.CustomMembers.Values, options, schemas, detectedErrors);
                            return PropertyValidationOutcome.Ok;
                        }
                    }
                }
                else if (schemaPropertyDef.Type == JsonDataType.Object)
                {
                    detectedErrors.Add(new ValidationWarning(ValidationErrorCode.CustomValidationNotSupported, null, "Schema type was 'Custom' which is not supported. Add a resource type to the definition of property: {0}", inputProperty.Name));
                    return PropertyValidationOutcome.MissingResourceType;
                }
                else
                {
                    detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedTypeDifferent, null, "Type mismatch: property '{0}' [{1}] doesn't match expected type [{2}].",
                        inputProperty.Name, inputProperty.Type, schemaPropertyDef.Type));
                    return PropertyValidationOutcome.InvalidType;
                }
            }
            else
            {
                detectedErrors.Add(new ValidationWarning(ValidationErrorCode.AdditionalPropertyDetected, null, "Extra property: property '{0}' [{1}] was not expected.", inputProperty.Name, inputProperty.Type));
                return PropertyValidationOutcome.MissingFromSchema;
            }
        }
Beispiel #7
0
        /// <summary>
        /// Validates that the actual response body matches the schema defined for the response and any additional constraints
        /// from the expected request (e.g. properties that are included in the expected response are required in the actual
        /// response even if the metadata defines that the response is truncated)
        /// </summary>
        /// <param name="method"></param>
        /// <param name="actualResponse"></param>
        /// <param name="expectedResponse"></param>
        /// <param name="schemaErrors"></param>
        /// <returns></returns>
        internal bool ValidateResponseMatchesSchema(MethodDefinition method, HttpResponse actualResponse, HttpResponse expectedResponse, out ValidationError[] schemaErrors, ValidationOptions options = null)
        {
            List <ValidationError> newErrors = new List <ValidationError>();

            var expectedResourceType = method.ExpectedResponseMetadata.ResourceType;

            switch (expectedResourceType)
            {
            case "stream":
            case "Stream":
                // No validation since we're streaming data
                schemaErrors = new ValidationError[0];
                return(true);

            case "string":
            case "String":
            case "Edm.String":
            case " Edm.string":
                schemaErrors = new ValidationError[0];
                return(true);
            }

            // Get a reference of our JsonSchema that we're checking the response with
            var        expectedResponseJson = (null != expectedResponse) ? expectedResponse.Body : null;
            JsonSchema schema = this.GetJsonSchema(expectedResourceType, newErrors, expectedResponseJson);

            if (null == schema)
            {
                newErrors.Add(new ValidationError(ValidationErrorCode.ResourceTypeNotFound, null, "Unable to locate a definition for resource type: {0}", expectedResourceType));
            }
            else
            {
                ValidationError[] validationJsonOutput;
                this.ValidateJsonCompilesWithSchema(schema, new JsonExample(actualResponse.Body, method.ExpectedResponseMetadata), out validationJsonOutput, (null != expectedResponseJson) ? new JsonExample(expectedResponseJson) : null, options);
                newErrors.AddRange(validationJsonOutput);
            }

            schemaErrors = newErrors.ToArray();
            return(!schemaErrors.WereWarningsOrErrors());
        }
Beispiel #8
0
        /// <summary>
        /// Check each member of the actualProperty's array to make sure it matches the resource type specified for the property.
        /// </summary>
        /// <param name="actualProperty"></param>
        /// <param name="schemas"></param>
        /// <param name="detectedErrors"></param>
        /// <param name="options"></param>
        private PropertyValidationOutcome ValidateArrayProperty(JsonProperty actualProperty, Dictionary<string, JsonSchema> schemas, List<ValidationError> detectedErrors, ValidationOptions options)
        {
            JArray actualArray = (JArray)JsonConvert.DeserializeObject(actualProperty.OriginalValue);

            JsonSchema memberSchema;
            if (string.IsNullOrEmpty(actualProperty.ODataTypeName))
            {
                return this.ValidateSimpleArrayProperty(actualProperty, this.ExpectedProperties[actualProperty.Name], detectedErrors);
            }
            else if (!schemas.TryGetValue(actualProperty.ODataTypeName, out memberSchema))
            {
                detectedErrors.Add(new ValidationError(ValidationErrorCode.ResourceTypeNotFound, null, "Failed to locate resource definition for: {0}", actualProperty.ODataTypeName));
                return PropertyValidationOutcome.MissingResourceType;
            }

            bool hadErrors = false;
            for(int i=0; i<actualArray.Count; i++)
            {
                JContainer member = actualArray[i] as JContainer;
                if (member != null)
                {
                    List<ValidationError> memberErrors = new List<ValidationError>();
                    memberSchema.ValidateContainerObject(member, options, schemas, memberErrors);

                    hadErrors |= memberErrors.Count > 0;
                    foreach (var error in memberErrors)
                    {
                        error.Source = string.Format("{0} [{1}]", actualProperty.Name, i);
                        detectedErrors.Add(error);
                    }
                }
            }

            return hadErrors ? PropertyValidationOutcome.GenericError : PropertyValidationOutcome.Ok;
        }
Beispiel #9
0
        private bool ValidateCustomObject(JsonProperty[] properties, out ValidationError[] errors, Dictionary<string, JsonSchema> otherSchemas, ValidationOptions options)
        {
            List<string> missingProperties = new List<string>(this.ExpectedProperties.Keys);
            List<ValidationError> detectedErrors = new List<ValidationError>();
            foreach (var inputProperty in properties)
            {
                missingProperties.Remove(inputProperty.Name);
                this.ValidateProperty(inputProperty, otherSchemas, detectedErrors, new ValidationOptions());
            }

            this.CleanMissingProperties(options, missingProperties);

            errors = detectedErrors.ToArray();
            return detectedErrors.Count == 0;
        }
Beispiel #10
0
        /// <summary>
        /// Check each member of the actualProperty's array to make sure it matches the resource type specified for the property.
        /// </summary>
        /// <param name="actualProperty"></param>
        /// <param name="schemas"></param>
        /// <param name="detectedErrors"></param>
        /// <param name="options"></param>
        private PropertyValidationOutcome ValidateArrayProperty(ParameterDefinition actualProperty, Dictionary <string, JsonSchema> schemas, List <ValidationError> detectedErrors, ValidationOptions options)
        {
            JArray actualArray = (JArray)JsonConvert.DeserializeObject(actualProperty.OriginalValue);

            JsonSchema memberSchema;

            if (string.IsNullOrEmpty(actualProperty.Type.CustomTypeName))
            {
                return(this.ValidateSimpleArrayProperty(actualProperty, this.ExpectedProperties[actualProperty.Name], detectedErrors));
            }
            else if (!schemas.TryGetValue(actualProperty.Type.CustomTypeName, out memberSchema))
            {
                detectedErrors.Add(new ValidationError(ValidationErrorCode.ResourceTypeNotFound, null, "Failed to locate resource definition for: {0}", actualProperty.Type.CustomTypeName));
                return(PropertyValidationOutcome.MissingResourceType);
            }

            bool hadErrors = false;

            for (int i = 0; i < actualArray.Count; i++)
            {
                JContainer member = actualArray[i] as JContainer;
                if (member != null)
                {
                    List <ValidationError> memberErrors = new List <ValidationError>();
                    memberSchema.ValidateContainerObject(member, options, schemas, memberErrors);

                    // TODO: Filter out non-unique errors
                    hadErrors |= memberErrors.Count > 0;
                    detectedErrors.AddUniqueErrors(memberErrors);
                }
            }

            return(hadErrors ? PropertyValidationOutcome.GenericError : PropertyValidationOutcome.Ok);
        }
Beispiel #11
0
        /// <summary>
        /// Validate the input json against the defined scehma when the instance was created.
        /// </summary>
        /// <param name="jsonInput">Input json to validate against schema</param>
        /// <param name="errors">Array of errors if the validation fails</param>
        /// <param name="otherSchemas"></param>
        /// <param name="options"></param>
        /// <param name="expectedJson"></param>
        /// <returns>True if validation was successful, otherwise false.</returns>
        public bool ValidateJson(JsonExample jsonInput, out ValidationError[] errors, Dictionary<string, JsonSchema> otherSchemas, ValidationOptions options, JsonExample expectedJson = null)
        {
            JContainer obj;
            try
            {
                var settings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.None, NullValueHandling = NullValueHandling.Include, DefaultValueHandling = DefaultValueHandling.Include };
                obj = (JContainer)JsonConvert.DeserializeObject(jsonInput.JsonData, settings);
            }
            catch (Exception ex)
            {
                errors = new ValidationError[] { new ValidationError(ValidationErrorCode.JsonParserException, null, "Failed to parse json string: {0}. Json: {1}", ex.Message, jsonInput.JsonData) };
                return false;
            }

            var annotation = jsonInput.Annotation ?? new CodeBlockAnnotation();

            List<ValidationError> detectedErrors = new List<ValidationError>();

            bool expectErrorObject = (jsonInput.Annotation != null) && jsonInput.Annotation.ExpectError;

            // Check for an error response
            dynamic errorObject = obj["error"];
            if (null != errorObject && !expectErrorObject)
            {
                string code = errorObject.code;
                string message = errorObject.message;

                detectedErrors.Clear();
                detectedErrors.Add(new ValidationError(ValidationErrorCode.JsonErrorObject, null, "Error response received. Code: {0}, Message: {1}", code, message));
                errors = detectedErrors.ToArray();
                return false;
            }
            else if (expectErrorObject && null == errorObject)
            {
                detectedErrors.Clear();
                detectedErrors.Add(new ValidationError(ValidationErrorCode.JsonErrorObjectExpected, null, "Expected an error object response, but didn't receive one."));
                errors = detectedErrors.ToArray();
                return false;
            }

            // Check to see if this is a "collection" instance
            if (null != annotation && annotation.IsCollection)
            {
                this.ValidateCollectionObject(obj, annotation, otherSchemas, options.CollectionPropertyName, detectedErrors);
            }
            // otherwise verify the object matches this schema
            else
            {
                options = options ?? new ValidationOptions(annotation);
                if (null != expectedJson)
                {
                    var expectedJsonSchema = new JsonSchema(expectedJson.JsonData, expectedJson.Annotation);
                    options.ExpectedJsonSchema = expectedJsonSchema;
                    options.RequiredPropertyNames = expectedJsonSchema.ExpectedProperties.Keys.ToArray();
                }
                this.ValidateContainerObject(obj, options, otherSchemas, detectedErrors);
            }

            errors = detectedErrors.ToArray();
            return detectedErrors.Count == 0;
        }
        /// <summary>
        /// Creates a new ValidationOptions instance inheriting values from this instance, and modified for the specific options of a property.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        internal ValidationOptions CreateForProperty(string propertyName)
        {
            var newOption = new ValidationOptions
            {
                AllowTruncatedResponses = this.AllowTruncatedResponses,
                CollectionPropertyName = this.CollectionPropertyName,
                RelaxedStringValidation = this.RelaxedStringValidation
            };

            if (null != this.ExpectedJsonSchema)
            {
                var propertyData = this.ExpectedJsonSchema.Properties.FirstOrDefault(x => x.Name.Equals(propertyName));
                if (null != propertyData && null != propertyData.OriginalValue)
                {
                    // Compute a new expected schema for the property's contents.
                    newOption.ExpectedJsonSchema = new JsonSchema(propertyData.OriginalValue, new CodeBlockAnnotation());
                    newOption.RequiredPropertyNames = newOption.ExpectedJsonSchema.Properties.Select(x => x.Name).ToArray();
                }
                else
                {
                    newOption.ExpectedJsonSchema = null;
                }
            }
            else
            {
                newOption.RequiredPropertyNames = this.RequiredPropertyNames;
            }
            return newOption;
        }
Beispiel #13
0
        private void ValidateObjectProperties(IEnumerable <ParameterDefinition> propertiesOnObject, ValidationOptions options, Dictionary <string, JsonSchema> otherSchemas, List <ValidationError> detectedErrors)
        {
            List <string> missingProperties = new List <string>();

            missingProperties.AddRange(from m in this.ExpectedProperties select m.Key);

            foreach (var property in propertiesOnObject)
            {
                missingProperties.Remove(property.Name);

                // This detects bad types, extra properties, etc.
                if (null != options && (property.Type.IsCollection || property.Type.IsObject))
                {
                    var propertyOptions = options.CreateForProperty(property.Name);
                    this.ValidateProperty(property, otherSchemas, detectedErrors, propertyOptions);
                }
                else
                {
                    this.ValidateProperty(property, otherSchemas, detectedErrors, options);
                }
            }

            this.CleanMissingProperties(options, missingProperties);
            if (missingProperties.Count > 0)
            {
                detectedErrors.Add(new ValidationError(ValidationErrorCode.RequiredPropertiesMissing, null, "Missing properties: response was missing these required properties: {0}", missingProperties.ComponentsJoinedByString(", ")));
            }
        }
Beispiel #14
0
        /// <summary>
        /// Verify that a property from the json-to-validate matches something in our schema
        /// </summary>
        /// <param name="inputProperty"></param>
        /// <param name="schemas"></param>
        /// <param name="detectedErrors"></param>
        /// <param name="options"></param>
        /// <returns></returns>
        private PropertyValidationOutcome ValidateProperty(ParameterDefinition inputProperty, Dictionary<string, JsonSchema> schemas, List<ValidationError> detectedErrors, ValidationOptions options)
        {
            if (this.ExpectedProperties.ContainsKey(inputProperty.Name))
            {
                // The property was expected to be found in this schema! Yay.
                var schemaPropertyDef = this.ExpectedProperties[inputProperty.Name];

                // Check for simple value types first
                if (this.SimpleValueTypes(schemaPropertyDef.Type, inputProperty.Type) && this.AllFalse(schemaPropertyDef.Type.IsCollection, inputProperty.Type.IsCollection))
                {
                    return ValidateSameDataType(schemaPropertyDef, inputProperty, detectedErrors, (null != options) ? options.RelaxedStringValidation : false);
                }
                else if (null == inputProperty.OriginalValue)
                {
                    if (null != this.NullableProperties && !this.NullableProperties.Contains(schemaPropertyDef.Name))
                    {
                        detectedErrors.Add(new ValidationWarning(ValidationErrorCode.NullPropertyValue, null, "Non-nullable property {0} had a null value in the response. Expected {1}.", schemaPropertyDef.Name, schemaPropertyDef.Type));
                    }
                    return PropertyValidationOutcome.Ok;
                }
                else if (schemaPropertyDef.Type.IsCollection || inputProperty.Type.IsCollection)
                {
                    // Check for an array
                    if (schemaPropertyDef.Type.IsCollection && !inputProperty.Type.IsCollection)
                    {
                        // Expected an array, but didn't get one
                        detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedArrayValue, null, "Expected an array but property was not an array: {0}", inputProperty.Name));
                        return PropertyValidationOutcome.InvalidType;
                    }
                    else if (!schemaPropertyDef.Type.IsCollection && inputProperty.Type.IsCollection)
                    {
                        detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedNonArrayValue, null, "Expected a value of type {0} but property was an array: {1}", schemaPropertyDef.Type, inputProperty.Name));
                        return PropertyValidationOutcome.InvalidType;
                    }

                    return this.ValidateArrayProperty(inputProperty, schemas, detectedErrors, options);
                }
                else if (schemaPropertyDef.Type.IsObject && inputProperty.Type.IsObject)
                {
                    // Compare the ODataType schema to the custom schema
                    if (null == schemaPropertyDef.Type.CustomTypeName || !schemas.ContainsKey(schemaPropertyDef.Type.CustomTypeName))
                    {
                        detectedErrors.Add(new ValidationError(ValidationErrorCode.ResourceTypeNotFound, null, "Missing resource: resource {0} was not found (property name '{1}').", schemaPropertyDef.Type.CustomTypeName, inputProperty.Name));
                        return PropertyValidationOutcome.MissingResourceType;
                    }
                    else if (inputProperty.Type.IsObject)
                    {
                        var odataSchema = schemas[schemaPropertyDef.Type.CustomTypeName];
                        ValidationError[] odataErrors;
                        if (null != inputProperty.Type.CustomMembers && !odataSchema.ValidateCustomObject(inputProperty.Type.CustomMembers.ToArray(), out odataErrors, schemas, options))
                        {
                            var propertyError = ValidationError.NewConsolidatedError(ValidationErrorCode.ConsolidatedError, odataErrors, "Schemas validation failed on property '{0}' ['{1}']", inputProperty.Name, odataSchema.ResourceName);
                            detectedErrors.Add(propertyError);

                            return PropertyValidationOutcome.InvalidType;
                        }
                        else if (null == inputProperty.Type.CustomMembers)
                        {
                            detectedErrors.Add(new ValidationError(ValidationErrorCode.NoCustomMembersFound, null, "Property '{0}' is of type Custom but has no custom members.", inputProperty.Name));
                        }
                        return PropertyValidationOutcome.Ok;
                    }
                    else
                    {
                        var odataSchema = schemas[schemaPropertyDef.Type.CustomTypeName];
                        if (inputProperty.Type.CustomMembers == null)
                        {
                            detectedErrors.Add(new ValidationError(ValidationErrorCode.MissingCustomMembers, null, "Property {0} is missing custom members and cannot be validated.", inputProperty.Name));
                            return PropertyValidationOutcome.InvalidType;
                        }
                        else
                        {
                            odataSchema.ValidateObjectProperties(inputProperty.Type.CustomMembers, options, schemas, detectedErrors);
                            return PropertyValidationOutcome.Ok;
                        }
                    }
                }
                else if (schemaPropertyDef.Type.IsObject)
                {
                    detectedErrors.Add(new ValidationWarning(ValidationErrorCode.CustomValidationNotSupported, null, "Schemas type was 'Custom' which is not supported. Add a resource type to the definition of property: {0}", inputProperty.Name));
                    return PropertyValidationOutcome.MissingResourceType;
                }
                else
                {
                    detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedTypeDifferent, null, "Type mismatch: property '{0}' [{1}] doesn't match expected type [{2}].",
                        inputProperty.Name, inputProperty.Type, schemaPropertyDef.Type));
                    return PropertyValidationOutcome.InvalidType;
                }
            }
            else
            {

                // Check to see if this property is on the ignorable list
                string[] ignorableUndocumentedProperties = this.OriginalResource?.SourceFile.Parent.Requirements?.IgnorableProperties;

                string propertyName = inputProperty.Name;
                string annotationName = null;
                var indexOfAtSign = propertyName.IndexOf('@');
                if (indexOfAtSign > 0)
                {
                    // [email protected] is an example of what we're looking for here
                    annotationName = propertyName.Substring(indexOfAtSign);
                    propertyName = propertyName.Substring(0, indexOfAtSign);
                }

                if (null != annotationName)
                {
                    // Check to see if propertyName is known or not. If it isn't known, fail.
                    if (this.Properties.Any(x => x.Name.Equals(propertyName)))
                    {
                        // If the cleaned up propertyName is known, then check to see if the annotation is ignorable
                        if (null != ignorableUndocumentedProperties && ignorableUndocumentedProperties.Contains(annotationName))
                        {
                            // If we know of both the property and the annotation, we're good.
                            return PropertyValidationOutcome.Ok;
                        }
                    }
                }

                if (null != ignorableUndocumentedProperties && ignorableUndocumentedProperties.Contains(propertyName))
                {
                    return PropertyValidationOutcome.Ok;
                }

                // This property isn't legit
                detectedErrors.Add(new ValidationWarning(ValidationErrorCode.AdditionalPropertyDetected, null, "Undocumented property '{0}' [{1}] was not expected on resource {2}.", inputProperty.Name, inputProperty.Type, this.ResourceName));
                return PropertyValidationOutcome.MissingFromSchema;

            }
        }
Beispiel #15
0
        private void ValidateCollectionObject(JContainer obj, CodeBlockAnnotation annotation, Dictionary<string, JsonSchema> otherSchemas, string collectionPropertyName, List<ValidationError> detectedErrors, ValidationOptions options)
        {
            // TODO: also validate additional properties on the collection, like nextDataLink
            var collection = obj[collectionPropertyName];
            if (null == collection)
            {
                detectedErrors.Add(new ValidationError(ValidationErrorCode.MissingCollectionProperty, null, "Failed to locate collection property '{0}' in response.", collectionPropertyName));
            }
            else
            {
                var collectionMembers = obj[collectionPropertyName];
                if (!collectionMembers.Any())
                {
                    if (!annotation.IsEmpty)
                    {
                        detectedErrors.Add(
                            new ValidationWarning(
                                ValidationErrorCode.CollectionArrayEmpty,
                                null,
                                "Property contained an empty array that was not validated: {0}",
                                collectionPropertyName));
                    }
                }
                else if (annotation.IsEmpty)
                {
                    detectedErrors.Add(
                        new ValidationWarning(
                            ValidationErrorCode.CollectionArrayNotEmpty,
                            null,
                            "Property contained a non-empty array that was expected to be empty: {0}",
                            collectionPropertyName));
                }

                foreach (var jToken in collectionMembers)
                {

                    var container = jToken as JContainer;
                    if (null != container)
                    {
                        List<ValidationError> containerErrors = new List<ValidationError>();

                        var deeperOptions = new ValidationOptions(options)
                        {
                            AllowTruncatedResponses = annotation.TruncatedResult
                        };

                        this.ValidateContainerObject(
                            container,
                            deeperOptions,
                            otherSchemas,
                            containerErrors);

                        detectedErrors.AddUniqueErrors(containerErrors);
                    }
                }
            }
        }
Beispiel #16
0
        /// <summary>
        /// Check each member of the actualProperty's array to make sure it matches the resource type specified for the property.
        /// </summary>
        /// <param name="actualProperty"></param>
        /// <param name="schemas"></param>
        /// <param name="detectedErrors"></param>
        /// <param name="options"></param>
        private PropertyValidationOutcome ValidateArrayProperty(ParameterDefinition actualProperty, Dictionary<string, JsonSchema> schemas, List<ValidationError> detectedErrors, ValidationOptions options)
        {
            JArray actualArray = null;
            try
            {
                actualArray = (JArray)JsonConvert.DeserializeObject(actualProperty.OriginalValue);
            }
            catch (InvalidCastException ex)
            {
                throw new InvalidCastException($"Property {actualProperty.Name} expected to be an array, but failed to cast value to an array: {actualProperty.OriginalValue}");
            }

            var expectedPropertyDefinition = this.ExpectedProperties[actualProperty.Name];
            JsonSchema memberSchema = null;
            if (actualProperty.Type.CollectionResourceType == SimpleDataType.Object && expectedPropertyDefinition.Type.CustomTypeName != null)
            {
                // We have an ambigious array, but we know what it's supposed to be so let's use that
                schemas.TryGetValue(expectedPropertyDefinition.Type.CustomTypeName, out memberSchema);
            }

            if (memberSchema == null && string.IsNullOrEmpty(actualProperty.Type.CustomTypeName))
            {
                return this.ValidateSimpleArrayProperty(actualProperty, this.ExpectedProperties[actualProperty.Name], detectedErrors);
            }
            else if (memberSchema == null && !schemas.TryGetValue(actualProperty.Type.CustomTypeName, out memberSchema))
            {
                detectedErrors.Add(new ValidationError(ValidationErrorCode.ResourceTypeNotFound, null, "Failed to locate resource definition for: {0}", actualProperty.Type.CustomTypeName));
                return PropertyValidationOutcome.MissingResourceType;
            }

            bool hadErrors = false;
            for(int i=0; i<actualArray.Count; i++)
            {
                JContainer member = actualArray[i] as JContainer;
                if (member != null)
                {
                    List<ValidationError> memberErrors = new List<ValidationError>();
                    memberSchema.ValidateContainerObject(member, options, schemas, memberErrors);

                    // TODO: Filter out non-unique errors
                    hadErrors |= memberErrors.Count > 0;
                    detectedErrors.AddUniqueErrors(memberErrors);
                }
            }

            return hadErrors ? PropertyValidationOutcome.GenericError : PropertyValidationOutcome.Ok;
        }
Beispiel #17
0
        /// <summary>
        /// Validates the value of json according to an implicit schmea defined by expectedJson
        /// </summary>
        /// <param name="expectedResponseAnnotation"></param>
        /// <param name="actualResponseBodyJson"></param>
        /// <param name="errors"></param>
        /// <returns></returns>
        public bool ValidateJsonExample(CodeBlockAnnotation expectedResponseAnnotation, string actualResponseBodyJson, out ValidationError[] errors, ValidationOptions options = null)
        {
            List <ValidationError> newErrors = new List <ValidationError>();

            var resourceType = expectedResponseAnnotation.ResourceType;

            if (resourceType == "stream")
            {
                // No validation since we're streaming data
                errors = null;
                return(true);
            }
            else
            {
                JsonSchema schema;
                if (string.IsNullOrEmpty(resourceType))
                {
                    schema = JsonSchema.EmptyResponseSchema;
                }
                else if (!this.registeredSchema.TryGetValue(resourceType, out schema))
                {
                    newErrors.Add(new ValidationWarning(ValidationErrorCode.ResponseResourceTypeMissing, null, "Missing required resource: {0}. Validation limited to basics only.", resourceType));
                    // Create a new schema based on what's avaiable in the json
                    schema = new JsonSchema(actualResponseBodyJson, new CodeBlockAnnotation {
                        ResourceType = expectedResponseAnnotation.ResourceType
                    });
                }

                ValidationError[] validationJsonOutput;
                this.ValidateJsonCompilesWithSchema(schema, new JsonExample(actualResponseBodyJson, expectedResponseAnnotation), out validationJsonOutput, options: options);

                newErrors.AddRange(validationJsonOutput);
                errors = newErrors.ToArray();
                return(errors.Length == 0);
            }
        }
Beispiel #18
0
        /// <summary>
        /// Examines input json string to ensure that it compiles with the JsonSchema definition. Any errors in the
        /// validation of the schema are returned via the errors out parameter.
        /// </summary>
        /// <param name="schema">Schemas definition used as a reference.</param>
        /// <param name="inputJson">Input json example to be validated</param>
        /// <param name="errors">Out parameter that provides any errors, warnings, or messages that were generated</param>
        /// <param name="expectedJson"></param>
        /// <returns></returns>
        public bool ValidateJsonCompilesWithSchema(JsonSchema schema, JsonExample inputJson, out ValidationError[] errors, JsonExample expectedJson = null, ValidationOptions options = null)
        {
            if (null == schema)
            {
                throw new ArgumentNullException("schema");
            }
            if (null == inputJson)
            {
                throw new ArgumentNullException("inputJson");
            }

            string collectionPropertyName = "value";

            if (null != inputJson.Annotation && null != inputJson.Annotation.CollectionPropertyName)
            {
                collectionPropertyName = inputJson.Annotation.CollectionPropertyName;
            }

            // If we didn't get an options, create a new one with some defaults provided by the annotation
            options = options ?? new ValidationOptions();
            options.AllowTruncatedResponses = (inputJson.Annotation ?? new CodeBlockAnnotation()).TruncatedResult;
            options.CollectionPropertyName  = collectionPropertyName;

            return(schema.ValidateJson(inputJson, out errors, this.registeredSchema, options, expectedJson));
        }
Beispiel #19
0
        /// <summary>
        /// Verify that a property from the json-to-validate matches something in our schema
        /// </summary>
        /// <param name="inputProperty"></param>
        /// <param name="schemas"></param>
        /// <param name="detectedErrors"></param>
        /// <param name="options"></param>
        /// <returns></returns>
        private PropertyValidationOutcome ValidateProperty(ParameterDefinition inputProperty, Dictionary <string, JsonSchema> schemas, List <ValidationError> detectedErrors, ValidationOptions options)
        {
            if (this.ExpectedProperties.ContainsKey(inputProperty.Name))
            {
                // The property was expected to be found in this schema! Yay.
                var schemaPropertyDef = this.ExpectedProperties[inputProperty.Name];

                // Check for simple value types first
                if (this.SimpleValueTypes(schemaPropertyDef.Type, inputProperty.Type) && this.AllFalse(schemaPropertyDef.Type.IsCollection, inputProperty.Type.IsCollection))
                {
                    return(ValidateSameDataType(schemaPropertyDef, inputProperty, detectedErrors, (null != options) ? options.RelaxedStringValidation : false));
                }
                else if (null == inputProperty.OriginalValue)
                {
                    if (null != this.NullableProperties && !this.NullableProperties.Contains(schemaPropertyDef.Name))
                    {
                        detectedErrors.Add(new ValidationWarning(ValidationErrorCode.NullPropertyValue, null, "Non-nullable property {0} had a null value in the response. Expected {1}.", schemaPropertyDef.Name, schemaPropertyDef.Type));
                    }
                    return(PropertyValidationOutcome.Ok);
                }
                else if (schemaPropertyDef.Type.IsCollection || inputProperty.Type.IsCollection)
                {
                    // Check for an array
                    if (schemaPropertyDef.Type.IsCollection && !inputProperty.Type.IsCollection)
                    {
                        // Expected an array, but didn't get one
                        detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedArrayValue, null, "Expected an array but property was not an array: {0}", inputProperty.Name));
                        return(PropertyValidationOutcome.InvalidType);
                    }
                    else if (!schemaPropertyDef.Type.IsCollection && inputProperty.Type.IsCollection)
                    {
                        detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedNonArrayValue, null, "Expected a value of type {0} but property was an array: {1}", schemaPropertyDef.Type, inputProperty.Name));
                        return(PropertyValidationOutcome.InvalidType);
                    }

                    return(this.ValidateArrayProperty(inputProperty, schemas, detectedErrors, options));
                }
                else if (schemaPropertyDef.Type.IsObject && inputProperty.Type.IsObject)
                {
                    // Compare the ODataType schema to the custom schema
                    if (null == schemaPropertyDef.Type.CustomTypeName || !schemas.ContainsKey(schemaPropertyDef.Type.CustomTypeName))
                    {
                        detectedErrors.Add(new ValidationError(ValidationErrorCode.ResourceTypeNotFound, null, "Missing resource: resource {0} was not found (property name '{1}').", schemaPropertyDef.Type.CustomTypeName, inputProperty.Name));
                        return(PropertyValidationOutcome.MissingResourceType);
                    }
                    else if (inputProperty.Type.IsObject)
                    {
                        var odataSchema = schemas[schemaPropertyDef.Type.CustomTypeName];
                        ValidationError[] odataErrors;
                        if (null != inputProperty.Type.CustomMembers && !odataSchema.ValidateCustomObject(inputProperty.Type.CustomMembers.ToArray(), out odataErrors, schemas, options))
                        {
                            var propertyError = ValidationError.NewConsolidatedError(ValidationErrorCode.ConsolidatedError, odataErrors, "Schemas validation failed on property '{0}' ['{1}']", inputProperty.Name, odataSchema.ResourceName);
                            detectedErrors.Add(propertyError);

                            return(PropertyValidationOutcome.InvalidType);
                        }
                        else if (null == inputProperty.Type.CustomMembers)
                        {
                            detectedErrors.Add(new ValidationError(ValidationErrorCode.NoCustomMembersFound, null, "Property '{0}' is of type Custom but has no custom members.", inputProperty.Name));
                        }
                        return(PropertyValidationOutcome.Ok);
                    }
                    else
                    {
                        var odataSchema = schemas[schemaPropertyDef.Type.CustomTypeName];
                        if (inputProperty.Type.CustomMembers == null)
                        {
                            detectedErrors.Add(new ValidationError(ValidationErrorCode.MissingCustomMembers, null, "Property {0} is missing custom members and cannot be validated.", inputProperty.Name));
                            return(PropertyValidationOutcome.InvalidType);
                        }
                        else
                        {
                            odataSchema.ValidateObjectProperties(inputProperty.Type.CustomMembers, options, schemas, detectedErrors);
                            return(PropertyValidationOutcome.Ok);
                        }
                    }
                }
                else if (schemaPropertyDef.Type.IsObject)
                {
                    detectedErrors.Add(new ValidationWarning(ValidationErrorCode.CustomValidationNotSupported, null, "Schemas type was 'Custom' which is not supported. Add a resource type to the definition of property: {0}", inputProperty.Name));
                    return(PropertyValidationOutcome.MissingResourceType);
                }
                else
                {
                    detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedTypeDifferent, null, "Type mismatch: property '{0}' [{1}] doesn't match expected type [{2}].",
                                                           inputProperty.Name, inputProperty.Type, schemaPropertyDef.Type));
                    return(PropertyValidationOutcome.InvalidType);
                }
            }
            else
            {
                detectedErrors.Add(new ValidationWarning(ValidationErrorCode.AdditionalPropertyDetected, null, "Undocumented property '{0}' [{1}] was not expected.", inputProperty.Name, inputProperty.Type));
                return(PropertyValidationOutcome.MissingFromSchema);
            }
        }
        /// <summary>
        /// Validates that the actual response body matches the schema defined for the response and any additional constraints
        /// from the expected request (e.g. properties that are included in the expected response are required in the actual 
        /// response even if the metadata defines that the response is truncated)
        /// </summary>
        /// <param name="method"></param>
        /// <param name="actualResponse"></param>
        /// <param name="expectedResponse"></param>
        /// <param name="schemaErrors"></param>
        /// <returns></returns>
        internal bool ValidateResponseMatchesSchema(MethodDefinition method, HttpResponse actualResponse, HttpResponse expectedResponse, out ValidationError[] schemaErrors, ValidationOptions options = null)
        {
            List<ValidationError> newErrors = new List<ValidationError>();

            var expectedResourceType = method.ExpectedResponseMetadata.ResourceType;

            switch (expectedResourceType)
            {
                case "stream":
                case "Stream":
                    // No validation since we're streaming data
                    schemaErrors = new ValidationError[0];
                    return true;
                case "string":
                case "String":
                case "Edm.String":
                case" Edm.string":
                    schemaErrors = new ValidationError[0];
                    return true;
            }

            // Get a reference of our JsonSchema that we're checking the response with
            var expectedResponseJson = (null != expectedResponse) ? expectedResponse.Body : null;
            JsonSchema schema = this.GetJsonSchema(expectedResourceType, newErrors, expectedResponseJson);

            if (null == schema)
            {
                newErrors.Add(new ValidationError(ValidationErrorCode.ResourceTypeNotFound, null, "Unable to locate a definition for resource type: {0}", expectedResourceType));
            }
            else
            {
                ValidationError[] validationJsonOutput;
                this.ValidateJsonCompilesWithSchema(schema, new JsonExample(actualResponse.Body, method.ExpectedResponseMetadata), out validationJsonOutput, (null != expectedResponseJson) ? new JsonExample(expectedResponseJson) : null, options);
                newErrors.AddRange(validationJsonOutput);
            }

            schemaErrors = newErrors.ToArray();
            return !schemaErrors.WereWarningsOrErrors();
        }
Beispiel #21
0
        private bool ValidateCustomObject(ParameterDefinition[] properties, out ValidationError[] errors, Dictionary <string, JsonSchema> otherSchemas, ValidationOptions options)
        {
            List <string>          missingProperties = new List <string>(this.ExpectedProperties.Keys);
            List <ValidationError> detectedErrors    = new List <ValidationError>();

            foreach (var inputProperty in properties)
            {
                missingProperties.Remove(inputProperty.Name);
                this.ValidateProperty(inputProperty, otherSchemas, detectedErrors, options);
            }

            this.CleanMissingProperties(options, missingProperties);

            errors = detectedErrors.ToArray();
            return(detectedErrors.Count == 0);
        }
        /// <summary>
        /// Validates that a particular HttpResponse matches the method definition and optionally the expected response.
        /// </summary>
        /// <param name="method">Method definition that was used to generate a request.</param>
        /// <param name="actualResponse">Actual response from the service (this is what we validate).</param>
        /// <param name="expectedResponse">Prototype response (expected) that shows what a valid response should look like.</param>
        /// <param name="scenario">A test scenario used to generate the response, which may include additional parameters to verify.</param>
        /// <param name="errors">A collection of errors, warnings, and verbose messages generated by this process.</param>
        public void ValidateResponse(HttpResponse actualResponse, HttpResponse expectedResponse, ScenarioDefinition scenario, out ValidationError[] errors, ValidationOptions options = null)
        {
            if (null == actualResponse) throw new ArgumentNullException("actualResponse");

            List<ValidationError> detectedErrors = new List<ValidationError>();

            // Verify the request is valid (headers, etc)
            this.VerifyHttpRequest(detectedErrors);

            // Verify that the expected response headers match the actual response headers
            ValidationError[] httpErrors;
            if (null != expectedResponse && !expectedResponse.ValidateResponseHeaders(actualResponse, out httpErrors, (null != scenario) ? scenario.AllowedStatusCodes : null))
            {
                detectedErrors.AddRange(httpErrors);
            }

            // Verify the actual response body is correct according to the schema defined for the response
            ValidationError[] bodyErrors;
            this.VerifyResponseBody(actualResponse, expectedResponse, out bodyErrors, options);
            detectedErrors.AddRange(bodyErrors);

            // Verify any expectations in the scenario are met
            if (null != scenario)
            {
                scenario.ValidateExpectations(actualResponse, detectedErrors);
            }

            errors = detectedErrors.ToArray();
        }
Beispiel #23
0
        /// <summary>
        /// Modifies the missingProperties list to remove optional properties 
        /// and handle truncated or required properties.
        /// </summary>
        /// <param name="options"></param>
        /// <param name="missingProperties"></param>
        private void CleanMissingProperties(ValidationOptions options, List<string> missingProperties)
        {
            if (null == options)
                return;

            // Ignore any missing properties that are defined in the schema as optional
            missingProperties.RemoveRange(this.OptionalProperties);

            if (ValidationConfig.ExpectedResponseAsRequiredProperties &&
                options.AllowTruncatedResponses && null != options.RequiredPropertyNames)
            {
                // Ignore any missing properties that aren't in the required properties list
                missingProperties.IntersectInPlace(options.RequiredPropertyNames);
            }
            else if (options.AllowTruncatedResponses)
            {
                // Ignore all missing properties
                missingProperties.Clear();
            }
        }
        /// <summary>
        /// Verify that the body of the actual response is consistent with the method definition and expected response parameters
        /// </summary>
        /// <param name="method">The MethodDefinition that generated the response.</param>
        /// <param name="actualResponse">The actual response from the service to validate.</param>
        /// <param name="expectedResponse">The prototype expected response from the service.</param>
        /// <param name="detectedErrors">A collection of errors that will be appended with any detected errors</param>
        private void VerifyResponseBody(HttpResponse actualResponse, HttpResponse expectedResponse, out ValidationError[] errors, ValidationOptions options = null)
        {
            List<ValidationError> detectedErrors = new List<ValidationError>();

            if (string.IsNullOrEmpty(actualResponse.Body) &&
                (expectedResponse != null && !string.IsNullOrEmpty(expectedResponse.Body)))
            {
                detectedErrors.Add(new ValidationError(ValidationErrorCode.HttpBodyExpected, null, "Body missing from response (expected response includes a body or a response type was provided)."));
            }
            else if (!string.IsNullOrEmpty(actualResponse.Body))
            {
                ValidationError[] schemaErrors;
                if (this.ExpectedResponseMetadata == null ||
                    (string.IsNullOrEmpty(this.ExpectedResponseMetadata.ResourceType) &&
                     (expectedResponse != null && !string.IsNullOrEmpty(expectedResponse.Body))))
                {
                    detectedErrors.Add(new ValidationError(ValidationErrorCode.ResponseResourceTypeMissing, null, "Expected a response, but resource type on method is missing: {0}", this.Identifier));
                }
                else
                {
                    var otherResources = this.SourceFile.Parent.ResourceCollection;
                    if (
                        !otherResources.ValidateResponseMatchesSchema(
                            this,
                            actualResponse,
                            expectedResponse,
                            out schemaErrors, 
                            options))
                    {
                        detectedErrors.AddRange(schemaErrors);
                    }
                }

                var responseValidation = actualResponse.IsResponseValid(
                    this.SourceFile.DisplayName,
                    this.SourceFile.Parent.Requirements);
                detectedErrors.AddRange(responseValidation.Messages);
            }

            errors = detectedErrors.ToArray();
        }
Beispiel #25
0
        /// <summary>
        /// Verify that a Json container (object) is valid according to it's resource name (schema).
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="options"></param>
        /// <param name="otherSchemas"></param>
        /// <param name="detectedErrors"></param>
        private void ValidateContainerObject(JContainer obj, ValidationOptions options, Dictionary<string, JsonSchema> otherSchemas, List<ValidationError> detectedErrors)
        {
            var containerProperties = from p in obj
                                      select ParseProperty(p, this, detectedErrors);

            this.ValidateObjectProperties(containerProperties.Where(x => null != x), options, otherSchemas, detectedErrors);
        }
        /// <summary>
        /// Verify that a property from the json-to-validate matches something in our schema
        /// </summary>
        /// <param name="inputProperty"></param>
        /// <param name="schemas"></param>
        /// <param name="detectedErrors"></param>
        /// <param name="options"></param>
        /// <returns></returns>
        private PropertyValidationOutcome ValidateProperty(ParameterDefinition inputProperty, Dictionary <string, JsonSchema> schemas, List <ValidationError> detectedErrors, ValidationOptions options)
        {
            if (this.ExpectedProperties.ContainsKey(inputProperty.Name))
            {
                // The property was expected to be found in this schema! Yay.
                var schemaPropertyDef = this.ExpectedProperties[inputProperty.Name];

                // Check for simple value types first
                if (this.SimpleValueTypes(schemaPropertyDef.Type, inputProperty.Type) && this.AllFalse(schemaPropertyDef.Type.IsCollection, inputProperty.Type.IsCollection))
                {
                    return(ValidateSameDataType(schemaPropertyDef, inputProperty, detectedErrors, (null != options) ? options.RelaxedStringValidation : false));
                }
                else if (null == inputProperty.OriginalValue)
                {
                    if (null != this.NullableProperties && !this.NullableProperties.Contains(schemaPropertyDef.Name))
                    {
                        detectedErrors.Add(new ValidationWarning(ValidationErrorCode.NullPropertyValue, null, "Non-nullable property {0} had a null value in the response. Expected {1}.", schemaPropertyDef.Name, schemaPropertyDef.Type));
                    }
                    return(PropertyValidationOutcome.Ok);
                }
                else if (schemaPropertyDef.Type.IsCollection || inputProperty.Type.IsCollection)
                {
                    // Check for an array
                    if (schemaPropertyDef.Type.IsCollection && !inputProperty.Type.IsCollection)
                    {
                        // Expected an array, but didn't get one
                        detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedArrayValue, null, "Expected an array but property was not an array: {0}", inputProperty.Name));
                        return(PropertyValidationOutcome.InvalidType);
                    }
                    else if (!schemaPropertyDef.Type.IsCollection && inputProperty.Type.IsCollection)
                    {
                        detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedNonArrayValue, null, "Expected a value of type {0} but property was an array: {1}", schemaPropertyDef.Type, inputProperty.Name));
                        return(PropertyValidationOutcome.InvalidType);
                    }

                    return(this.ValidateArrayProperty(inputProperty, schemas, detectedErrors, options));
                }
                else if (schemaPropertyDef.Type.IsObject && inputProperty.Type.IsObject)
                {
                    // Compare the ODataType schema to the custom schema
                    if (null == schemaPropertyDef.Type.CustomTypeName || !schemas.ContainsKey(schemaPropertyDef.Type.CustomTypeName))
                    {
                        detectedErrors.Add(new ValidationError(ValidationErrorCode.ResourceTypeNotFound, null, "Missing resource: resource {0} was not found (property name '{1}').", schemaPropertyDef.Type.CustomTypeName, inputProperty.Name));
                        return(PropertyValidationOutcome.MissingResourceType);
                    }
                    else if (inputProperty.Type.IsObject)
                    {
                        var odataSchema = schemas[schemaPropertyDef.Type.CustomTypeName];
                        ValidationError[] odataErrors;
                        if (null != inputProperty.Type.CustomMembers && !odataSchema.ValidateCustomObject(inputProperty.Type.CustomMembers.ToArray(), out odataErrors, schemas, options))
                        {
                            var propertyError = ValidationError.NewConsolidatedError(ValidationErrorCode.ConsolidatedError, odataErrors, "Schemas validation failed on property '{0}' ['{1}']", inputProperty.Name, odataSchema.ResourceName);
                            detectedErrors.Add(propertyError);

                            return(PropertyValidationOutcome.InvalidType);
                        }
                        else if (null == inputProperty.Type.CustomMembers)
                        {
                            detectedErrors.Add(new ValidationError(ValidationErrorCode.NoCustomMembersFound, null, "Property '{0}' is of type Custom but has no custom members.", inputProperty.Name));
                        }
                        return(PropertyValidationOutcome.Ok);
                    }
                    else
                    {
                        var odataSchema = schemas[schemaPropertyDef.Type.CustomTypeName];
                        if (inputProperty.Type.CustomMembers == null)
                        {
                            detectedErrors.Add(new ValidationError(ValidationErrorCode.MissingCustomMembers, null, "Property {0} is missing custom members and cannot be validated.", inputProperty.Name));
                            return(PropertyValidationOutcome.InvalidType);
                        }
                        else
                        {
                            odataSchema.ValidateObjectProperties(inputProperty.Type.CustomMembers, options, schemas, detectedErrors);
                            return(PropertyValidationOutcome.Ok);
                        }
                    }
                }
                else if (schemaPropertyDef.Type.IsObject)
                {
                    detectedErrors.Add(new ValidationWarning(ValidationErrorCode.CustomValidationNotSupported, null, "Schemas type was 'Custom' which is not supported. Add a resource type to the definition of property: {0}", inputProperty.Name));
                    return(PropertyValidationOutcome.MissingResourceType);
                }
                else
                {
                    detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedTypeDifferent, null, "Type mismatch: property '{0}' [{1}] doesn't match expected type [{2}].",
                                                           inputProperty.Name, inputProperty.Type, schemaPropertyDef.Type));
                    return(PropertyValidationOutcome.InvalidType);
                }
            }
            else
            {
                // Check to see if this property is on the ignorable list
                string[] ignorableUndocumentedProperties = this.OriginalResource?.SourceFile.Parent.Requirements?.IgnorableProperties;

                string propertyName   = inputProperty.Name;
                string annotationName = null;
                var    indexOfAtSign  = propertyName.IndexOf('@');
                if (indexOfAtSign > 0)
                {
                    // [email protected] is an example of what we're looking for here
                    annotationName = propertyName.Substring(indexOfAtSign);
                    propertyName   = propertyName.Substring(0, indexOfAtSign);
                }


                if (null != annotationName)
                {
                    // Check to see if propertyName is known or not. If it isn't known, fail.
                    if (this.Properties.Any(x => x.Name.Equals(propertyName)))
                    {
                        // If the cleaned up propertyName is known, then check to see if the annotation is ignorable
                        if (null != ignorableUndocumentedProperties && ignorableUndocumentedProperties.Contains(annotationName))
                        {
                            // If we know of both the property and the annotation, we're good.
                            return(PropertyValidationOutcome.Ok);
                        }
                    }
                }

                if (null != ignorableUndocumentedProperties && ignorableUndocumentedProperties.Contains(propertyName))
                {
                    return(PropertyValidationOutcome.Ok);
                }

                // This property isn't documented
                detectedErrors.Add(new UndocumentedPropertyWarning(null, inputProperty.Name, inputProperty.Type, ResourceName));
                return(PropertyValidationOutcome.MissingFromSchema);
            }
        }
Beispiel #27
0
        private void ValidateObjectProperties(IEnumerable<JsonProperty> propertiesOnObject, ValidationOptions options, Dictionary<string, JsonSchema> otherSchemas, List<ValidationError> detectedErrors)
        {
            List<string> missingProperties = new List<string>();
            missingProperties.AddRange(from m in this.ExpectedProperties select m.Key);

            foreach(var property in propertiesOnObject)
            {
                missingProperties.Remove(property.Name);

                // This detects bad types, extra properties, etc.
                if (null != options && (property.IsArray || property.Type == JsonDataType.ODataType || property.Type == JsonDataType.Object))
                {
                    var propertyOptions = options.CreateForProperty(property.Name);
                    this.ValidateProperty(property, otherSchemas, detectedErrors, propertyOptions);
                }
                else
                {
                    this.ValidateProperty(property, otherSchemas, detectedErrors, options);
                }
            }

            this.CleanMissingProperties(options, missingProperties);
            if (missingProperties.Count > 0)
            {
                detectedErrors.Add(new ValidationError(ValidationErrorCode.RequiredPropertiesMissing, null, "Missing properties: response was missing these required properties: {0}", missingProperties.ComponentsJoinedByString(", ")));
            }
        }
        /// <summary>
        /// Check each member of the actualProperty's array to make sure it matches the resource type specified for the property.
        /// </summary>
        /// <param name="actualProperty"></param>
        /// <param name="schemas"></param>
        /// <param name="detectedErrors"></param>
        /// <param name="options"></param>
        private PropertyValidationOutcome ValidateArrayProperty(ParameterDefinition actualProperty, Dictionary <string, JsonSchema> schemas, List <ValidationError> detectedErrors, ValidationOptions options)
        {
            JArray actualArray = null;

            try
            {
                actualArray = (JArray)JsonConvert.DeserializeObject(actualProperty.OriginalValue);
            }
            catch (InvalidCastException)
            {
                throw new InvalidCastException($"Property {actualProperty.Name} expected to be an array, but failed to cast value to an array: {actualProperty.OriginalValue}");
            }

            var        expectedPropertyDefinition = this.ExpectedProperties[actualProperty.Name];
            JsonSchema memberSchema = null;

            if (actualProperty.Type.CollectionResourceType == SimpleDataType.Object && expectedPropertyDefinition.Type.CustomTypeName != null)
            {
                // We have an ambigious array, but we know what it's supposed to be so let's use that
                schemas.TryGetValue(expectedPropertyDefinition.Type.CustomTypeName, out memberSchema);
            }

            if (memberSchema == null && string.IsNullOrEmpty(actualProperty.Type.CustomTypeName))
            {
                return(this.ValidateSimpleArrayProperty(actualProperty, this.ExpectedProperties[actualProperty.Name], detectedErrors));
            }
            else if (memberSchema == null && !schemas.TryGetValue(actualProperty.Type.CustomTypeName, out memberSchema))
            {
                detectedErrors.Add(new ValidationError(ValidationErrorCode.ResourceTypeNotFound, null, "Failed to locate resource definition for: {0}", actualProperty.Type.CustomTypeName));
                return(PropertyValidationOutcome.MissingResourceType);
            }

            bool hadErrors = false;

            for (int i = 0; i < actualArray.Count; i++)
            {
                JContainer member = actualArray[i] as JContainer;
                if (member != null)
                {
                    List <ValidationError> memberErrors = new List <ValidationError>();
                    memberSchema.ValidateContainerObject(member, options, schemas, memberErrors);

                    // TODO: Filter out non-unique errors
                    hadErrors |= memberErrors.Count > 0;
                    detectedErrors.AddUniqueErrors(memberErrors);
                }
            }

            return(hadErrors ? PropertyValidationOutcome.GenericError : PropertyValidationOutcome.Ok);
        }
        /// <summary>
        /// Validates the value of json according to an implicit schmea defined by expectedJson
        /// </summary>
        /// <param name="expectedResponseAnnotation"></param>
        /// <param name="actualResponseBodyJson"></param>
        /// <param name="errors"></param>
        /// <returns></returns>
        public bool ValidateJsonExample(CodeBlockAnnotation expectedResponseAnnotation, string actualResponseBodyJson, out ValidationError[] errors, ValidationOptions options = null)
        {
            List<ValidationError> newErrors = new List<ValidationError>();

            var resourceType = expectedResponseAnnotation.ResourceType;
            if (resourceType == "stream")
            {
                // No validation since we're streaming data
                errors = null;
                return true;
            }
            else
            {
                JsonSchema schema;
                if (string.IsNullOrEmpty(resourceType))
                {
                    schema = JsonSchema.EmptyResponseSchema;
                }
                else if (!this.registeredSchema.TryGetValue(resourceType, out schema))
                {
                    newErrors.Add(new ValidationWarning(ValidationErrorCode.ResponseResourceTypeMissing, null, "Missing required resource: {0}. Validation limited to basics only.", resourceType));
                    // Create a new schema based on what's avaiable in the json
                    schema = new JsonSchema(actualResponseBodyJson, new CodeBlockAnnotation { ResourceType = expectedResponseAnnotation.ResourceType });
                }

                ValidationError[] validationJsonOutput;
                this.ValidateJsonCompilesWithSchema(schema, new JsonExample(actualResponseBodyJson, expectedResponseAnnotation), out validationJsonOutput, options: options);

                newErrors.AddRange(validationJsonOutput);
                errors = newErrors.ToArray();
                return errors.Length == 0;
            }
        }
        private static async Task ValidateMethodWithScenarioAsync(
            MethodDefinition method,
            ScenarioDefinition scenario,
            IServiceAccount account,
            ValidationResults results,
            ValidationOptions options = null)
        {
            if (null == method)
                throw new ArgumentNullException("method");
            if (null == scenario)
                throw new ArgumentNullException("scenario");
            if (null == account)
                throw new ArgumentNullException("account");
            if (null == results)
                throw new ArgumentNullException("results");

            var actionName = scenario.Description;

            // Check to see if the account + scenario scopes are aligned

            string[] requiredScopes = method.RequiredScopes.Union(scenario.RequiredScopes).ToArray();

            if (!account.Scopes.ProvidesScopes(requiredScopes, options.IgnoreRequiredScopes))
            {
                var missingScopes = from scope in requiredScopes where !account.Scopes.Contains(scope) select scope;

                results.AddResult(actionName,
                    new ValidationWarning(ValidationErrorCode.RequiredScopesMissing,
                    null,
                    "Scenario was not run. Scopes required were not available: {0}", missingScopes.ComponentsJoinedByString(",")));
                return;
            }

            // Generate the tested request by "previewing" the request and executing
            // all test-setup procedures
            long startTicks = DateTimeOffset.UtcNow.Ticks;
            var requestPreviewResult = await method.GenerateMethodRequestAsync(scenario, method.SourceFile.Parent, account);
            TimeSpan generateMethodDuration = new TimeSpan(DateTimeOffset.UtcNow.Ticks - startTicks);
            
            // Check to see if an error occured building the request, and abort if so.
            var generatorResults = results[actionName /*+ " [test-setup requests]"*/];
            generatorResults.AddResults(requestPreviewResult.Messages, requestPreviewResult.IsWarningOrError ? ValidationOutcome.Error :  ValidationOutcome.Passed);
            generatorResults.Duration = generateMethodDuration;

            if (requestPreviewResult.IsWarningOrError)
            {
                return;
            }

            // We've done all the test-setup work, now we have the real request to make to the service
            HttpRequest requestPreview = requestPreviewResult.Value;

            results.AddResult(
                actionName,
                new ValidationMessage(null, "Generated Method HTTP Request:\r\n{0}", requestPreview.FullHttpText()));

            HttpParser parser = new HttpParser();
            HttpResponse expectedResponse = null;
            if (!string.IsNullOrEmpty(method.ExpectedResponse))
            {
                expectedResponse = parser.ParseHttpResponse(method.ExpectedResponse);
            }

            // Execute the actual tested method (the result of the method preview call, which made the test-setup requests)
            startTicks = DateTimeOffset.UtcNow.Ticks;
            var actualResponse = await requestPreview.GetResponseAsync(account.BaseUrl);
            TimeSpan actualMethodDuration = new TimeSpan(DateTimeOffset.UtcNow.Ticks - startTicks);

            var requestResults = results[actionName];
            if (actualResponse.RetryCount > 0)
            {
                requestResults.AddResults(
                    new ValidationError[]
                    { new ValidationWarning(ValidationErrorCode.RequestWasRetried, null, "HTTP request was retried {0} times.", actualResponse.RetryCount) });
            }


            requestResults.AddResults(
                new ValidationError[]
                { new ValidationMessage(null, "HTTP Response:\r\n{0}", actualResponse.FullText(false)) });
            requestResults.Duration = actualMethodDuration;
           

            // Perform validation on the method's actual response
            ValidationError[] errors;
            method.ValidateResponse(actualResponse, expectedResponse, scenario, out errors, options);

            requestResults.AddResults(errors);

            // TODO: If the method is defined as a long running operation, we need to go poll the status 
            // URL to make sure that the operation finished and the response type is valid.

            if (errors.WereErrors())
                results.SetOutcome(actionName, ValidationOutcome.Error);
            else if (errors.WereWarnings())
                results.SetOutcome(actionName, ValidationOutcome.Warning);
            else
                results.SetOutcome(actionName, ValidationOutcome.Passed);
        }