Beispiel #1
0
        /// <summary>
        /// Check for response types according to operation/global parameters.
        /// </summary>
        /// <param name="validationOptions">The validation options.</param>
        /// <param name="path">The path.</param>
        public static List <LogKeyValueItem> ValidateGetOperations(
            ApiOptionsValidation validationOptions,
            KeyValuePair <string, OpenApiPathItem> path)
        {
            if (validationOptions == null)
            {
                throw new ArgumentNullException(nameof(validationOptions));
            }

            var logItems    = new List <LogKeyValueItem>();
            var logCategory = validationOptions.StrictMode
                ? LogCategoryType.Error
                : LogCategoryType.Warning;

            foreach (var(key, value) in path.Value.Operations)
            {
                if (key != OperationType.Get || (path.Value.Parameters.All(x => x.In != ParameterLocation.Path) &&
                                                 value.Parameters.All(x => x.In != ParameterLocation.Path)))
                {
                    continue;
                }

                var httpStatusCodes = value.Responses.GetHttpStatusCodes();
                if (!httpStatusCodes.Contains(HttpStatusCode.NotFound))
                {
                    logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation14, $"Missing NotFound response type for operation '{value.GetOperationName()}', required by url parameter."));
                }
            }

            return(logItems);
        }
Beispiel #2
0
        /// <summary>
        /// Check global parameters.
        /// </summary>
        /// <param name="validationOptions">The validation options.</param>
        /// <param name="globalPathParameterNames">The global path parameter names.</param>
        /// <param name="path">The path.</param>
        public static List <LogKeyValueItem> ValidateGlobalParameters(
            ApiOptionsValidation validationOptions,
            IEnumerable <string> globalPathParameterNames,
            KeyValuePair <string, OpenApiPathItem> path)
        {
            if (validationOptions == null)
            {
                throw new ArgumentNullException(nameof(validationOptions));
            }

            if (globalPathParameterNames == null)
            {
                throw new ArgumentNullException(nameof(globalPathParameterNames));
            }

            var logItems    = new List <LogKeyValueItem>();
            var logCategory = validationOptions.StrictMode
                ? LogCategoryType.Error
                : LogCategoryType.Warning;

            foreach (var pathParameterName in globalPathParameterNames)
            {
                if (!path.Key.Contains(pathParameterName, StringComparison.OrdinalIgnoreCase))
                {
                    logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation11, $"Defined global path parameter '{pathParameterName}' does not exist in route '{path.Key}'."));
                }
            }

            return(logItems);
        }
Beispiel #3
0
        /// <summary>
        /// Check for operations that are not defining parameters, which are present in the path.key.
        /// </summary>
        /// <param name="validationOptions">The validation options.</param>
        /// <param name="path">The path.</param>
        public static List <LogKeyValueItem> ValidateMissingOperationParameters(
            ApiOptionsValidation validationOptions,
            KeyValuePair <string, OpenApiPathItem> path)
        {
            if (validationOptions == null)
            {
                throw new ArgumentNullException(nameof(validationOptions));
            }

            var logItems    = new List <LogKeyValueItem>();
            var logCategory = validationOptions.StrictMode
                ? LogCategoryType.Error
                : LogCategoryType.Warning;

            if (!path.Key.Contains('{', StringComparison.Ordinal) ||
                !path.Key.IsStringFormatParametersBalanced(false))
            {
                return(logItems);
            }

            var parameterNamesToCheckAgainst    = GetParameterListFromPathKey(path.Key);
            var allOperationsParametersFromPath = GetAllOperationsParametersFromPath(path.Value.Operations);
            var distinctOperations = allOperationsParametersFromPath
                                     .Select(x => x.Item1)
                                     .Distinct()
                                     .ToList();

            foreach (var parameterName in parameterNamesToCheckAgainst)
            {
                var allOperationWithTheMatchingParameterName = allOperationsParametersFromPath
                                                               .Where(x => x.Item2.Equals(parameterName, StringComparison.OrdinalIgnoreCase))
                                                               .ToList();

                if (distinctOperations.Count != allOperationWithTheMatchingParameterName.Count)
                {
                    var operationsWithMissingParameter = allOperationsParametersFromPath
                                                         .Where(x => string.IsNullOrEmpty(x.Item2))
                                                         .Select(x => x.Item1)
                                                         .ToList();

                    logItems.Add(operationsWithMissingParameter.Count == 0
                        ? LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation12, $"The operations in path '{path.Key}' does not define a parameter named '{parameterName}'.")
                        : LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation12, $"The operations '{string.Join(',', operationsWithMissingParameter)}' in path '{path.Key}' does not define a parameter named '{parameterName}'."));
                }
            }

            return(logItems);
        }
Beispiel #4
0
        /// <summary>
        /// Check for operations with parameters, that are not present in the path.key.
        /// </summary>
        /// <param name="validationOptions">The validation options.</param>
        /// <param name="path">The path.</param>
        public static List <LogKeyValueItem> ValidateOperationsWithParametersNotPresentInPath(
            ApiOptionsValidation validationOptions,
            KeyValuePair <string, OpenApiPathItem> path)
        {
            if (validationOptions == null)
            {
                throw new ArgumentNullException(nameof(validationOptions));
            }

            var logItems    = new List <LogKeyValueItem>();
            var logCategory = validationOptions.StrictMode
                ? LogCategoryType.Error
                : LogCategoryType.Warning;

            var openApiOperationsWithPathParameter = path.Value.Operations.Values
                                                     .Where(x => x.Parameters.Any(p => p.In == ParameterLocation.Path))
                                                     .ToList();

            if (!openApiOperationsWithPathParameter.Any())
            {
                return(logItems);
            }

            var operationPathParameterNames = new List <string>();

            foreach (var openApiOperation in openApiOperationsWithPathParameter)
            {
                operationPathParameterNames.AddRange(openApiOperation.Parameters
                                                     .Where(x => x.In == ParameterLocation.Path)
                                                     .Select(openApiParameter => openApiParameter.Name));
            }

            if (!operationPathParameterNames.Any())
            {
                return(logItems);
            }

            foreach (var operationParameterName in operationPathParameterNames)
            {
                if (!path.Key.Contains(operationParameterName, StringComparison.OrdinalIgnoreCase))
                {
                    logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation13, $"Defined path parameter '{operationParameterName}' does not exist in route '{path.Key}'."));
                }
            }

            return(logItems);
        }
Beispiel #5
0
        private static List <LogKeyValueItem> ValidateOperationsParametersAndResponses(
            ApiOptionsValidation validationOptions,
            Dictionary <string, OpenApiPathItem> .ValueCollection paths)
        {
            var logItems    = new List <LogKeyValueItem>();
            var logCategory = validationOptions.StrictMode
                ? LogCategoryType.Error
                : LogCategoryType.Warning;

            foreach (var path in paths)
            {
                foreach (var(_, value) in path.Operations)
                {
                    var httpStatusCodes = value.Responses.GetHttpStatusCodes();
                    if (httpStatusCodes.Contains(HttpStatusCode.BadRequest) &&
                        !value.HasParametersOrRequestBody() && !path.HasParameters())
                    {
                        logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation10, $"Contains BadRequest response type for operation '{value.GetOperationName()}', but has no parameters."));
                    }

                    foreach (var parameter in value.Parameters)
                    {
                        switch (parameter.In)
                        {
                        case ParameterLocation.Path:
                            if (!parameter.Required)
                            {
                                logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation15, $"Path parameter '{parameter.Name}' for operation '{value.GetOperationName()}' is missing required=true."));
                            }

                            if (parameter.Schema.Nullable)
                            {
                                logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation16, $"Path parameter '{parameter.Name}' for operation '{value.GetOperationName()}' must not be nullable."));
                            }

                            break;

                        case ParameterLocation.Query:
                            break;
                        }
                    }
                }
            }

            return(logItems);
        }
Beispiel #6
0
        private static List <LogKeyValueItem> ValidateSchemaModelPropertyNameCasing(
            ApiOptionsValidation validationOptions,
            string key,
            OpenApiSchema schema)
        {
            var logItems    = new List <LogKeyValueItem>();
            var logCategory = validationOptions.StrictMode
                ? LogCategoryType.Error
                : LogCategoryType.Warning;

            if (!key.IsCasingStyleValid(validationOptions.ModelPropertyNameCasingStyle))
            {
                logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema07, $"Object '{schema.Title}' with property '{key}' is not using {validationOptions.ModelPropertyNameCasingStyle}."));
            }

            return(logItems);
        }
Beispiel #7
0
        private static List <LogKeyValueItem> ValidateSchemaModelNameCasing(
            ApiOptionsValidation validationOptions,
            OpenApiSchema schema)
        {
            var logItems    = new List <LogKeyValueItem>();
            var logCategory = validationOptions.StrictMode
                ? LogCategoryType.Error
                : LogCategoryType.Warning;

            var modelName = schema.GetModelName(false);

            if (!modelName.IsCasingStyleValid(validationOptions.ModelNameCasingStyle))
            {
                logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema06, $"Object '{modelName}' is not using {validationOptions.ModelNameCasingStyle}."));
            }

            return(logItems);
        }
Beispiel #8
0
        private static List <LogKeyValueItem> ValidatePathsAndOperations(
            ApiOptionsValidation validationOptions,
            OpenApiPaths paths)
        {
            var logItems    = new List <LogKeyValueItem>();
            var logCategory = validationOptions.StrictMode
                ? LogCategoryType.Error
                : LogCategoryType.Warning;

            foreach (var path in paths)
            {
                if (!path.Key.IsStringFormatParametersBalanced(false))
                {
                    logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Path01, $"Path parameters are not well-formatted for '{path.Key}'."));
                }

                var globalPathParameterNames = path.Value.Parameters
                                               .Where(x => x.In == ParameterLocation.Path)
                                               .Select(x => x.Name)
                                               .ToList();

                if (globalPathParameterNames.Any())
                {
                    logItems.AddRange(ValidatePathsAndOperationsHelper.ValidateGlobalParameters(validationOptions, globalPathParameterNames, path));
                }
                else
                {
                    logItems.AddRange(ValidatePathsAndOperationsHelper.ValidateMissingOperationParameters(validationOptions, path));
                    logItems.AddRange(ValidatePathsAndOperationsHelper.ValidateOperationsWithParametersNotPresentInPath(validationOptions, path));
                }

                logItems.AddRange(ValidatePathsAndOperationsHelper.ValidateGetOperations(validationOptions, path));
            }

            return(logItems);
        }
Beispiel #9
0
        public static List <LogKeyValueItem> Validate(Tuple <OpenApiDocument, OpenApiDiagnostic, FileInfo> apiDocument, ApiOptionsValidation validationOptions)
        {
            if (apiDocument == null)
            {
                throw new ArgumentNullException(nameof(apiDocument));
            }

            if (validationOptions == null)
            {
                throw new ArgumentNullException(nameof(validationOptions));
            }

            var logItems = new List <LogKeyValueItem>();

            if (apiDocument.Item2.SpecificationVersion == OpenApiSpecVersion.OpenApi2_0)
            {
                logItems.Add(LogItemHelper.Create(LogCategoryType.Error, "#", "OpenApi 2.x is not supported."));
                return(logItems);
            }

            foreach (var diagnosticError in apiDocument.Item2.Errors)
            {
                if (diagnosticError.Message.EndsWith("#/components/schemas", StringComparison.Ordinal))
                {
                    continue;
                }

                var description = string.IsNullOrEmpty(diagnosticError.Pointer)
                    ? $"{diagnosticError.Message}"
                    : $"{diagnosticError.Message} <#> {diagnosticError.Pointer}";
                logItems.Add(LogItemHelper.Create(LogCategoryType.Error, ValidationRuleNameConstants.OpenApiCore, description));
            }

            logItems.AddRange(OpenApiDocumentValidationHelper.ValidateDocument(apiDocument.Item1, validationOptions));
            return(logItems);
        }
Beispiel #10
0
        private static List <LogKeyValueItem> ValidateSchemas(
            ApiOptionsValidation validationOptions,
            ICollection <OpenApiSchema> schemas)
        {
            var logItems    = new List <LogKeyValueItem>();
            var logCategory = validationOptions.StrictMode
                ? LogCategoryType.Error
                : LogCategoryType.Warning;

            foreach (var schema in schemas)
            {
                switch (schema.Type)
                {
                case OpenApiDataTypeConstants.Array:
                {
                    if (string.IsNullOrEmpty(schema.Title))
                    {
                        logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema01, $"Missing title on array type '{schema.Reference.ReferenceV3}'."));
                    }
                    else if (schema.Title.IsFirstCharacterLowerCase())
                    {
                        logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema02, $"Title on array type '{schema.Title}' is not starting with uppercase."));
                    }

                    logItems.AddRange(ValidateSchemaModelNameCasing(validationOptions, schema));
                    break;
                }

                case OpenApiDataTypeConstants.Object:
                {
                    if (string.IsNullOrEmpty(schema.Title))
                    {
                        logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema03, $"Missing title on object type '{schema.Reference.ReferenceV3}'."));
                    }
                    else if (schema.Title.IsFirstCharacterLowerCase())
                    {
                        logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema04, $"Title on object type '{schema.Title}' is not starting with uppercase."));
                    }

                    foreach (var(key, value) in schema.Properties)
                    {
                        if (value.Nullable && schema.Required.Contains(key))
                        {
                            logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema08, $"Nullable property '{key}' must not be present in required property list in type '{schema.Reference.ReferenceV3}'."));
                        }

                        switch (value.Type)
                        {
                        case OpenApiDataTypeConstants.Object:
                        {
                            if (!value.IsObjectReferenceTypeDeclared())
                            {
                                logItems.Add(LogItemHelper.Create(LogCategoryType.Error, ValidationRuleNameConstants.Schema10, $"Implicit object definition on property '{key}' in type '{schema.Reference.ReferenceV3}' is not supported."));
                            }

                            break;
                        }

                        case OpenApiDataTypeConstants.Array:
                        {
                            if (value.Items == null)
                            {
                                logItems.Add(LogItemHelper.Create(LogCategoryType.Error, ValidationRuleNameConstants.Schema11, $"Not specifying a data type for array property '{key}' in type '{schema.Reference.ReferenceV3}' is not supported."));
                            }
                            else
                            {
                                if (value.Items.Type == null)
                                {
                                    logItems.Add(LogItemHelper.Create(LogCategoryType.Error, ValidationRuleNameConstants.Schema09, $"Not specifying a data type for array property '{key}' in type '{schema.Reference.ReferenceV3}' is not supported."));
                                }

                                if (value.Items.Type != null && !value.IsArrayReferenceTypeDeclared() && !value.IsItemsOfSimpleDataType())
                                {
                                    logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema05, $"Implicit object definition on property '{key}' in array type '{schema.Reference.ReferenceV3}' is not supported."));
                                }
                            }

                            break;
                        }
                        }

                        logItems.AddRange(ValidateSchemaModelPropertyNameCasing(validationOptions, key, schema));
                    }

                    logItems.AddRange(ValidateSchemaModelNameCasing(validationOptions, schema));
                    break;
                }
                }
            }

            return(logItems);
        }
Beispiel #11
0
        private static List <LogKeyValueItem> ValidateOperations(
            ApiOptionsValidation validationOptions,
            OpenApiPaths paths,
            IDictionary <string, OpenApiSchema> modelSchemas)
        {
            var logItems    = new List <LogKeyValueItem>();
            var logCategory = validationOptions.StrictMode
                ? LogCategoryType.Error
                : LogCategoryType.Warning;

            foreach (var(pathKey, pathValue) in paths)
            {
                foreach (var(operationKey, operationValue) in pathValue.Operations)
                {
                    if (string.IsNullOrEmpty(operationValue.OperationId))
                    {
                        logItems.Add(LogItemHelper.Create(LogCategoryType.Error, ValidationRuleNameConstants.Operation01, $"Missing OperationId in path '{operationKey} # {pathKey}'."));
                    }
                    else
                    {
                        if (!operationValue.OperationId.IsCasingStyleValid(validationOptions.OperationIdCasingStyle))
                        {
                            logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation02, $"OperationId '{operationValue.OperationId}' is not using {validationOptions.OperationIdCasingStyle}."));
                        }

                        if (operationKey == OperationType.Get)
                        {
                            if (!operationValue.OperationId.StartsWith("Get", StringComparison.OrdinalIgnoreCase) &&
                                !operationValue.OperationId.StartsWith("List", StringComparison.OrdinalIgnoreCase))
                            {
                                logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation03, $"OperationId should start with the prefix 'Get' or 'List' for operation '{operationValue.GetOperationName()}'."));
                            }
                        }
                        else if (operationKey == OperationType.Post)
                        {
                            if (operationValue.OperationId.StartsWith("Delete", StringComparison.OrdinalIgnoreCase))
                            {
                                logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation04, $"OperationId should not start with the prefix 'Delete' for operation '{operationValue.GetOperationName()}'."));
                            }
                        }
                        else if (operationKey == OperationType.Put)
                        {
                            if (!operationValue.OperationId.StartsWith("Update", StringComparison.OrdinalIgnoreCase))
                            {
                                logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation05, $"OperationId should start with the prefix 'Update' for operation '{operationValue.GetOperationName()}'."));
                            }
                        }
                        else if (operationKey == OperationType.Patch)
                        {
                            if (!operationValue.OperationId.StartsWith("Patch", StringComparison.OrdinalIgnoreCase) &&
                                !operationValue.OperationId.StartsWith("Update", StringComparison.OrdinalIgnoreCase))
                            {
                                logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation06, $"OperationId should start with the prefix 'Update' for operation '{operationValue.GetOperationName()}'."));
                            }
                        }
                        else if (operationKey == OperationType.Delete &&
                                 !operationValue.OperationId.StartsWith("Delete", StringComparison.OrdinalIgnoreCase) &&
                                 !operationValue.OperationId.StartsWith("Remove", StringComparison.OrdinalIgnoreCase))
                        {
                            logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation07, $"OperationId should start with the prefix 'Delete' for operation '{operationValue.GetOperationName()}'."));
                        }
                    }
                }

                foreach (var(_, value) in pathValue.Operations)
                {
                    var modelSchema = value.GetModelSchema();
                    if (modelSchema != null)
                    {
                        if (value.OperationId.EndsWith("s", StringComparison.Ordinal))
                        {
                            if (!IsModelOfTypeArray(modelSchema, modelSchemas))
                            {
                                logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation08, $"OperationId '{value.GetOperationName()}' is not singular - Response model is defined as a single item."));
                            }
                        }
                        else
                        {
                            if (IsModelOfTypeArray(modelSchema, modelSchemas))
                            {
                                logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation09, $"OperationId '{value.GetOperationName()}' is not pluralized - Response model is defined as an array."));
                            }
                        }
                    }
                }
            }

            return(logItems);
        }
        private static List <LogKeyValueItem> ValidateOperationsParametersAndResponses(
            ApiOptionsValidation validationOptions,
            Dictionary <string, OpenApiPathItem> .ValueCollection paths)
        {
            var logItems    = new List <LogKeyValueItem>();
            var logCategory = validationOptions.StrictMode
                ? LogCategoryType.Error
                : LogCategoryType.Warning;

            foreach (var path in paths)
            {
                foreach (var(_, value) in path.Operations)
                {
                    var httpStatusCodes = value.Responses.GetHttpStatusCodes();
                    if (httpStatusCodes.Contains(HttpStatusCode.BadRequest) &&
                        !value.HasParametersOrRequestBody() && !path.HasParameters())
                    {
                        logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation10, $"Contains BadRequest response type for operation '{value.GetOperationName()}', but has no parameters."));
                    }

                    if (httpStatusCodes.Contains(HttpStatusCode.OK) && httpStatusCodes.Contains(HttpStatusCode.Created))
                    {
                        // We do not support both 200 and 201, since our ActionResult - implicit operators only supports 1 type.
                        logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation18, $"The operation '{value.GetOperationName()}' contains both 200 and 201, which is not supported."));
                    }

                    if (value.HasParametersOrRequestBody())
                    {
                        var schema = value.RequestBody?.Content.GetSchema();
                        if (schema != null && string.IsNullOrEmpty(schema.GetModelName()))
                        {
                            logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation17, $"RequestBody is defined without model for operation '{value.GetOperationName()}'."));
                        }
                    }

                    foreach (var parameter in value.Parameters)
                    {
                        switch (parameter.In)
                        {
                        case ParameterLocation.Path:
                            if (!parameter.Required)
                            {
                                logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation15, $"Path parameter '{parameter.Name}' for operation '{value.GetOperationName()}' is missing required=true."));
                            }

                            if (parameter.Schema.Nullable)
                            {
                                logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation16, $"Path parameter '{parameter.Name}' for operation '{value.GetOperationName()}' must not be nullable."));
                            }

                            break;

                        case ParameterLocation.Query:
                            break;
                        }
                    }
                }
            }

            return(logItems);
        }