diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index e66968de13..0b090f4335 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -680,7 +680,7 @@ private static void AddCustomHeadersToOperation(OpenApiOperation operation) /// /// This method adds the input parameters from the stored procedure definition to the OpenApi operation parameters. - /// A input parameter will be marked REQUIRED if default value is not available. + /// A input parameter will be marked REQUIRED when it is explicitly flagged in config or when no default value is available. /// private static void AddStoredProcedureInputParameters(OpenApiOperation operation, StoredProcedureDefinition spDefinition) { @@ -690,7 +690,7 @@ private static void AddStoredProcedureInputParameters(OpenApiOperation operation GetOpenApiQueryParameter( name: paramKey, description: "Input parameter for stored procedure arguments", - required: false, + required: parameterDefinition.Required ?? !parameterDefinition.HasConfigDefault, type: TypeHelper.GetJsonDataTypeFromSystemType(parameterDefinition.SystemType).ToString().ToLower() ) ); @@ -1135,9 +1135,9 @@ private static bool IsRequestBodyRequired(SourceDefinition sourceDef, bool consi StoredProcedureDefinition spDef = (StoredProcedureDefinition)sourceDef; foreach (KeyValuePair parameterMetadata in spDef.Parameters) { - // A parameter which does not have any of the following properties + // A parameter which is explicitly marked required or has no default value // results in the body being required so that a value can be provided. - if (!parameterMetadata.Value.HasConfigDefault) + if (parameterMetadata.Value.Required ?? !parameterMetadata.Value.HasConfigDefault) { requestBodyRequired = true; break; @@ -1422,7 +1422,10 @@ private static OpenApiSchema CreateSpRequestComponentSchema(Dictionary + { + new() { Name = "title", Default = "Sample Title" } + }, + KeyFields: null), + Fields: null, + GraphQL: new(Singular: null, Plural: null, Enabled: false), + Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS), + Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), + Mappings: null, + Relationships: null, + Description: "Stored procedure with a parameter default"); + + // Entity whose "title" parameter is explicitly marked required: false in config. + // Even though it has no default value, the explicit override must be honored so that only + // "id" (no default, no override) remains required. + Entity entity3 = new( + Source: new( + Object: "update_book_title", + EntitySourceType.StoredProcedure, + Parameters: new List + { + new() { Name = "title", Required = false } + }, + KeyFields: null), + Fields: null, + GraphQL: new(Singular: null, Plural: null, Enabled: false), + Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS), + Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), + Mappings: null, + Relationships: null, + Description: "Stored procedure with an explicit required override"); + + // Entity whose only parameter is explicitly marked required: false in config. + // Because no parameter is required, requestBody.Required and the GET query parameter's + // required flag must both be false, and the schema-level required set must be empty. + Entity entity4 = new( + Source: new( + Object: "get_publisher_by_id", + EntitySourceType.StoredProcedure, + Parameters: new List + { + new() { Name = "id", Required = false } + }, + KeyFields: null), + Fields: null, + GraphQL: new(Singular: null, Plural: null, Enabled: false), + Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS), + Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), + Mappings: null, + Relationships: null, + Description: "Stored procedure with all parameters marked required: false"); + Dictionary entities = new() { - { "sp1", entity1 } + { "sp1", entity1 }, + { "sp2", entity2 }, + { "sp3", entity3 }, + { "sp4", entity4 } }; _runtimeEntities = new(entities); @@ -74,13 +137,23 @@ public static void CreateEntities() /// /// Validates that the generated request body references stored procedure parameters /// and not result set columns. + /// Also validates that the request body schema component flags the expected parameters + /// as required: a parameter is required when it has no default value and is not explicitly + /// marked required: false in the runtime config. + /// Also validates the body-level required flag on the OpenApiRequestBody object, which + /// should be true when any parameter is required and false when all parameters are optional. /// /// Entity name /// Expected parameters in request body /// Expected parameter value types in request body. - [DataRow("sp1", new string[] { "title", "publisher_name" }, new string[] { "string", "string" }, DisplayName = "Validate request body parameters and parameter Json data types.")] + /// Expected parameters flagged as required in the schema component. + /// Expected value of the body-level required flag. + [DataRow("sp1", new string[] { "title", "publisher_name" }, new string[] { "string", "string" }, new string[] { "title", "publisher_name" }, true, DisplayName = "Parameters without defaults are all required.")] + [DataRow("sp2", new string[] { "title", "publisher_id" }, new string[] { "string", "integer" }, new string[] { "publisher_id" }, true, DisplayName = "Parameter with a config default is not required.")] + [DataRow("sp3", new string[] { "id", "title" }, new string[] { "integer", "string" }, new string[] { "id" }, true, DisplayName = "Parameter explicitly marked required: false is not required.")] + [DataRow("sp4", new string[] { "id" }, new string[] { "integer" }, new string[] { }, false, DisplayName = "All parameters marked required: false produces empty required set and optional request body.")] [DataTestMethod] - public void ValidateRequestBodyContents(string entityName, string[] expectedParameters, string[] expectedParametersJsonTypes) + public void ValidateRequestBodyContents(string entityName, string[] expectedParameters, string[] expectedParametersJsonTypes, string[] expectedRequiredParameters, bool expectedRequestBodyRequired) { Dictionary configuredOperations = ResolveConfiguredOperations(_runtimeEntities[entityName]); foreach (OperationType opType in configuredOperations.Keys) @@ -100,7 +173,17 @@ public void ValidateRequestBodyContents(string entityName, string[] expectedPara OpenApiReference schemaComponentReference = GetRequestBodyReference(requestBody); string expectedSchemaReferenceId = $"{entityName}{OpenApiDocumentor.SP_REQUEST_SUFFIX}"; + // Validate the body-level required flag. + Assert.AreEqual(expectedRequestBodyRequired, requestBody.Required, message: "Unexpected request body required value."); + ValidateOpenApiReferenceContents(schemaComponentReference, expectedSchemaReferenceId, expectedParameters, expectedParametersJsonTypes); + + // Validate that the expected parameters are marked as required in the schema component. + ISet requiredParameters = _openApiDocument.Components.Schemas[expectedSchemaReferenceId].Required ?? new HashSet(); + CollectionAssert.AreEquivalent( + expectedRequiredParameters, + requiredParameters.ToArray(), + message: "Unexpected required parameters in request body schema component."); } } @@ -149,6 +232,37 @@ public void OpenApiDocumentor_TagsIncludeEntityDescription() $"Expected tag for '{entityName}' with description '{expectedDescription}' not found."); } + /// + /// Validates that the generated GET operation query parameters for stored procedure entities + /// have the correct required flag: a parameter without a config default and not explicitly + /// marked required: false is required; a parameter explicitly marked required: false is not. + /// + /// Entity name. + /// Expected query parameter names. + /// Whether each corresponding query parameter is expected to be required. + [DataRow("sp1", new string[] { "title", "publisher_name" }, new bool[] { true, true }, DisplayName = "GET parameters without defaults are required.")] + [DataRow("sp4", new string[] { "id" }, new bool[] { false }, DisplayName = "GET parameter explicitly marked required: false is not required.")] + [DataTestMethod] + public void ValidateGetQueryParameters(string entityName, string[] expectedParameters, bool[] expectedRequired) + { + OpenApiOperation getOperation = _openApiDocument.Paths["/" + entityName].Operations[OperationType.Get]; + Assert.IsNotNull(getOperation, "GET operation not found."); + + // Filter to query parameters only (excludes the Authorization and X-MS-API-ROLE header parameters). + List spParams = getOperation.Parameters + .Where(p => p.In == ParameterLocation.Query) + .ToList(); + + Assert.AreEqual(expectedParameters.Length, spParams.Count, "Unexpected number of GET query parameters."); + + for (int i = 0; i < expectedParameters.Length; i++) + { + OpenApiParameter param = spParams.FirstOrDefault(p => p.Name == expectedParameters[i]); + Assert.IsNotNull(param, $"Parameter '{expectedParameters[i]}' not found in GET query parameters."); + Assert.AreEqual(expectedRequired[i], param.Required, $"Unexpected required value for GET query parameter '{expectedParameters[i]}'."); + } + } + /// /// Validates that the provided OpenApiReference object has the expected schema reference id /// and that that id is present in the list of component schema in the OpenApi document.